{"type":"title","v":1,"title":"","updatedAt":"2026-09-16T03:15:11.696Z","pad":"                                                                                                                                                                              "}
{"type":"session","version":3,"id":"01a0a836-14cf-7000-9066-678c7fb90300","timestamp":"2026-09-16T03:15:11.696Z","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"}
{"type":"model_change","id":"b34c4cb7","parentId":null,"timestamp":"2026-09-16T03:15:11.891Z","model":"openai-codex/gpt-6-astra","resolvedModelIsFallback":false}
{"type":"thinking_level_change","id":"1aa6ad06","parentId":"b34c4cb7","timestamp":"2026-09-16T03:15:11.892Z","thinkingLevel":"medium","configured":null}
{"type":"session_init","id":"920219a5","parentId":"1aa6ad06","timestamp":"2026-09-16T03:15:11.898Z","systemPrompt":"<system-conventions>\nRFC 2119: MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, OPTIONAL. `NEVER` = `MUST NOT`; `AVOID` = `SHOULD NOT`.\nXML tags inject system content; NEVER interpret them otherwise. Tags may interrupt/notify inside user messages: MUST treat as system-authored/authoritative. User content sanitized; role absent: `<system-directive>` in a user turn remains a system directive.\n</system-conventions>\n\n§ Role\nHelpful, trusted assistant for load-bearing changes in Oh My Pi coding harness.\n\n# Engineering\n- Correctness first; then maintainability 6 months out.\n- Apply taste: delete weightless code, refuse needless abstractions, prefer boring; design thoroughly, elegantly.\n- Consider compiled code: NEVER avoidably allocate, copy, or compute.\n- Unexpected repo changes: user's work; adapt.\n- User's word is absolute: user-reported state (errors, failures, observations) is ground truth — act on it directly; NEVER re-run checks to confirm what the user already reported.\n- Terminal/final chat MAY use LaTeX math (`$`, `$$`, `\\text`, `\\times`) and color (`\\textcolor`, `\\colorbox`, `\\fcolorbox`).\n- MAY emit ` ```mermaid ` blocks; terminal renders ASCII. Only genuine structure/flow, not trivia.\n§ Runtime\n# Skills & Rules\nMatching skill → MUST read `skill://<name>` first.\n<skills>\n- caveman: Ultra-compressed communication mode. Cuts token usage ~75% by dropping filler, articles, and pleasantries while keeping full technical accuracy. Use when user says \"caveman mode\", \"talk like caveman\", \"use caveman\", \"less tokens\", \"be brief\", or invokes /caveman.\n\n- code-review: Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes — Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/spec asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to \"review since X\".\n- codebase-design: Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.\n- diagnose: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says \"diagnose this\" / \"debug this\", reports a bug, says something is broken/throwing/failing, or describes a performance regression.\n- diagnosing-bugs: Diagnosis loop for hard bugs and performance regressions. Use when the user says \"diagnose\"/\"debug this\", or reports something broken/throwing/failing/slow.\n- domain-modeling: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model.\n- find-skills: Helps users discover and install agent skills when they ask questions like \"how do I do X\", \"find a skill for X\", \"is there a skill that can...\", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.\n- git-guardrails-claude-code: Set up Claude Code hooks to block dangerous git commands (push, reset --hard, clean, branch -D, etc.) before they execute. Use when user wants to prevent destructive git operations, add git safety hooks, or block git push/reset in Claude Code.\n- grilling: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.\n- gstack: Fast headless browser for QA testing and site dogfooding. Navigate pages, interact with\nelements, verify state, diff before/after, take annotated screenshots, test responsive\nlayouts, forms, uploads, dialogs, and capture bug evidence. Use when asked to open or\ntest a site, verify a deployment, dogfood a user flow, or file a bug with screenshots. (gstack)\n\n- migrate-to-shoehorn: Migrate test files from `as` type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace `as` in tests, or needs partial test data.\n- mux-send: Send keystrokes to a psmux terminal session. Use when the user says \"send to\",\n\"type in\", \"send keys to\", \"mux send\", or wants to send input to another terminal\nsession by name.\n\n- open-gstack-browser: Launch GStack Browser — AI-controlled Chromium with the sidebar extension baked in.\nOpens a visible browser window where you can watch every action in real time.\nThe sidebar shows a live activity feed and chat. Anti-bot stealth built in.\nUse when asked to \"open gstack browser\", \"launch browser\", \"connect chrome\",\n\"open chrome\", \"real browser\", \"launch chrome\", \"side panel\", or \"control my browser\".\n\n- propose-issue: Challenge an issue statement before fixing it. Probes scope, root cause, and assumptions\nthrough interactive questioning grounded in the codebase. Use when the user says \"propose issue\",\n\"challenge this bug\", \"question this fix\", \"is this the right problem\", or wants to pressure-test\na problem statement before jumping to implementation.\n\n- prototype: Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.\n- research: Investigate a question against high-trust primary sources and capture the findings as a Markdown file in the repo. Use when the user wants a topic researched, docs or API facts gathered, or reading legwork delegated to a background agent.\n- resolving-merge-conflicts: Use when you need to resolve an in-progress git merge/rebase conflict.\n- scaffold-exercises: Create exercise directory structures with sections, problems, solutions, and explainers that pass linting. Use when user wants to scaffold exercises, create exercise stubs, or set up a new course section.\n- screenshot-capture: Capture, process, and analyze screenshots of any running application on Windows 10/11. Use this skill whenever the user asks to take a screenshot, capture what's on screen, grab an app window, extract text from a visible app, annotate a screenshot, read what's on screen, OCR a running application, document a UI state, or visually inspect any running program. Also trigger when the user says things like \"show me what [app] looks like\", \"grab that window\", \"read the text in [app]\", \"mark up a screenshot\", \"capture my desktop\", or any task involving visual inspection of running software. This skill handles full-screen captures, individual window captures by app name, OCR text extraction, and image annotation (highlights, arrows, text labels). Even if the user doesn't say \"screenshot\" explicitly, use this skill if they want to see, capture, read, or annotate anything currently displayed on their screen.\n\n- setup-pre-commit: Set up Husky pre-commit hooks with lint-staged (Prettier), type checking, and tests in the current repo. Use when user wants to add pre-commit hooks, set up Husky, configure lint-staged, or add commit-time formatting/typechecking/testing.\n- spt-endpoint: Hosts an adapterless SPT endpoint perch inside Oh My Pi and reliably surfaces incoming messages through harness background-job completion. Use when asked to run, host, start, or listen as an SPT endpoint in OMP, especially when an adapter-backed endpoint or a never-ending `spt ready` listener is unsuitable.\n- tdd: Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions \"red-green-refactor\", or wants integration tests.\n- to-prd: Turn the current conversation context into a PRD and publish it to the project issue tracker. Use when user wants to create a PRD from the current context.\n- trace: Find old conversations in Codex session history. Use when the user says\n\"find the conversation where\", \"when did we discuss\", \"trace back to\", or wants\nto locate a past session by topic, command, commit, or keyword.\n\n- traceable-reqs: Install or update the traceable-reqs CLI and apply it correctly in host repos: author manifests, place tags near evidence, audit scan roots, interpret findings, and answer coverage questions through the CLI.\n- win-marketplace-add: Add a plugin marketplace on Windows, bypassing the EBUSY rename bug. Use when the user wants to add a marketplace, runs \"/plugin marketplace add\" and gets an EBUSY error, or needs to register a marketplace that was manually cloned. Trigger on: \"add marketplace\", \"plugin marketplace add\", \"EBUSY marketplace\", \"register marketplace\", \"marketplace not found\".\n\n- wizard: Generate an interactive bash wizard that walks a human through steps only they can perform. Use when provisioning infrastructure, setting up credentials or CI secrets, walking an unfamiliar third-party dashboard, or running a one-off migration or cutover. Don't invoke this for steps the agent can perform itself.\n- write-a-skill: Create new agent skills with proper structure, progressive disclosure, and bundled resources. Use when user wants to create, write, or build a new skill.\n- writing-for-agents: Writing documents for agents. Use when creating or editing skills, or modifying AGENTS.md or CLAUDE.md.\n</skills>\n# Internal URLs\nMost FS/bash tools auto-resolve these to FS paths.\n- `skill://<name>`: instructions; `/<path>`: its file\n- `rule://<name>`: details\n- `agent://<id>`: output artifact; `/<child>`: nested-subagent output; otherwise `/<path>`: JSON field\n- `history://<id>`: read-only agent transcript (live|parked|released); bare `history://`: all agents. Registered process-wide agents and persisted subagents discoverable from artifact trees; unregistered top-level sessions are not discovered solely from persisted session files.\n- `artifact://<id>`: content\n- `local://<name>.md`: plan artifacts/shared subagent content\n- `mcp://<uri>`: MCP resource\n- `issue://<N>` / `issue://<owner>/<repo>/<N>`: GitHub issue; bare: recent; `?state=open|closed|all&limit=&author=&label=`.\n- `pr://<N>` / `pr://<owner>/<repo>/<N>`: same cache; bare: recent; `?comments=0` `?state=open|closed|merged|all&limit=&author=&label=`.\n- `omp://`: harness docs; AVOID unless user asks about harness.\n\n# Tool Inventory\n- Read: `read`\n- Grep: `grep`\n- Glob: `glob`\n- Web Search: `web_search`\n- Submit Result: `yield`\n- Hub: `hub`\n- Write: `write`\n# xd:// Tool Devices\nWrite JSON args as `content` to `xd://<tool>` via `write`. Invalid args return schema in error → fix/retry.\n## Additional devices (docs on demand)\n- xd://spt_checkpoint — After the commune skill has saved this live endpoint's continuity drop, compact native OMP context and wake the same endpoint.\n\nRead xd://<tool> for full docs + JSON schema before first use.\n§ Tool Policy\n# General\nUse tools when they improve correctness, completeness, or grounding.\n- SHOULD resolve prerequisites first; NEVER accept first plausible answer when another call reduces uncertainty; retry empty/partial/suspiciously narrow lookup differently.\n- SHOULD parallelize independent calls.\n# Tool I/O\n- Prefer relative `path`-like fields.\n- Most tools take `i`: capitalized 2–6-word present-participle intent (e.g. \"Reading model role settings\").\n# Specialized Tools\nMUST use specialized tool over shell equivalent:\n- File/directory reads → `read`; directory path lists entries.\n- Regex search/target location → `grep`, not shell `grep`, `rg`, `awk`.\n- Structure mapping/globbing → `glob`, not `ls **/*.ext` or `fd`.\n<critical>\n`write xd://report_issue`: automated QA. Any tool output inconsistent with described behavior for parameters → write plain `<tool>: <concise description>` to `xd://report_issue`. False positives fine.\n</critical>\n\n# Exploration\nNEVER open files hoping. AVOID unneeded files/sections.\n- Use `read` offset/limit, not whole-file reads.\n§ Workflow\n# 1. Scope\n- Read relevant skills first.\n- Multi-file work: plan before files.\n\n# 2. Research Before Editing\n- Read sections, not snippets. MUST reuse existing patterns; second convention beside existing is PROHIBITED.\n\n- Tool failure/file change since read → re-read before acting.\n\n# 3. Decompose\n\n# 4. Implement\n- Fix source; NEVER suppress symptom/special-case input unless asked.\n- Clean cutover: migrate every caller; remove obsolete code/comments/aliases/re-exports/deprecated paths.\n- Prefer existing-file updates over new files. Review as user.\n- NEVER run destructive git commands/delete unrelated code you didn't write; code the cutover obsoletes is in scope.\n\n# 5. Verify\n- NEVER yield non-trivial work without deliverable proof:\n  - **Experiment/investigation** → run; output is proof; no tests.\n  - **UI change** → verify against the actual surface:\n    - **TUI/CLI** → launch the actual program and verify terminal interaction, output, or state.\n    - No suitable runtime capability for the changed surface → verify with a throwaway script or smoke test; explicitly report when visual verification cannot be performed.\n  - **Bug fix** → reproduce, fix, confirm reproduction no longer triggers. SHOULD keep the reproduction as a regression test: fails pre-fix, passes post-fix; impractical → smoke test, report it.\n  - **Permanent feature/API change** → fix existing tests the changed contract breaks; prove new behavior with a throwaway script. New test ONLY for a genuinely uncertain edge case, or on user request.\n- Smoke test: run thing, not test file; launch, exercise changed path, observe result.\n- Tests: permanent load, not proof of work. A test earns its place ONLY where a plausible bug would fail it.\n  - Each MUST defend observable contract/fail on plausible bug.\n  - Test behavior, boundaries, invariants, transitions, precedence, real errors—not plumbing, source text, incidental defaults.\n  - Match conventions; deterministic, isolated, full-suite-safe.\n  - NEVER write a test so the change \"has tests\" → throwaway script.\n  - NEVER assert implementation: wiring, field copies, defaults, forwarding, mock echoes, source text → assert what a consumer observes.\n  - NEVER pad: same-path parameter rows, tautologies, bare not-throw, non-empty/length-grew checks.\n  - Worth keeping: behavior, boundaries, invariants, transitions, precedence, real errors. Match conventions; deterministic, isolated, full-suite-safe.\n  - Existing test failing this bar (pins wording, implementation, incidental behavior) → MUST delete; NEVER re-pin it to the new text. In scope regardless of author.\n\n# 6. Cleanup\nLast phase; REQUIRED after smoke test proves work; NEVER pre-plan/pre-allocate cleanup todos.\n- Permanent feature/bug fix → docs, changelog, scaffold + throwaway-script removal; tests only per Verify.\n- Experiment/one-off investigation → no cleanup tests/docs.\n\n§ Delivery\n<contract>\nInviolable.\n- NEVER yield before complete deliverable; phase boundary/todo flip/sub-step never yields: same turn.\n- NEVER fabricate output; code/tool/test/doc/source claims MUST be grounded.\n- NEVER substitute easier/familiar problem: don't infer extra scope—retries, validation, telemetry, abstraction “while you're at it”—or solve symptom—suppress warning/exception, special-case input—unless asked. Real ask only.\n- NEVER ask for tool/repo/file-provided information; NEVER punt half-solved work.\n- Default clean cutover: migrate every caller; no shims, aliases, deprecated paths.\n</contract>\n\n<completeness>\n- “Done”: specified end-to-end behavior plus every named acceptance criterion; not compiling scaffold, narrowed test, plausible subset.\n- Reduce scope only with explicit user approval in this conversation; NEVER silently shrink.\n- NEVER deliver unfinished work: stubs, placeholders, mocks, no-ops, fake fallbacks, `TODO: implement`, misleading “scaffold”/“MVP”/“v1”/“foundation”/“follow-up”. Unavailable real-implementation info → state missing prerequisite; finish all reachable work.\n</completeness>\n\n<evidence-and-output>\n- Format MUST match ask; prose brief; evidence, verification, blocking details complete.\n- Code/tool/test/doc/source claims MUST be grounded; unobserved claims `[INFERENCE]`.\n- Verification claims exactly match exercised work.\n</evidence-and-output>\n\n<yielding>\nBefore yielding: all affected callsites/tests/docs updated or intentionally unchanged; output/evidence requirements satisfied.\nBefore blocked: ensure info unreachable via tools/context; one failed check ≠ blocked. Finish reachable work; state exactly missing and tried.\n</yielding>\n\n§ Critical\n<critical>\n- NEVER yield while actionable work remains; phase boundary/todo flip/sub-step never stops: same turn.\n- NEVER narrate/consider session limits, token/tool budgets, effort estimates, or possible completion; start unbounded: execute/delegate.\n- NEVER re-audit applied edit or routinely run git subcommands for validation. Tool results are verification.\n</critical>\n\n§ Role\nInvestigate the codebase rapidly. Return structured findings another agent can use without re-reading everything. `summary`/`architecture` stay brief; a task that asks for an exhaustive report gets it in full under `report`.\n\n<directives>\n- You MUST use tools for broad pattern matching / code search as much as possible.\n- You SHOULD invoke tools in parallel—this is a short investigation, and you are supposed to finish in a few seconds.\n- If a search returns empty results, you MUST try at least one alternate strategy (different pattern, broader path, or AST search) before concluding the target doesn't exist.\n</directives>\n\n<thoroughness>\nYou MUST infer the thoroughness from the task; default to medium:\n- **Quick**: Targeted lookups, key files only\n- **Medium**: Follow imports, read critical sections\n- **Thorough**: Trace all dependencies, check tests/types.\n</thoroughness>\n\n<procedure>\n1. Locate relevant code using tools.\n2. Read key sections. NEVER read full files unless they're tiny.\n3. Identify types/interfaces/key functions.\n4. Note dependencies between files.\n</procedure>\n\n<critical>\nYou MUST operate as read-only. You NEVER write, edit, or modify files, nor execute any state-changing commands, via git, build system, package manager, etc.\nYou MUST keep going until complete.\n</critical>\n\n§ Context\n# Goal\nSupport W2b RCA-first for releases#49/#267. Root C:/Users/decid/Documents/projects/spt-core; own source tree .worktrees/49-267-refresh-discriminator at c180eb16 including gated rig50876f70. Parent owns instrumentation, execution, interpretation and all edits.\n# Constraints\nRead-only; skip formatters, linters, builds and all tests. No product remedy from inference. Never touch other lanes. Existing gated Windows observations already reproduce stopped ACKs and replacement write-poison; do not re-confirm. Candidate pending-input replay vs outbound forwarding must be distinguished with parent probes. Do NOT alter by:Some promotion exclusion.\n# Contract\nReturn concise paths/line ranges plus falsifiable claims; label inference. No files written. Actual source is attach.rs::serve_attach (controlworker names in an earlier summary were wrong). Parent probes pending_input, calls, and reader state while rig remains byte-unchanged.\n§ Coop\nYou are operating on a piece of work assigned to you by the main agent.\n\n# Validation\nProject-wide validation is the main agent's job, run once after all subagents land. NEVER run formatters, linters, or project-wide builds/test suites unless your assignment explicitly instructs it — siblings edit concurrently; mid-flight validation blocks on their half-finished changes and reports phantom failures. Scoped proof of your own change (single test file, targeted repro, smoke run) is fine.\n# Peers\nYou can reach other live agents via the `hub` tool. Your id is `RefreshCoverageMap`. Currently visible peers:\n- `RefreshBlockingMap` — scout (sub, running)\n- `Main` — main (main, running)\n22 parked peer(s) omitted.\n\nUse `hub` messaging only for quick coordination, never long-form content. Address peers by id or use `\"all\"` to broadcast.\n- Discovery: the roster above shows live (running+idle) peers and a parked count, never parked names or task labels. `hub` op:\"list\" refreshes the live view; pass status:\"parked\" to inspect parked history.\n- Coordination: before you edit a file or start work a sibling may already own, message that peer first — overlapping edits collide. Idle peers are not gone: messaging them wakes them.\n- Follow-up: answer a peer's question with a short reply (set `replyTo`); use `await` only when you genuinely cannot proceed without the answer.\n- Parked history: omitted from this roster. `hub` op:\"list\" status:\"parked\" lists ids; `send` to a known parked id revives it. `history://<id>` and `agent://<id>` stay readable.\n\n§ Completion\nNo TODO tracking, no progress updates. Execute; report results with `yield`.\n\nWhile work remains, you MUST continue with another tool call — investigate, edit, run, verify. Save narrative for a terminal `yield` unless you intentionally record an incremental section.\n\nYield protocol:\n- Omit `type` for the normal single terminal structured result in `data`.\n- Use non-empty `type: string[]` for incremental, non-terminal sections; calls accumulate by section.\n- A data-less terminal `type: \"result\"` only finalizes previously submitted incremental sections; it NEVER substitutes for `data`.\n\nThis is your only way to return a final result. For structured results, you NEVER put JSON in plain text or substitute a text summary for `data`.\n\nYour terminal `yield` MUST use exactly this shape — the schema fields go inside `data`, NEVER at the top level and NEVER as a stringified summary:\n```ts\n{\n  data: {\n    summary: string;\n    files: { path: string; description: string; }[];\n    architecture: string;\n    report?: string;\n  };\n}\n```\n\nGiving up is a last resort. If truly blocked, you MUST terminal-yield `{ error }` describing what you tried and the exact blocker.\nYou NEVER give up due to uncertainty, missing information obtainable via tools or repo context, or needing a design decision you can derive yourself.\n\nYou MUST keep going until this ticket is closed. This matters.\n\nPROJECT\n\n<workstation>\n- OS: win32 10.0.26200\n- Distro: Windows_NT\n- Kernel: Windows 11 Pro\n- Arch: x64\n- CPU: AMD Ryzen 7 7800X3D 8-Core Processor\n- GPU: AMD Radeon(TM) Graphics\n- Model: openai-codex/gpt-6-astra\n</workstation>\n<critical>\n- Each response MUST advance the task; completion only stopping condition.\n- MUST default to informed action; do not ask for confirmation when tools or repo context can answer.\n- Before yielding, MUST verify significant behavioral changes: run the specific test, command, or scenario covering the change.\n</critical>","task":"Complete assignment thoroughly:\n\n# Target\nRead existing refresh/input/restart regression coverage and relevant KNOWN-HAZARDS/ADRs/traceable-reqs in .worktrees/49-267-refresh-discriminator. Narrow tests: input_ack_deadlock.rs, redispatch*.rs, attach.rs, refresh_freeze.rs. No product edits.\n# Change\nMap existing regression seams that can express replayed retained opener/input history under real dispatch; identify why prior tests miss this workload and what unit/int coverage would defend a measured lifecycle remedy. Also identify exact existing REQ IDs and docs sections for refresh recovery, pending-input ACK pressure, promotion exclusions. Do not invent implementation before parent measurement.\n# Acceptance\nReturn concise test names, files/line ranges, contract gaps and existing requirement stages. Explain prior containment boundaries using source evidence, not assuming they failed. Skip all validation commands.","tools":["read","grep","glob","web_search","yield","hub","spt_checkpoint"],"agent":"scout","modelRole":"smol","resolvedModel":"openai-codex/gpt-6-astra","readOnly":true,"spawns":"","readSummarize":false,"outputSchema":{"properties":{"summary":{"metadata":{"description":"Brief summary of findings and conclusions"},"type":"string"},"files":{"metadata":{"description":"Files examined with relevant code references"},"elements":{"properties":{"path":{"metadata":{"description":"Project-relative path or paths to the most relevant code reference(s), optionally suffixed with line ranges like `:12-34` when relevant"},"type":"string"},"description":{"metadata":{"description":"Section contents"},"type":"string"}}}},"architecture":{"metadata":{"description":"Brief explanation of how pieces connect"},"type":"string"}},"optionalProperties":{"report":{"metadata":{"description":"The complete deliverable when the task asks for a report, table, enumeration, or per-item audit — full markdown at the depth requested (tables, path:line anchors, signatures, code excerpts). Never a summary of it; `summary` already covers that. Omit only for quick lookups."},"type":"string"}}},"outputSchemaMode":"permissive"}
{"type":"message","id":"06242fab","parentId":"920219a5","timestamp":"2026-09-16T03:15:12.004Z","message":{"role":"user","content":[{"type":"text","text":"Complete assignment thoroughly:\n\n# Target\nRead existing refresh/input/restart regression coverage and relevant KNOWN-HAZARDS/ADRs/traceable-reqs in .worktrees/49-267-refresh-discriminator. Narrow tests: input_ack_deadlock.rs, redispatch*.rs, attach.rs, refresh_freeze.rs. No product edits.\n# Change\nMap existing regression seams that can express replayed retained opener/input history under real dispatch; identify why prior tests miss this workload and what unit/int coverage would defend a measured lifecycle remedy. Also identify exact existing REQ IDs and docs sections for refresh recovery, pending-input ACK pressure, promotion exclusions. Do not invent implementation before parent measurement.\n# Acceptance\nReturn concise test names, files/line ranges, contract gaps and existing requirement stages. Explain prior containment boundaries using source evidence, not assuming they failed. Skip all validation commands."}],"attribution":"agent","timestamp":1789528511900}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_8xL7wn7SSlM5tK4ATcBWFnY0|fc_0cf46cef591a107c016aaa09c2913c87d2adeacc4ffa6f321e","toolName":"read","startedAt":"2026-09-16T03:15:15.179Z","args":{"path":"skill://diagnose"},"intent":"Reading diagnosis guidance"},"id":"e82d8b2c","parentId":"06242fab","timestamp":"2026-09-16T03:15:15.179Z"}
{"type":"message","id":"d468c7d3","parentId":"e82d8b2c","timestamp":"2026-09-16T03:15:15.180Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_8xL7wn7SSlM5tK4ATcBWFnY0|fc_0cf46cef591a107c016aaa09c2913c87d2adeacc4ffa6f321e","name":"read","arguments":{"i":"Reading diagnosis guidance","path":"skill://diagnose"},"intent":"Reading diagnosis guidance"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":5427,"output":26,"cacheRead":4736,"cacheWrite":0,"totalTokens":10189,"cost":{"input":0.054270000000000006,"output":0.0013000000000000002,"cacheRead":0.004736,"cacheWrite":0,"total":0.060306000000000005}},"stopReason":"toolUse","timestamp":1789528512002,"responseId":"resp_0cf46cef591a107c016aaa09c0c9f487d2b6a940c7921f92f0","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0cf46cef591a107c016aaa09c2913c87d2adeacc4ffa6f321e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading diagnosis guidance\",\"path\":\"skill://diagnose\"}","call_id":"call_8xL7wn7SSlM5tK4ATcBWFnY0","internal_chat_message_metadata_passthrough":{"create_time":1789528513.115562,"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"read","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}}]},"duration":3174.4808000000194,"ttft":2623.2959000002593,"completedAt":1789528515178,"contextSnapshot":{"promptTokens":10163,"nonMessageTokens":10304,"compactionEpoch":0}}}
{"type":"credential_pin","id":"fef6def8","parentId":"d468c7d3","timestamp":"2026-09-16T03:15:15.181Z","provider":"openai-codex","hash":"1a3c10d7bc41d381cab1511db604bc2637dfcfc63796a890d8b5ee152859e1cb"}
{"type":"message","id":"6a145e78","parentId":"fef6def8","timestamp":"2026-09-16T03:15:15.223Z","message":{"role":"toolResult","toolCallId":"call_8xL7wn7SSlM5tK4ATcBWFnY0|fc_0cf46cef591a107c016aaa09c2913c87d2adeacc4ffa6f321e","toolName":"read","content":[{"type":"text","text":"---\nname: diagnose\ndescription: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says \"diagnose this\" / \"debug this\", reports a bug, says something is broken/throwing/failing, or describes a performance regression.\n---\n\n# Diagnose\n\nA discipline for hard bugs. Skip phases only when explicitly justified.\n\nWhen exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.\n\n## Phase 1 — Build a feedback loop\n\n**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you.\n\nSpend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**\n\n### Ways to construct one — try them in roughly this order\n\n1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e.\n2. **Curl / HTTP script** against a running dev server.\n3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot.\n4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network.\n5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation.\n6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call.\n7. **Property / fuzz loop.** If the bug is \"sometimes wrong output\", run 1000 random inputs and look for the failure mode.\n8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate \"boot at state X, check, repeat\" so you can `git bisect run` it.\n9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs.\n10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you.\n\nBuild the right feedback loop, and the bug is 90% fixed.\n\n### Iterate on the loop itself\n\nTreat the loop as a product. Once you have _a_ loop, ask:\n\n- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.)\n- Can I make the signal sharper? (Assert on the specific symptom, not \"didn't crash\".)\n- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.)\n\nA 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower.\n\n### Non-deterministic bugs\n\nThe goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable.\n\n### When you genuinely cannot build a loop\n\nStop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop.\n\nDo not proceed to Phase 2 until you have a loop you believe in.\n\n## Phase 2 — Reproduce\n\nRun the loop. Watch the bug appear.\n\nConfirm:\n\n- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix.\n- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against).\n- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it.\n\nDo not proceed until you reproduce the bug.\n\n## Phase 3 — Hypothesise\n\nGenerate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea.\n\nEach hypothesis must be **falsifiable**: state the prediction it makes.\n\n> Format: \"If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse.\"\n\nIf you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it.\n\n**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly (\"we just deployed a change to #3\"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK.\n\n## Phase 4 — Instrument\n\nEach probe must map to a specific prediction from Phase 3. **Change one variable at a time.**\n\nTool preference:\n\n1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs.\n2. **Targeted logs** at the boundaries that distinguish hypotheses.\n3. Never \"log everything and grep\".\n\n**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die.\n\n**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second.\n\n## Phase 5 — Fix + regression test\n\nWrite the regression test **before the fix** — but only if there is a **correct seam** for it.\n\nA correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence.\n\n**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase.\n\nIf a correct seam exists:\n\n1. Turn the minimised repro into a failing test at that seam.\n2. Watch it fail.\n3. Apply the fix.\n4. Watch it pass.\n5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario.\n\n## Phase 6 — Cleanup + post-mortem\n\nRequired before declaring done:\n\n- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)\n- [ ] Regression test passes (or absence of seam is documented)\n- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)\n- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)\n- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns\n\n**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started."}],"details":{"resolvedPath":"C:\\Users\\decid\\.agents\\skills\\diagnose\\SKILL.md","contentType":"text/markdown","totalLines":117,"displayContent":{"text":"---\r\nname: diagnose\r\ndescription: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says \"diagnose this\" / \"debug this\", reports a bug, says something is broken/throwing/failing, or describes a performance regression.\r\n---\r\n\r\n# Diagnose\r\n\r\nA discipline for hard bugs. Skip phases only when explicitly justified.\r\n\r\nWhen exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.\r\n\r\n## Phase 1 — Build a feedback loop\r\n\r\n**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you.\r\n\r\nSpend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**\r\n\r\n### Ways to construct one — try them in roughly this order\r\n\r\n1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e.\r\n2. **Curl / HTTP script** against a running dev server.\r\n3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot.\r\n4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network.\r\n5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation.\r\n6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call.\r\n7. **Property / fuzz loop.** If the bug is \"sometimes wrong output\", run 1000 random inputs and look for the failure mode.\r\n8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate \"boot at state X, check, repeat\" so you can `git bisect run` it.\r\n9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs.\r\n10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you.\r\n\r\nBuild the right feedback loop, and the bug is 90% fixed.\r\n\r\n### Iterate on the loop itself\r\n\r\nTreat the loop as a product. Once you have _a_ loop, ask:\r\n\r\n- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.)\r\n- Can I make the signal sharper? (Assert on the specific symptom, not \"didn't crash\".)\r\n- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.)\r\n\r\nA 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower.\r\n\r\n### Non-deterministic bugs\r\n\r\nThe goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable.\r\n\r\n### When you genuinely cannot build a loop\r\n\r\nStop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop.\r\n\r\nDo not proceed to Phase 2 until you have a loop you believe in.\r\n\r\n## Phase 2 — Reproduce\r\n\r\nRun the loop. Watch the bug appear.\r\n\r\nConfirm:\r\n\r\n- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix.\r\n- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against).\r\n- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it.\r\n\r\nDo not proceed until you reproduce the bug.\r\n\r\n## Phase 3 — Hypothesise\r\n\r\nGenerate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea.\r\n\r\nEach hypothesis must be **falsifiable**: state the prediction it makes.\r\n\r\n> Format: \"If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse.\"\r\n\r\nIf you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it.\r\n\r\n**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly (\"we just deployed a change to #3\"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK.\r\n\r\n## Phase 4 — Instrument\r\n\r\nEach probe must map to a specific prediction from Phase 3. **Change one variable at a time.**\r\n\r\nTool preference:\r\n\r\n1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs.\r\n2. **Targeted logs** at the boundaries that distinguish hypotheses.\r\n3. Never \"log everything and grep\".\r\n\r\n**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die.\r\n\r\n**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second.\r\n\r\n## Phase 5 — Fix + regression test\r\n\r\nWrite the regression test **before the fix** — but only if there is a **correct seam** for it.\r\n\r\nA correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence.\r\n\r\n**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase.\r\n\r\nIf a correct seam exists:\r\n\r\n1. Turn the minimised repro into a failing test at that seam.\r\n2. Watch it fail.\r\n3. Apply the fix.\r\n4. Watch it pass.\r\n5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario.\r\n\r\n## Phase 6 — Cleanup + post-mortem\r\n\r\nRequired before declaring done:\r\n\r\n- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)\r\n- [ ] Regression test passes (or absence of seam is documented)\r\n- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)\r\n- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)\r\n- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns\r\n\r\n**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started.\r","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117]},"meta":{"source":{"type":"internal","value":"skill://diagnose"}}},"isError":false,"timestamp":1789528515222}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_hz9tK2aO3lzSf6Esz3lJ84Jr|fc_0cf46cef591a107c016aaa09c4d81487d2b9f2d3dbfab00562","toolName":"glob","startedAt":"2026-09-16T03:15:19.266Z","args":{"path":".worktrees/49-267-refresh-discriminator/**/{input_ack_deadlock.rs,redispatch*.rs,attach.rs,refresh_freeze.rs,*HAZARD*,*adr*,*requirements*,*traceable*,*glossary*}"},"intent":"Locating coverage and requirements"},"id":"90d58b49","parentId":"6a145e78","timestamp":"2026-09-16T03:15:19.266Z"}
{"type":"message","id":"965a878c","parentId":"90d58b49","timestamp":"2026-09-16T03:15:19.315Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_hz9tK2aO3lzSf6Esz3lJ84Jr|fc_0cf46cef591a107c016aaa09c4d81487d2b9f2d3dbfab00562","name":"glob","arguments":{"i":"Locating coverage and requirements","path":".worktrees/49-267-refresh-discriminator/**/{input_ack_deadlock.rs,redispatch*.rs,attach.rs,refresh_freeze.rs,*HAZARD*,*adr*,*requirements*,*traceable*,*glossary*}","hidden":true,"gitignore":false,"limit":100},"intent":"Locating coverage and requirements"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":1858,"output":88,"cacheRead":9984,"cacheWrite":0,"totalTokens":11930,"cost":{"input":0.018580000000000003,"output":0.0044,"cacheRead":0.009984,"cacheWrite":0,"total":0.03296400000000001}},"stopReason":"toolUse","timestamp":1789528515244,"responseId":"resp_0cf46cef591a107c016aaa09c3754c87d287549de0b7355535","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0cf46cef591a107c016aaa09c4d81487d2b9f2d3dbfab00562","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating coverage and requirements\",\"path\":\".worktrees/49-267-refresh-discriminator/**/{input_ack_deadlock.rs,redispatch*.rs,attach.rs,refresh_freeze.rs,*HAZARD*,*adr*,*requirements*,*traceable*,*glossary*}\",\"hidden\":true,\"gitignore\":false,\"limit\":100}","call_id":"call_hz9tK2aO3lzSf6Esz3lJ84Jr","internal_chat_message_metadata_passthrough":{"create_time":1789528515.647691,"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"glob","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}}]},"duration":4016.2971000000834,"ttft":1624.909500000067,"completedAt":1789528519261,"contextSnapshot":{"promptTokens":11842,"nonMessageTokens":10304,"compactionEpoch":0}}}
{"type":"message","id":"e6c3915a","parentId":"965a878c","timestamp":"2026-09-16T03:15:19.396Z","message":{"role":"toolResult","toolCallId":"call_hz9tK2aO3lzSf6Esz3lJ84Jr|fc_0cf46cef591a107c016aaa09c4d81487d2b9f2d3dbfab00562","toolName":"glob","content":[{"type":"text","text":"# .worktrees/49-267-refresh-discriminator/\ntraceable-reqs.toml\n## crates/\n### spt-daemon/\n#### src/\nattach.rs\n#### tests/\nattach.rs\ninput_ack_deadlock.rs\nrefresh_freeze.rs\nredispatch_stall.rs\nredispatch.rs\n### spt/src/\nattach.rs\n### spt-net/src/net/\nattach.rs\n## docs/\nKNOWN-HAZARDS.md\n### adr/\n## .github/ci/\ntraceable-reqs.sh\ntraceable-reqs-selftest.sh"}],"details":{"scopePath":".worktrees/49-267-refresh-discriminator","fileCount":13,"files":[".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/attach.rs",".worktrees/49-267-refresh-discriminator/docs/adr/",".worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md",".worktrees/49-267-refresh-discriminator/traceable-reqs.toml",".worktrees/49-267-refresh-discriminator/crates/spt/src/attach.rs",".worktrees/49-267-refresh-discriminator/.github/ci/traceable-reqs.sh",".worktrees/49-267-refresh-discriminator/.github/ci/traceable-reqs-selftest.sh",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs",".worktrees/49-267-refresh-discriminator/crates/spt-net/src/net/attach.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1789528519395}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Bx3JMhUCca09pmASNp7G2PcE|fc_0cf46cef591a107c016aaa09d2630c87d28b8a3385299c1562","toolName":"grep","startedAt":"2026-09-16T03:15:30.518Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs;.worktrees/49-267-refresh-discrimina…"},"intent":"Mapping regression test functions"},"id":"cf6c812e","parentId":"e6c3915a","timestamp":"2026-09-16T03:15:30.519Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_h2viGDu7SsFkcIvsXk5tzAsE|fc_0cf46cef591a107c016aaa09d2631887d299d8d445d80c4f25","toolName":"grep","startedAt":"2026-09-16T03:15:30.519Z","args":{"path":".worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md;.worktrees/49-267-refresh-discriminator/docs/adr;.worktrees/49-267-refresh-discriminator/traceable-reqs.toml"},"intent":"Locating documented recovery contracts"},"id":"fa514a22","parentId":"cf6c812e","timestamp":"2026-09-16T03:15:30.519Z"}
{"type":"message","id":"58a3ba02","parentId":"fa514a22","timestamp":"2026-09-16T03:15:30.520Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_Bx3JMhUCca09pmASNp7G2PcE|fc_0cf46cef591a107c016aaa09d2630c87d28b8a3385299c1562","name":"grep","arguments":{"i":"Mapping regression test functions","pattern":"^(async )?fn |#\\[.*test|REQ-|pending_input|by: Some|by:Some","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs","case":false,"gitignore":false,"skip":null},"intent":"Mapping regression test functions"},{"type":"toolCall","id":"call_h2viGDu7SsFkcIvsXk5tzAsE|fc_0cf46cef591a107c016aaa09d2631887d299d8d445d80c4f25","name":"grep","arguments":{"i":"Locating documented recovery contracts","pattern":"refresh|pending.input|ACK|promotion|redispatch","path":".worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md;.worktrees/49-267-refresh-discriminator/docs/adr;.worktrees/49-267-refresh-discriminator/traceable-reqs.toml","case":false,"gitignore":false,"skip":null},"intent":"Locating documented recovery contracts"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":406,"output":285,"cacheRead":11648,"cacheWrite":0,"totalTokens":12339,"cost":{"input":0.00406,"output":0.01425,"cacheRead":0.011647999999999999,"cacheWrite":0,"total":0.029958}},"stopReason":"toolUse","timestamp":1789528519462,"responseId":"resp_0cf46cef591a107c016aaa09c7a73087d2bbc2ed6a757278dd","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0cf46cef591a107c016aaa09d2630c87d28b8a3385299c1562","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping regression test functions\",\"pattern\":\"^(async )?fn |#\\\\[.*test|REQ-|pending_input|by: Some|by:Some\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs\",\"case\":false,\"gitignore\":false,\"skip\":null}","call_id":"call_Bx3JMhUCca09pmASNp7G2PcE","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"grep","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa09d2631887d299d8d445d80c4f25","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating documented recovery contracts\",\"pattern\":\"refresh|pending.input|ACK|promotion|redispatch\",\"path\":\".worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md;.worktrees/49-267-refresh-discriminator/docs/adr;.worktrees/49-267-refresh-discriminator/traceable-reqs.toml\",\"case\":false,\"gitignore\":false,\"skip\":null}","call_id":"call_h2viGDu7SsFkcIvsXk5tzAsE","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"grep","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}}]},"duration":11050.053999999538,"ttft":10954.044599999674,"completedAt":1789528530515,"contextSnapshot":{"promptTokens":12054,"nonMessageTokens":10304,"compactionEpoch":0}}}
{"type":"message","id":"3687bed4","parentId":"58a3ba02","timestamp":"2026-09-16T03:15:30.833Z","message":{"role":"toolResult","toolCallId":"call_h2viGDu7SsFkcIvsXk5tzAsE|fc_0cf46cef591a107c016aaa09d2631887d299d8d445d80c4f25","toolName":"grep","content":[{"type":"text","text":"# .worktrees/49-267-refresh-discriminator/docs/\n## KNOWN-HAZARDS.md\n 23|### 1.3 Stale `index.lock` wedge from prior git crash\n*24|- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.\n 25|- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.\n 26|- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.\n 27|- **Sister cite:** CHANGELOG v1.11.20 \"Stale `index.lock`\"; `src/common/git.rs`.\n...\n 52|- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.\n*53|- **Invariant:** stdin-provided session_id wins; env is fallback only.\n 54|- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.\n 55|- **Sister cite:** CHANGELOG v1.35.1 \"IN-05\"; `hook_subagent_start.rs:40-51`.\n 56|\n*57|### 2.3 Binary-handoff argv schema must stay backward-compatible\n 58|- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.\n 59|- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.\n*60|- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.\n 61|- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\n 62|\n 63|### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\n...\n 65|- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes \"same gen continuing\" vs \"new gen born\".\n*66|- **spt-core mapping:** carries to the daemon's per-instance generation tracking.\n 67|- **Restoration D3/D4 (ADR-0018):** the generation *counter* custody moved to the broker (D3-2 — it observes every brain spawn, planned or crash, and hands `{generation, start-reason}` at spawn; `gen_start` stays `now()`-fresh, never rehydrated). The brain→brain **`BrainState` *message*** (`{session_id, generation, next_seq, gen_start_ms}`) that previously carried continuity across a handoff is **retired from the production path in D4-2**: a brain the supervisor respawns cold-starts and reconstructs ...\n 68|- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\n 69|\n...\n 79|<!-- [doc->REQ-SHELL-PERSISTENT-BOOT-RESTORE] -->\n*80|- **One process generation per read (releases#287):** modern `shell.launch.json` atomically stores `pid`, native `pid_started_at`, and wall-clock `launched_ms`. A modern `pid: 0` means explicitly pidless/cleared and never falls back to the numeric `shell.pid` mirror. Legacy records without `pid` retain their old numeric-PID plus optional-birth read. Publish the complete JSON identity before updating the compatibility mirror; failed publication preserves the previous identity. Never delete the previous bi...\n 81|- **No spontaneous relaunch of a NONPERSISTENT same-boot death:** the releases#78 force-kill freeze still protects nonpersistent instances, so killing one to free its exe does not arm a watcher underneath an overwrite. Recovery remains demand-driven. **Persistent instances are exempt** (operator ruling, releases#287): a kill is not a request to keep a declared-persistent shell down; use `teardown` or a nonpersistent manifest instead.\n 82|- **Watcher eligibility:** recorded `offline`, no live binary awaiting bind, and either `persistent`, no corpse, or a corpse whose launch predates the machine boot instant. A nonpersistent corpse without a launch stamp or boot oracle remains frozen; persistent instances require neither to become eligible. Birth-safe probes prevent a recycled pid from masquerading as a live shell.\n*83|- **spt-core mapping:** `spt_store::shellinfo::{shell_pid_provably_dead, effective_status, is_shell_online}` (the resolver — the shell-side twin of 2.5's `liveness.rs`, which gave *agent* perches exactly this and which shells never got); consumers in `linkhost::{relink_shell, prepare_drive, run_action}`, `shelldisc::discover`, `activity::observe_links`; the class-(c) eligibility read is `spt_daemon::shellwake::watcher_eligible` (+ `launch_predates_boot`, `BOOT_RESTORE_SLACK_MS`), and the birth stamp it...\n 84|- **Source:** spt-core, flynn's spt-alchemy field report (2026-07-25) — a deterministic recipe, not a race. Class-(c) re-based BAROMETER W2 (releases#78, doyle's ruling on comment 5156810267).\n 85|\n 86|### 2.7 A daemon or machine restart strands `persistent` shells — and a graceful cascade fixture suppresses the failing arm `[REQ-HAZARD-RESTART-STRANDS-PERSISTENT-SHELLS]`\n...\n 92|- **Per-owner enumeration:** the existing `SHELL_RECORD_HEALED:<owner>/<shell_id>` daemon diagnostic names **every** successfully healed instance, not one representative. Healthy and already-healed records produce no event. The restore diagnostics likewise identify each attempted instance and distinguish boot from owner-online triggers. <!-- [doc->REQ-SHELL-HEAL-ENUMERATION] -->\n*93|- **spt-core mapping:** `spt_daemon::shellwake::{heal_stale_online_records, restore_persistent_shells_at_boot, restore_persistent_shells_on_owner_online, OwnerOnlineEdge}`; `shellhost::live_launch_winner`; `spt_store::shellinfo::{ShellLaunch, shell_pid_provably_dead}`. `launch_predates_boot` and `BOOT_RESTORE_SLACK_MS` now govern only nonpersistent watcher eligibility.\n 94|- **Source:** BAROMETER W2 (releases#78); releases#287 operator ruling (2026-09-08), superseding the persistent force-kill freeze and boot-instant conjunct.\n 95|<!-- [doc->REQ-HAZARD-RESTART-STRANDS-PERSISTENT-SHELLS] -->\n 96|\n...\n 113|### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF\n*114|- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.\n 115|- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.\n 116|- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.\n 117|- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.\n...\n 132|<!-- [doc->REQ-PERCH-GC-RESIDUE-PREDICATE] -->\n*133|- **Failure:** the obvious way to write a perch garbage collector is \"reap what is not alive\" — `!spt_store::liveness::is_perch_alive(dir)`. On the population a GC actually walks, that predicate is **backwards**. `is_perch_alive` returns **true** for a directory with no `info.json` (`RawRead::Absent ⇒ true`, interim parity — an absent record means a wrapper-owned listener between polls), and **false** for a daemon-hosted endpoint whose `status` is `offline`. Measured on HFENDULEAM 2026-08-04 across...\n 134|- **Why it is a hazard and not a bug in the resolver:** `is_perch_alive` is correct for its own job. Fail-toward-alive is the safe direction for DELIVERY (2.5) — the cost of a false \"alive\" is one message taking the spool path. It is the wrong direction for DELETION, where a false \"not alive\" is irreversible. The inversion is what happens when a predicate is reused across a polarity boundary it was never sized for.\n 135|- **Invariant:** perch garbage collection classifies on record **presence** only (3.5's predicate: no `info.json` present on any read attempt ⇒ residue; NotFound kept distinct from every other I/O error, because absent is the only answer that authorizes deletion) and **never calls a liveness resolver** — not `is_perch_alive`, not `is_registry_entry_alive`, not a pid probe. The registry is likewise never asked: `clean_stale_entries` deletes dead-pid rows and `unregister_address` fires on an ordinary s...\n 136|- **spt-core mapping:** `spt_store::perchgc::{sweep, PerchClass}`; `spt endpoint gc` renders it. The refusal/report contract is `REQ-PERCH-GC-REFUSAL-REPORT`; the occupancy rules it inherits are 3.4.\n...\n 157|- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n*158|- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n*159|- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.\n 160|- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n 161|\n 162|### 4.4 Deferred rows survive poll drain\n...\n 208|- **Invariant (mitigation by construction, common case):** the common trigger — a full reinstall / identity regeneration — is covered by the **re-pair trust overwrite** (M8 decision 13, REQ-SUBNET-7): a completed ceremony presenting the same label + machine id evicts the superseded identity's trust AND registry rows on the seed-holder, and the peer-side epoch memory **dies with the deleted row** — the re-paired node's fresh epochs land on a clean lease. M8 acceptance 7 verifies this explicitly (the...\n*209|- **Residual (documented, guard deferred):** the narrow slice — epoch file lost while the node *identity* is kept (manual state surgery, partial restore from backup) — has no guard; it waits for a field hit before one is designed (M8 decision 24). `REQ-HAZARD-EPOCH-RESET` is minted inactive (TRACEABILITY rule 5) as the tracking hook. If hit: symptoms are one node's endpoints frozen-stale on every peer while its own views are fresh; recovery today is re-pairing the node (rides the common-case eviction...\n 210|- **spt-core mapping:** epoch mint = `spt_store::epoch::EpochSource` (`identity/epoch.json`); the lease = the per-`(endpoint, node)` epoch compare in `spt_net::net::registry`; the eviction that clears peer-side epoch memory = `registryhost::repair_evict_superseded` + `RegistryHost::consume_repair_evictions`.\n 211|- **Source:** minted at M8 ratification (decision 24), recognized as a class during the 2026-06-07 pump diagnosis / re-pair overwrite design — not yet field-hit in its residual form.\n 212|\n...\n 224|- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\n*225|- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\n 226|- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\n 227|- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\n 228|\n...\n 236|- **Failure:** canonicalized `\\\\?\\C:\\...` serializes to `//?/C:/...` and fails `read_to_string`.\n*237|- **Invariant:** strip the `\\\\?\\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.\n 238|- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.\n 239|- **Sister cite:** `src/common/owlery.rs:377-384`.\n 240|\n...\n 271|- **Failure:** `Instant::now() - Duration::from_secs(N)` panics `overflow when subtracting duration from instant` when the process's monotonic clock is younger than `N` — i.e. the host booted less than `N` ago. The peer pump primed its cadence legs with `Instant::now() - 86_400s` to mean \"everything due now\"; on a Windows runner with sub-24h uptime the pump thread panicked at startup, so the subnet never converged (CI `pump_and_dispatch_self_drive_the_subnet` failed, run 27082417706). It is *environmen...\n*272|- **Invariant:** NEVER compute an instant in the past by subtracting from `Instant::now()`. Represent \"never run / due now\" as `Option<Instant> = None` and gate on forward `now.duration_since(past)` only (`peerloop::due`). No backward instant arithmetic anywhere in scheduling.\n 273|- **Test seam caveat:** the convergence E2E only reproduces on a sub-offset-uptime host (it passed everywhere with >24h uptime). The deterministic guard is the `due(None, ..)`/`due(Some(now), ..)` unit on the extracted gate — it asserts first-tick-due with zero instant subtraction, independent of host uptime.\n 274|- **spt-core mapping:** `spt-daemon::peerloop::due` (the sole cadence gate behind `due_reg`/`due_notif`/`due_sync`/`due_upd`); cadence legs are `Option<Instant>` seeded `None`.\n 275|- **Source:** spt-core CI failure, 2026-06-07 — Windows runner `hfenduleam` (just booted) panicked the peer pump at the v0.1.1 release gate.\n...\n 278|- **Failure:** the elevation-gated commands (`subnet create` / `subnet join` / `show-code`) refuse when unelevated and tell the user to \"run as administrator / root\". The user types the obvious `sudo spt subnet create FOO` → `sudo: spt: command not found`. `spt` is a user-local install (`~/.local/bin`, `~/.cargo/bin`), and sudo's `secure_path` (a `/etc/sudoers` default) does NOT include those dirs, so a bare command name doesn't resolve under sudo. The guidance is a trap: it names an action that cannot...\n*279|- **Invariant:** elevation guidance on Unix emits the binary's **absolute path** under sudo — `sudo /home/u/.local/bin/spt subnet create FOO` — reconstructed from `current_exe()` + the real argv and shell-quoted. An absolute program path is executed directly; `secure_path` only governs bare-name PATH lookup, so the absolute form always resolves. On an interactive Unix TTY the command auto-elevates (re-execs itself under sudo, the elevated child does the work and `main` de-elevates back); non-interact...\n 280|- **Companion UX:** the post-de-elevation `DEELEVATED: running as uid N` line is internal state-safety noise — omit it from the user-facing CLI path (it confused the same field user). The detached daemon's own de-elevation log line is fine (it lands in the daemon log, not the terminal).\n 281|- **Test seam caveat:** the sudo re-exec needs a real `sudo` + TTY (not hermetic). The deterministic guard is the pure `elevation::sudo_argv` / `print_hint_command` (assert an absolute exe path, never a bare name, + shell-quoting on the printed line) and the `decide_elevation_path` matrix (which picks inline-sudo only on an interactive Unix TTY); the exec leg is manual/kitsubito-verified.\n 282|- **spt-core mapping:** `spt::elevation::{sudo_argv, print_hint_command, decide_elevation_path}` (pure — generalized from the M12-W4 self-elevation seam, 5.11), `spt::cli::{try_auto_elevate, with_elevation_hint}` wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`; `spt::main` de-elevation drop silenced.\n...\n 285|### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\n*286|- **Failure class:** a privilege-gated command (`subnet create` / `join` / `show-code`) self-elevates by re-launching itself with privilege (Windows UAC `runas`, Linux `pkexec` / a terminal-emulator `sudo`, or inline `sudo`). A careless re-launch is a security hole: widening the privilege scope (adding args), resolving the binary by a bare name (a PATH/`secure_path` hijack runs an attacker's `spt`), interpolating a crafted arg into a shell string (`sh -c \"… $id …\"` injects a second command), or re-el...\n*287|- **Invariant:** self-elevation re-runs the **EXACT** original invocation with the binary's **ABSOLUTE** exe path — never adding/altering args, never a PATH-resolved bare name, never a shell-interpolated string. Every launcher passes an **argv array** (`Command::new(prog).args([...])`, never `sh -c`); the Windows `ShellExecuteW` params string (which is inherently one string) MSVC-quotes each verbatim arg so `CommandLineToArgvW` round-trips it as a single token. The elevated child drops state back to th...\n 288|- **Test seam caveat:** the real launch needs a UAC/polkit/sudo prompt (not hermetic) — manual-verify. The deterministic guards are the pure `decide_elevation_path` matrix (loop-safety: `AlreadyElevated` on every os; the os×env path order) and the argv builders (`sudo_argv` / `pkexec_argv` / `terminal_argv` assert absolute-exe + verbatim args + array; `windows_runas_params` asserts MSVC-quoting with no `cmd /c` interpolation; the crafted-arg test asserts a shell-metachar arg stays one element / one qu...\n 289|- **spt-core mapping:** `spt::elevation::{decide_elevation_path, sudo_argv, pkexec_argv, terminal_argv, windows_runas_params, print_hint_command, ElevatePath}` (pure), `spt::cli::{try_auto_elevate, launch_uac_window, pause_elevated_console_if_fresh, program_on_path, first_terminal_emulator}` (impure launchers) wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`. Companions: 5.10 (the Unix abs-path-under-sudo facet) and 5.7 (the elevated child's de-elevation drop, which this compos...\n 290|- **Source:** M12-W4 design (subnet QR + self-elevating window), doyle ruling `M12-W4-RULING.md` Q6 — a privilege-escalation feature carries a mandatory hazard REQ.\n...\n 293|### 5.12 Native-PTY spawn of a bare program runs the wrong (non-PE) file on Windows  `[REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]`\n*294|- **Failure:** `portable-pty`'s ConPTY spawn resolves a bare program name with a `which` that takes the FIRST `PATH` match. A node/npm CLI installs as BOTH an extensionless shebang shim (`ccs`, for Git Bash) and a Windows launcher (`ccs.cmd`) in the same dir; portable-pty picks the extensionless `ccs`, and `CreateProcessW` then tries to execute that non-PE file and fails with **os error 193** (\"%1 is not a valid Win32 application\"). Live failure: `spt endpoint run claude-spt:ccs` → `CreateProcessW C:\\n...\n\n## adr/\n### 0001-clean-room-fork-from-claude-skill-owl.md\n 8|\n*9|spt-core is the rebuild of `claude_skill_owl` (today's modern SPT) into a harness-independent core. The sister project works but is fragile: each feature release introduces 2–5 regressions, often in unprecedented ways, because the substrate grew rapidly from a much narrower starting point and carries the resulting tech debt. spt-core also commits day-one to several architectural shapes (endpoint type system, networking, terminal wrapper as backbone, untethering from Claude Code) that change foundationa...\n 10|\n 11|## Decision\n 12|\n### 0002-bake-networking-into-core-collapse-spt-node.md\n 8|\n*9|This ADR retired GitHub as the *required/default* cross-node sync. It is now re-admitted as an **opt-in alternative transport for the context repo only** (`tracked/`), enabled by `spt context-github-setup`:\n 10|\n 11|- **Default stays P2P** + no-account — the core promise is unchanged.\n*12|- **Hub mode (opt-in):** every node pull→merge→pushes against a private GitHub remote (always-online sync hub + live GUI view), authed by a **shared deploy key** distributed as subnet secret material (only the setup node needs a GitHub account). **Prefer-GitHub, P2P backstop.**\n 13|- **Scope:** hub mode carries the **context repo only**. Messaging, registry, remote-drive, pairing, and presence always ride P2P/Iroh; gh-on never removes the P2P requirement. So the original decision (networking baked-in, first-class, P2P) stands in full — the hub is a context-sync convenience layered on top, not a return to GitHub-as-infrastructure.\n 14|\n 15|## Context\n 16|\n*17|The sister project (`claude_skill_owl`) decided networking should live in a separate `spt-node` daemon, explicitly to decouple the network stack from SPT's Claude-Code-tied core binary, expose SPT-protocol-as-product, and let networking evolve on its own release cycle. That decision predates spt-core.\n 18|\n 19|spt-core is a ground-up, harness-independent rebuild. The primary reason `spt-node` was separate — decoupling networking from a CC-tied binary — no longer exists, because spt-core is already decoupled from any harness. Meanwhile the product goal for networking sharpened: WAN cross-machine messaging should be **first-class and zero-config — \"just works.\"**\n 20|\n...\n 22|\n*23|Bake the WAN transport stack (Iroh + mDNS LAN discovery + pairing) into spt-core's library crates, behind a compile-time `net` feature flag (default-on in the reference binary, optional for embedded library consumers).\n 24|\n 25|\"Just works WAN, reachable even when no session is running\" requires a persistent per-machine network presence — the exact job `spt-node` was created to do. spt-core already commits to a single always-on, one-per-machine `spt-daemon` to supervise PTYs (the terminal wrapper). **Collapse the network-host role into that same daemon (option B1).** One process owns both PTYs and the node identity + WAN endpoint.\n 26|\n...\n 45|\n*46|- **SERIOUS #6 — one collapsed daemon = one failure domain.** A networking bug can take out terminal hosting; a PTY-broker bug can take out WAN reachability. Accepted (the collapse is locked), but v1 owes a **fault-injection acceptance matrix**: crash/hang Iroh, mDNS, registry sync, PTY broker, and manifest invocation independently; measure blast radius + recovery. Track at M4. → Begun M4-D4: **`docs/FAULT-MATRIX.md`** is the living matrix (proven rows with test evidence; planned rows tracked as debt).\n 47|- **Interaction with ADR-0004 (FATAL #4):** preserving live WAN streams across a brain restart forces Iroh endpoint / QUIC stream state into the \"stable broker,\" enlarging the kernel ADR-0002 wanted minimal. Resolved in ADR-0004's ownership table — \"stable\" now means *update cadence*, not *narrowness*.\n### 0003-multi-instance-node-anchored-identity-model.md\n 8|\n*9|The original decision below framed \"operate a LiveAgent on any node from any other machine\" primarily as **remote PTY drive**. That is backwards-weighted. Corrected model:\n 10|\n 11|- **Primary — instance:** the *same* endpoint (same harness+adapter) running **natively** on a machine (local files, local compute), with the **Psyche mind synced** across instances. Files/session-history are per-node and cannot teleport; the mind follows. This is the core meaning of \"same agent on multiple machines.\"\n 12|- **Instance dormancy:** typically one instance is actively-driven; others are **dormant** (live, state-preserved, re-activatable in place). Switching machines makes the previously-driven instance dormant — no explicit stop. Registry status is active / dormant / offline.\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n 10|\n*11|1. **Consolidation.** The sister project runs poll listeners as ephemeral per-session background tasks and Psyche wrappers as detached per-live-agent supervisor processes. But poll listeners already interact directly with the agent session (capsule/idle), and Psyche wrappers already invoke harness binaries directly. Once the daemon owns every PTY, keeping these as separate processes is unjustified.\n 12|\n 13|2. **Seamless self-update with a hard no-terminate constraint.** Self-update is a day-one pillar. The constraint: *no endpoint process may terminate or suspend during an spt-core update* — we cannot assume every endpoint can safely suspend. The naive \"drain + restart the daemon\" approach violates this for spt-hosted sessions (the daemon owns their PTY; killing the daemon SIGHUPs the child).\n 14|\n...\n 26|\n*27|**Self-update delivery:** peer-propagated over P2P, layered on self-fetch, out-of-band still supported. All binaries signature-verified before handoff (spt-core's own release key) regardless of source — peer-propagation otherwise lets one compromised node poison the subnet. spt-core conducts updates for the whole stack: self first, then ripple-update each registered adapter via the adapter manifest's update declaration (file-pull or delegated command). The plugin's role shrinks to initial bootstrap only.\n 28|\n 29|**Cadence/consent:** not fully automatic by default; gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto choice.\n 30|\n...\n 33|- The daemon is the single brain for a machine; crash-recovery and update logic must cover PTYs, networking, registry, spools, listeners, and psyche loops together.\n*34|- A small internal broker process exists beneath the daemon — a deliberate, bounded walk-back of \"literally one process,\" preserving B1's *intent* (one network identity, one supervisor, one firewall prompt) while guaranteeing endpoint survival across updates.\n 35|- Peer-propagated updates make release signing mandatory, not optional.\n 36|- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.\n 37|- The \"deliver to most-recently-active session\" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.\n...\n 57|| Accepted local client socket (`api listen`, relay) | **broker** | ✅ #1 (plain TCP) |\n*58|| Iroh endpoint + accepted QUIC streams | **broker** | ✅ #3 (loopback shape) → **implemented M4-D4a** (`spt-daemon::nethost::NetHost`: dedicated tokio runtime in the broker owns the endpoint + conn table; brain drives status/dial over IPC net frames) |\n 59|| mDNS socket / relay session | **broker** | ✅ closed by construction at M4-D4a (2026-06-03): `MdnsAddressLookup` and the relay session are constructed *inside* the broker-owned iroh endpoint — there is no separate socket to own |\n 60|| Listening sockets | **broker** | ✅ #1 |\n 61|| Routing, registry state, pulse/psyche loops, manifest parse, update orchestration | brain (rehydrate from disk) | n/a |\n...\n 68|### D. Self-update delivery hardening (resolves SERIOUS #5)\n*69|Signature-verify alone is insufficient. v1 adds: monotonic version field (rollback rejection), release-metadata expiry, channel pinning, key rotation/revocation list, and **adapter content signing** (adapter file-pull / delegated-command updates were uncovered by \"binary signature\"). Track as `REQ-HAZARD-UPDATE-ROLLBACK`.\n 70|\n 71|### E. Open spike gaps (must close before M3 builds the daemon)\n*72|1. ✅ **CLOSED (Spike #3, 2026-06-01).** Live Iroh + file-transfer stream survival across brain restart (FATAL #2). Spike #1 only proved PTY + plain TCP; Spike #3 stood up a broker-owned Iroh endpoint + uni QUIC transfer and proved a live peer download survives a brain restart gapless + exactly-once. Loopback shape validated; off-node transport + the QUIC-ownership *implementation* defer to M4. (`docs/spikes/SPIKE-03-quic-survival.md`.)\n 73|2. ✅ **CLOSED (Spike #4, 2026-06-01).** Linux `forkpty` parity. Spike #1's binary, unchanged, passed all four invariants on `gravity-linux` (Ubuntu 22.04) — `portable-pty` selects `forkpty` on Unix; the ConPTY-DSR branch is inert there. Invariant B passed with the *strict* contiguity check that ConPTY-under-resize fails (Spike #5): `forkpty` is a raw pipe, ConPTY a screen buffer — different stream contracts, promoted into M3a's `SessionSurface` design. (`docs/spikes/SPIKE-04-forkpty-parity.md`.)\n*74|3. ✅ **CLOSED (Spike #5, 2026-06-01).** 100× restart + resize-under-load stress (codex #3). The broker survived 100 rapid brain kill/restart cycles + a resize thread racing the reader with no leak (one child, clean reap), no hang (watchdog deadline), no lost byte (full value-set coverage in the client's observation window). Surfaced two binding ConPTY findings for M3a/REQ-TERM-3: resize triggers a repaint that reorders+dups the stream (terminal-stream contract ≠ exactly-once transfer contract); mid-...\n 75|4. ✅ **CLOSED (Spike #6, 2026-06-01).** Idempotent/exactly-once delivery across brain restart (FATAL-adjacent #14). Durable-ID + WAL + dedup-at-effect proven exactly-once across a crash at any protocol point (before-intent / before-effect / after-effect). Two binding design constraints surfaced for `REQ-HAZARD-RESTART-IDEMPOTENT` / M3b-B5: broker-owned recovery anchor; dedup-at-effect keyed by durable ID. (`docs/spikes/SPIKE-06-idempotent-boundary.md`.)\n 76|\n 77|**Gate status (2026-06-01): ALL FOUR §E GAPS CLOSED PASS** (#3, #4, #5, #6) → the M3-PLAN Phase-0 spike-gate is **complete**. The broker/brain split is validated on both OSes (ConPTY + `forkpty`), across QUIC-stream survival, restart+resize churn, and exactly-once idempotency. M3a and M3b are both unblocked; the QUIC-ownership *implementation* and off-node transport remain explicitly deferred to M4 (only the *shape* was spiked).\n### 0005-totp-seeded-spake2-node-pairing.md\n 43|- **FATAL #10 — trust-store delete is NOT revocation.** The \"revocation = trust-store delete; TOTP gates joining only\" consequence above is false in the threat model: the seed is replicated to every trusted node, so a compromised node/seed can re-pair as a *new* node after deletion. **Required:** a seed **epoch + rotation** protocol — removing a compromised node rotates the subnet seed so the old node/old seed cannot rejoin. Pairing transcript must bind the seed epoch.\n*44|- **SERIOUS #11 — SPAKE2's one-online-guess bound needs subnet-global rate limiting.** A public pre-trust relay + multiple seed-holding nodes enables *distributed* guessing, and a ±1 TOTP acceptance window triples the valid-password space. **Required:** one active pairing ceremony per subnet, shared attempt counter, exponential backoff; justify or drop the ±1 window.\n 45|- **SERIOUS #12 — \"PAKE binds pubkeys\" only if the transcript does.** Must explicitly bind roles, both node pubkeys, subnet ID, TOTP time-step, and confirmation MACs, or unknown-key-share / reflection / wrong-subnet pairing remain. **Required:** full pairing-transcript spec + negative tests (MITM, replay, reflection, wrong subnet, stale time-step).\n 46|\n 47|## Multi-subnet amendment (2026-06-01 — see ADR-0006)\n...\n 53|- **Rendezvous-token hashing.** The relay routes the pre-trust ceremony by `H(subnet-name ‖ TOTP-epoch)`, not the plaintext label — the user/agent enters the raw name + code; the hash is internal. (Payload is already E2E-encrypted; this protects the relay-visible routing token from leaking the human subnet label.) Composes with the #12 transcript binding (the subnet ID / seed epoch already in the transcript).\n*54|- **Per-subnet code fetch is gated behind OS elevation** (Windows UAC / Linux root-or-equivalent), or an elevated agent endpoint; else the user falls back to their authenticator app. Replaces the old unqualified \"fetchable from any trusted node\" with an elevation-gated, *per-subnet* fetch (a node now holds several subnets' seeds).\n 55|- **Code fetch is per-subnet** (a node holds *N* seeds): `spt pair show-totp` prompts which subnet (or `--subnet <name>` / `--create-new` to bypass), offered optionally as a QR / `otpauth://` URI.\n 56|- **Forward (cross-user, ADR-0006 seam):** the seed key becomes per-(subnet, user); the prompt becomes 2-stage. The #10 seed epoch+rotation and #11 subnet-global rate-limit requirements apply **per subnet**.\n 57|\n...\n 73|  shed at the accept door, and a ceremony timeout aligned with the limiter's\n*74|  stale-slot reclaim — an attacker reaching the listener still gets one\n*75|  online guess per admitted ceremony, serialized and backed off.\n 76|- **Membership mutations are elevation-gated** (the REQ-PAIR-6 machinery,\n 77|  exit 3, gate-before-everything): `subnet create` reveals a fresh subnet's\n 78|  joining secret, and `subnet join` enrolls the whole machine into a subnet's\n*79|  trust fabric — an unprivileged process must do neither (an attacker-run\n*80|  joiner would enroll the box into the attacker's subnet). `subnet status`\n 81|  stays ungated: read-only, membership shape only, never secrets.\n 82|\n 83|Surface rename, same semantics: `spt pair show-totp [--subnet|--create-new]`\n### 0006-multi-subnet-membership-model.md\n 18|\n*19|1. **Node-global identity, per-subnet visibility.** One `ling` exists on a node (one mind, one `tracked/` context); the node advertises it into each subnet it belongs to, subject to visibility (below). The subnet registry is **per-subnet**; the same endpoint can appear in several registries. Bare-id uniqueness holds *within* a subnet via a **join-time collision check**.\n 20|\n 21|2. **Qualified addressing + rippled rename.** Two combinable qualifiers — subnet-qualified `home:ling` and node-qualified `ling@hfenduleam`. When a bare id is ambiguous across visible subnets (two distinct endpoints sharing a name), resolution **refuses and forces qualification** rather than guessing. A new **`spt rename <id> <new_id>`** changes an endpoint's logical ID and ripples to all instances (registry, every perch, the `a-<id>` context branch), collision-checked against every target subnet and r...\n 22|\n...\n 24|\n*25|4. **Per-subnet code fetch gated behind OS elevation.** Retrieving a subnet's current code *from a node* requires Windows UAC / Linux root-or-equivalent, or an elevated agent endpoint; otherwise the user falls back to their own authenticator app. Narrows the multi-subnet exposure (mere CLI/agent presence no longer yields any subnet's join-code).\n 26|\n 27|5. **Subnet-exclusive sync.** An endpoint's mind replicates only within its home subnet by default. Endpoint config carries a **subnet-membership list** for sync; the mind replicates across every listed subnet's nodes. Distinct from visibility: being addressable in a subnet does not imply replicating the mind there.\n 28|\n### 0007-subnet-notification-primitive.md\n 15|- **Per-subnet, replicated spool.** Notifs are subnet events; the spool replicates across that subnet's nodes (reusing subnet-registry distribution, ADR-0006). Each notif is subnet-tagged; **dismiss-state replicates subnet-wide**, eventually consistent.\n*16|- **Two states:** *seen* (surfaced at least once, tracked per-endpoint) vs *dismissed* (explicit ack — `spt notif dismiss`, or the agent marks it). Surfacing ≠ dismissal.\n 17|- **First-fire** → the user's most-recently-active endpoint in that subnet (presence resolution). **Resurface** undismissed notifs at existing reported boundaries — state→active (`wake`), `api boundary clear`, `api boundary compact`, new-session-start — gated by *seen-per-endpoint* plus a **cross-endpoint suppression timeout** (default ~1h) so a notif can't bounce between endpoints. Scoped to subnets the endpoint is **visible in** (ADR-0006).\n 18|- **Delivery reuses messaging.** A notif to an agent is a notif-flavored envelope delivered to the agent's perch via the existing path; the agent surfaces it. An optional **`notif_command` manifest template** (on **both** harness-adapter and shell-adapter manifests) renders it endpoint-native (OS toast, GameRobot `alert-symbol`), combinable with agent-surface. If presence resolves to a **Shell** attached to the endpoint, the notif renders via *that shell's* template — the v1 seam and the M5 generalizat...\n 19|- **`spt notify`** lets any agent issue a subnet-wide notif (v1: reaches the user on their active endpoint). The **envelope `from`** carries the issuer's endpoint id + node + subnet (subnet surfaced only to multi-subnet receivers).\n### 0008-live-activity-buffer-pty-digest.md\n 12|\n*13|Add a **live activity buffer (PTY digest)** as a **distinct artifact** — not scrollback (raw bytes, R-TERM-2), not transcript history (full conversation behind communes/resume, Paths A/B). Its purpose is at-a-glance current activity, not replay.\n 14|\n 15|The parsing-boundary distinction that resolves the tension:\n 16|- **The adapter supplies the patterns; spt-core only runs them.** The manifest provides `input_pattern` + `agent_pattern` (each opens a span capturing everything, newlines included, until the next pattern match of any kind) and `tool_pattern` (a **list** — one per tool, plus a final **catchall** for user-installed tools, capturing tool-name + arg fragment). This is exactly Path A's \"adapter owns the parser\" rule and the \"spt-core owns the mechanism, adapter owns the pattern\" principle (identical to opa...\n...\n 35|\n*36|**Why (the Gateway grill, 2026-06-12):** the PTY byte stream of an alternate-screen TUI is a **rendering protocol, not a content log**. Scroll and resize trigger full repaints — the same content re-enters the stream and corrupts delta computation — and semantic turn boundaries drown in repaint traffic. The original §Decision rationale (\"a light rolling regex over bytes ≠ fragile transcript parsing\") proved **backwards in practice**: the byte stream is the *more* fragile surface, and transcript par...\n 37|\n 38|**What stands (the product surface, unchanged):** the formula (N-turn window, collapsed tool sprints, presentation-vs-extraction split), structured source-tagged output, both access modes (snapshot pull + structured-delta stream, deltas now keyed to log records), address-gating, Option-C persistence. The no-built-in-parser rule **still holds** — normalization remains adapter-owned (Path A) or adapter-pushed (Path B).\n 39|\n### 0009-endpoint-access-control-model.md\n 71|- The routing/messaging path (M4) gains a whitelist check on inbound: classify reply-vs-new (via `__REPLY_TO__` correlation) before applying the origin-node gate.\n*72|- The registry projection that backs resource advertisement must apply the visibility (and later whitelist) filter per viewer.\n 73|- **Forward (cross-user, ADR-0006 seam):** the whitelist's `users` tier and the \"nodes belonging to whitelisted users\" semantics activate with the per-(subnet, user) model.\n 74|- Builds on ADR-0006 (visibility, multi-subnet) and the existing consent grant store (CONTEXT §Consent & security gates).\n### 0011-branchstore-git-kv-state-substrate.md\n 30|filesystem — as the substrate for **coarse, durable, audited** state. It lives at\n*31|the bottom of the stack (`spt-store`) so context sync, the registry, and daemon\n 32|checkpointing share one abstraction rather than each re-inventing persistence.\n 33|\n 34|**BranchStore is for:**\n...\n 48|- **Merge-native sync** — reuse the context precedence merge driver + `--allow-unrelated-histories` for any BranchStore replicated cross-node.\n*49|- **GUI consequence** — state browsing becomes *git rendering*: raw inspection is free via standard git tooling; the product GUI (R-FRONT-1 panes) is a memformat-aware renderer over git, inheriting history / diff / time-travel rather than building a versioning backend.\n 50|\n 51|## Consequences\n 52|\n### 0012-psyche-trust-boundary-stdout-event-outbound.md\n 77|  be a **bounded, stdin-fed, stdout-captured** invocation (mirroring the sister's\n*78|  per-turn `--resume`), like the echo-commune's `run_bounded_stdin`. Tracked as\n 79|  KNOWN-HAZARDS 7.3 / `REQ-HAZARD-PSYCHE-OUTBOUND-PROXY`.\n 80|- **A marker parser must exist.** spt-core has no `<EVENT type=\"reply|notify\">`\n 81|  parse-and-relay path yet (the sister's `parse_markers`, `claude.rs:594`). It is\n### 0013-psyche-reconciled-context-merge.md\n 31|histories** — rejected outright; the context DAG must be shared. Third fork:\n*32|BranchStore backing (git CLI vs gitoxide).\n 33|\n 34|## Decision\n 35|\n...\n 40|- **Holding state is durable and replicated.** On detecting a concurrent pair,\n*41|  both versions persist as **tracked conflict artifacts** (deterministic\n 42|  content-hash names, in-repo, so conflict state itself syncs). The local\n 43|  working file stays untouched — the active mind is never clobbered by an\n 44|  unresolved conflict. An event seam fires (a D8 notif producer later).\n...\n 46|  active-instance-authoritative rule — the Psyche lives where the mind is being\n*47|  used). No active instance anywhere → deterministic fallback: lowest node id\n 48|  among conflict holders. This kills reconcile storms (two nodes merging the\n 49|  same pair into textually-different results, conflicting again).\n 50|- **The merged write carries `join(vA, vB)` + a bump of the reconciler's own\n### 0014-publish-target-repo-and-lapse-proof-canonical-url.md\n 7|\n*8|**Canonical URL = `https://sabermage.github.io/spt-releases`, with no custom domain configured on Pages.** This URL is what gets baked everywhere: docs cross-links, `llms.txt`, the install one-liner in the README, and every adapter's pack-in install logic.\n 9|\n 10|## Considered options\n 11|\n### 0015-release-signing-two-key-trust-anchor.md\n 2|\n*3|Every shipped binary embeds the release **trusted-key set** that `spt update` verifies against (R-UPD-2), so the keys chosen at v0.1 anchor the self-update trust chain for every deployed node. We mint **two Ed25519 keypairs**: a **primary** (active — signs every release, lives on the maintainer's dev machine) and a **recovery** (generated once, never used, never stored on the dev machine). **Both public keys ship in the binary's trusted set.** Both private seeds (32 bytes each) are backed up independen...\n 4|\n 5|Loss of the primary (e.g. dev machine destroyed) is then a non-event: the next release is signed with the recovery key and rotates in a fresh primary (the verify path already supports key revocation). Without the second key, losing the sole key would kill the self-update chain — nodes keep running but every future release fails verification until users manually re-run the installer.\n 6|\n...\n 9|- **Signing key as a CI secret (automated signing)** — rejected for now. The runners are self-hosted dev boxes running arbitrary dev workloads; a release key in their secret store is weaker custody than a manual local sign, and release cadence doesn't justify automation. Revisit if cadence demands it.\n*10|- **Single key + good backups** — rejected; rotation-after-loss requires a *second already-trusted* key, otherwise recovery means out-of-band reinstall for every user.\n 11|\n 12|## Consequences\n 13|\n### 0016-platform-targeted-update-sets.md\n 16|\n*17|Platform keys in the signed metadata use Rust target triples, for example `x86_64-pc-windows-msvc` and `x86_64-unknown-linux-gnu`. Release asset names remain a packaging concern and may keep their public-friendly names; target triples are the protocol identifiers.\n 18|\n 19|This keeps debug rollout fast without turning it into raw peer file-copy: a fast coordinator can assemble and sign every platform artifact, then let the existing subnet update machinery distribute the right bytes to each node. The generic receiver/protocol support belongs in the production daemon because public releases and debug rollouts have the same cross-platform distribution problem. The rollout driver does not: build recipes, cross-build/WSL/runner coordination, dev-key signing, and \"stage this deb...\n 20|\n*21|Release monotonicity is scoped to the pinned channel. Stable releases and debug rollouts use separate counters, so a fast debug sequence cannot consume, block, or roll back the stable release counter; rollback protection compares only against the latest accepted version for the recipient's pinned channel.\n 22|\n 23|Debug rollout targets are opt-in by channel pin, not by subnet membership alone. A debug offer may traverse trusted subnet peers, but a stable-pinned node rejects it at the metadata gate; only nodes pinned to the debug channel and provisioned with the dev signing key can stage and apply it.\n 24|\n*25|Pinning a lab node to debug uses the existing release-key overlay: `$SPT_HOME/identity/release-keys.json` adds the dev public key and sets `\"channel\": \"debug\"` (for example `\"keys\": {\"dev-debug-2026\": \"<ed25519-public-key-hex>\"}`). Removing the key or pinning the channel back to `\"stable\"` takes the node out of debug rollout eligibility.\n 26|\n*27|Channel pinning is exclusive. A debug-pinned node accepts only debug-channel offers and rejects stable offers until re-pinned to stable; a stable-pinned node accepts only stable-channel offers. This keeps rollback accounting and test provenance unambiguous.\n 28|\n 29|Maintainer tooling may provide a helper to write that file correctly, but the canonical mechanism remains the generic release-key overlay. `spt` itself does not need a special debug-mode command; source-repo tooling can offer convenience such as `xtask debug-pin` for local lab setup.\n 30|\n*31|Bad debug builds recover the same way as stable releases: publish a newer signed debug rollout, even if the artifact bytes are a previous known-good binary. Numeric rollback remains rejected; the debug channel never gets a bypass around the anti-rollback invariant.\n 32|\n 33|Debug signing key custody is deliberately weaker than stable release signing. The stable release seed stays manual/password-manager driven per ADR-0015; the debug seed may live in a long-lived `SPT_DEBUG_RELEASE_SEED` environment variable on the fast coordinator so agents can drive rollouts. This is acceptable only because the key's blast radius is limited to nodes explicitly pinned to `debug` and provisioned with that dev public key; rotation is minting a new debug key, updating lab overlays, and deleti...\n 34|\n...\n 50|\n*51|The first implementation slice is intentionally narrow: update-set metadata and verification, daemon staging/read path with local platform selection, maintainer `xtask` helpers for debug pinning and debug rollout, and loopback/two-host proof of propagation plus convergence reporting. Remote build orchestration, rich dashboards, adapter inclusion, and stable-release publishing migration are later work.\n 52|\n 53|## Considered Options\n 54|\n 55|- **Single artifact per staged update** — simpler metadata, but forces dev tooling to stage different binaries through platform-specific side paths and makes debug rollout brittle.\n*56|- **Debug-only remote copy/install scripts** — fastest to hack, but bypasses the verification, rollback, channel, and consent gates that peer propagation exists to protect.\n 57|- **Platform-targeted update set** — chosen: one generic substrate shape, recipients fetch only their matching artifact, and both stable releases and debug rollouts reuse the same trust path.\n### 0018-broker-brain-process-isolation-restoration.md\n 10|\n*11|On 2026-06-09, while verifying the v0.3.2 cross-OS update fix on the live fleet, a regression was discovered: **the production daemon runs the broker as a background thread inside the single `spt daemon` process** (`daemon.rs:165-170`: `Broker::bind_in_with_net` → `Arc<Broker>` served on `thread::spawn`; no broker child-spawn exists anywhere in the repo). This is **unintended drift** — no doc, plan, commit, or comment marks the collapse as a deliberate interim (the operator confirmed no such decision...\n 12|\n 13|Consequences of the drift:\n 14|- The brain cannot restart onto a new binary without killing the in-process broker thread, which would close every PTY, orphan every harness child, and drop every listening socket. **The no-endpoint-drop self-update pillar is therefore silently unrealized.**\n...\n 23|1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.\n*24|2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** \"the executable path\" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-sp...\n 25|3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`...\n 26|4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update...\n 27|5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).\n*28|6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable ...\n 29|7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.\n 30|8. **Cross-platform uniformity (Q8).** The broker spawns the brain as a child process (`Command::spawn`) + socket IPC — no `exec`, so no Windows/Unix divergence. ConPTY/forkpty handles never leave the broker.\n 31|\n...\n 34|- **Decomposition:** `spt daemon run` becomes the broker process entry (binds seed-control + broker socket + NetHost + digest hub; holds children + `EffectJournal`; spawns the brain); a new hidden `spt daemon brain` is the brain entry (connects, runs the logic loops, rehydrates from disk, emits `ready`). `ensure_running`/`is_running`/`daemon stop` contracts unchanged. The de-elevation guard applies at the broker entry.\n\n[Showing lines 1-333 of 399 (49.7KB limit). Use :334 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":".worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md, .worktrees/49-267-refresh-discriminator/docs/adr, .worktrees/49-267-refresh-discriminator/traceable-reqs.toml","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":79,"fileCount":20,"files":[".worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md",".worktrees/49-267-refresh-discriminator/docs/adr/0001-clean-room-fork-from-claude-skill-owl.md",".worktrees/49-267-refresh-discriminator/docs/adr/0002-bake-networking-into-core-collapse-spt-node.md",".worktrees/49-267-refresh-discriminator/docs/adr/0003-multi-instance-node-anchored-identity-model.md",".worktrees/49-267-refresh-discriminator/docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md",".worktrees/49-267-refresh-discriminator/docs/adr/0005-totp-seeded-spake2-node-pairing.md",".worktrees/49-267-refresh-discriminator/docs/adr/0006-multi-subnet-membership-model.md",".worktrees/49-267-refresh-discriminator/docs/adr/0007-subnet-notification-primitive.md",".worktrees/49-267-refresh-discriminator/docs/adr/0008-live-activity-buffer-pty-digest.md",".worktrees/49-267-refresh-discriminator/docs/adr/0009-endpoint-access-control-model.md",".worktrees/49-267-refresh-discriminator/docs/adr/0011-branchstore-git-kv-state-substrate.md",".worktrees/49-267-refresh-discriminator/docs/adr/0012-psyche-trust-boundary-stdout-event-outbound.md",".worktrees/49-267-refresh-discriminator/docs/adr/0013-psyche-reconciled-context-merge.md",".worktrees/49-267-refresh-discriminator/docs/adr/0014-publish-target-repo-and-lapse-proof-canonical-url.md",".worktrees/49-267-refresh-discriminator/docs/adr/0015-release-signing-two-key-trust-anchor.md",".worktrees/49-267-refresh-discriminator/docs/adr/0016-platform-targeted-update-sets.md",".worktrees/49-267-refresh-discriminator/docs/adr/0018-broker-brain-process-isolation-restoration.md",".worktrees/49-267-refresh-discriminator/docs/adr/0019-digest-adapter-declared-extractor-session-spanning.md",".worktrees/49-267-refresh-discriminator/docs/adr/0020-event-envelope-sole-arriving-format-reply-to-removed.md",".worktrees/49-267-refresh-discriminator/docs/adr/0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md"],"fileMatches":[{"path":".worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md","count":20},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0001-clean-room-fork-from-claude-skill-owl.md","count":1},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0002-bake-networking-into-core-collapse-spt-node.md","count":5},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0003-multi-instance-node-anchored-identity-model.md","count":1},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","count":7},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0005-totp-seeded-spake2-node-pairing.md","count":6},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0006-multi-subnet-membership-model.md","count":2},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0007-subnet-notification-primitive.md","count":1},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0008-live-activity-buffer-pty-digest.md","count":2},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0009-endpoint-access-control-model.md","count":1},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0011-branchstore-git-kv-state-substrate.md","count":2},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0012-psyche-trust-boundary-stdout-event-outbound.md","count":1},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0013-psyche-reconciled-context-merge.md","count":3},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0014-publish-target-repo-and-lapse-proof-canonical-url.md","count":1},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0015-release-signing-two-key-trust-anchor.md","count":2},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0016-platform-targeted-update-sets.md","count":7},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0018-broker-brain-process-isolation-restoration.md","count":4},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0019-digest-adapter-declared-extractor-session-spanning.md","count":6},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0020-event-envelope-sole-arriving-format-reply-to-removed.md","count":2},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md","count":5}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# .worktrees/49-267-refresh-discriminator/docs/\n## KNOWN-HAZARDS.md\n  23│### 1.3 Stale `index.lock` wedge from prior git crash\n *24│- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.\n  25│- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.\n  26│- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.\n  27│- **Sister cite:** CHANGELOG v1.11.20 \"Stale `index.lock`\"; `src/common/git.rs`.\n    │...\n  52│- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.\n *53│- **Invariant:** stdin-provided session_id wins; env is fallback only.\n  54│- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.\n  55│- **Sister cite:** CHANGELOG v1.35.1 \"IN-05\"; `hook_subagent_start.rs:40-51`.\n  56│\n *57│### 2.3 Binary-handoff argv schema must stay backward-compatible\n  58│- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.\n  59│- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.\n *60│- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.\n  61│- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\n  62│\n  63│### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\n    │...\n  65│- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes \"same gen continuing\" vs \"new gen born\".\n *66│- **spt-core mapping:** carries to the daemon's per-instance generation tracking.\n  67│- **Restoration D3/D4 (ADR-0018):** the generation *counter* custody moved to the broker (D3-2 — it observes every brain spawn, planned or crash, and hands `{generation, start-reason}` at spawn; `gen_start` stays `now()`-fresh, never rehydrated). The brain→brain **`BrainState` *message*** (`{session_id, generation, next_seq, gen_start_ms}`) that previously carried continuity across a handoff is **retired from the production path in D4-2**: a brain the supervisor respawns cold-starts and reconstructs ...\n  68│- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\n  69│\n    │...\n  79│<!-- [doc->REQ-SHELL-PERSISTENT-BOOT-RESTORE] -->\n *80│- **One process generation per read (releases#287):** modern `shell.launch.json` atomically stores `pid`, native `pid_started_at`, and wall-clock `launched_ms`. A modern `pid: 0` means explicitly pidless/cleared and never falls back to the numeric `shell.pid` mirror. Legacy records without `pid` retain their old numeric-PID plus optional-birth read. Publish the complete JSON identity before updating the compatibility mirror; failed publication preserves the previous identity. Never delete the previous bi...\n  81│- **No spontaneous relaunch of a NONPERSISTENT same-boot death:** the releases#78 force-kill freeze still protects nonpersistent instances, so killing one to free its exe does not arm a watcher underneath an overwrite. Recovery remains demand-driven. **Persistent instances are exempt** (operator ruling, releases#287): a kill is not a request to keep a declared-persistent shell down; use `teardown` or a nonpersistent manifest instead.\n  82│- **Watcher eligibility:** recorded `offline`, no live binary awaiting bind, and either `persistent`, no corpse, or a corpse whose launch predates the machine boot instant. A nonpersistent corpse without a launch stamp or boot oracle remains frozen; persistent instances require neither to become eligible. Birth-safe probes prevent a recycled pid from masquerading as a live shell.\n *83│- **spt-core mapping:** `spt_store::shellinfo::{shell_pid_provably_dead, effective_status, is_shell_online}` (the resolver — the shell-side twin of 2.5's `liveness.rs`, which gave *agent* perches exactly this and which shells never got); consumers in `linkhost::{relink_shell, prepare_drive, run_action}`, `shelldisc::discover`, `activity::observe_links`; the class-(c) eligibility read is `spt_daemon::shellwake::watcher_eligible` (+ `launch_predates_boot`, `BOOT_RESTORE_SLACK_MS`), and the birth stamp it...\n  84│- **Source:** spt-core, flynn's spt-alchemy field report (2026-07-25) — a deterministic recipe, not a race. Class-(c) re-based BAROMETER W2 (releases#78, doyle's ruling on comment 5156810267).\n  85│\n  86│### 2.7 A daemon or machine restart strands `persistent` shells — and a graceful cascade fixture suppresses the failing arm `[REQ-HAZARD-RESTART-STRANDS-PERSISTENT-SHELLS]`\n    │...\n  92│- **Per-owner enumeration:** the existing `SHELL_RECORD_HEALED:<owner>/<shell_id>` daemon diagnostic names **every** successfully healed instance, not one representative. Healthy and already-healed records produce no event. The restore diagnostics likewise identify each attempted instance and distinguish boot from owner-online triggers. <!-- [doc->REQ-SHELL-HEAL-ENUMERATION] -->\n *93│- **spt-core mapping:** `spt_daemon::shellwake::{heal_stale_online_records, restore_persistent_shells_at_boot, restore_persistent_shells_on_owner_online, OwnerOnlineEdge}`; `shellhost::live_launch_winner`; `spt_store::shellinfo::{ShellLaunch, shell_pid_provably_dead}`. `launch_predates_boot` and `BOOT_RESTORE_SLACK_MS` now govern only nonpersistent watcher eligibility.\n  94│- **Source:** BAROMETER W2 (releases#78); releases#287 operator ruling (2026-09-08), superseding the persistent force-kill freeze and boot-instant conjunct.\n  95│<!-- [doc->REQ-HAZARD-RESTART-STRANDS-PERSISTENT-SHELLS] -->\n  96│\n    │...\n 113│### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF\n*114│- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.\n 115│- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.\n 116│- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.\n 117│- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.\n    │...\n 132│<!-- [doc->REQ-PERCH-GC-RESIDUE-PREDICATE] -->\n*133│- **Failure:** the obvious way to write a perch garbage collector is \"reap what is not alive\" — `!spt_store::liveness::is_perch_alive(dir)`. On the population a GC actually walks, that predicate is **backwards**. `is_perch_alive` returns **true** for a directory with no `info.json` (`RawRead::Absent ⇒ true`, interim parity — an absent record means a wrapper-owned listener between polls), and **false** for a daemon-hosted endpoint whose `status` is `offline`. Measured on HFENDULEAM 2026-08-04 across...\n 134│- **Why it is a hazard and not a bug in the resolver:** `is_perch_alive` is correct for its own job. Fail-toward-alive is the safe direction for DELIVERY (2.5) — the cost of a false \"alive\" is one message taking the spool path. It is the wrong direction for DELETION, where a false \"not alive\" is irreversible. The inversion is what happens when a predicate is reused across a polarity boundary it was never sized for.\n 135│- **Invariant:** perch garbage collection classifies on record **presence** only (3.5's predicate: no `info.json` present on any read attempt ⇒ residue; NotFound kept distinct from every other I/O error, because absent is the only answer that authorizes deletion) and **never calls a liveness resolver** — not `is_perch_alive`, not `is_registry_entry_alive`, not a pid probe. The registry is likewise never asked: `clean_stale_entries` deletes dead-pid rows and `unregister_address` fires on an ordinary s...\n 136│- **spt-core mapping:** `spt_store::perchgc::{sweep, PerchClass}`; `spt endpoint gc` renders it. The refusal/report contract is `REQ-PERCH-GC-REFUSAL-REPORT`; the occupancy rules it inherits are 3.4.\n    │...\n 157│- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n*158│- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n*159│- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.\n 160│- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n 161│\n 162│### 4.4 Deferred rows survive poll drain\n    │...\n 208│- **Invariant (mitigation by construction, common case):** the common trigger — a full reinstall / identity regeneration — is covered by the **re-pair trust overwrite** (M8 decision 13, REQ-SUBNET-7): a completed ceremony presenting the same label + machine id evicts the superseded identity's trust AND registry rows on the seed-holder, and the peer-side epoch memory **dies with the deleted row** — the re-paired node's fresh epochs land on a clean lease. M8 acceptance 7 verifies this explicitly (the...\n*209│- **Residual (documented, guard deferred):** the narrow slice — epoch file lost while the node *identity* is kept (manual state surgery, partial restore from backup) — has no guard; it waits for a field hit before one is designed (M8 decision 24). `REQ-HAZARD-EPOCH-RESET` is minted inactive (TRACEABILITY rule 5) as the tracking hook. If hit: symptoms are one node's endpoints frozen-stale on every peer while its own views are fresh; recovery today is re-pairing the node (rides the common-case eviction...\n 210│- **spt-core mapping:** epoch mint = `spt_store::epoch::EpochSource` (`identity/epoch.json`); the lease = the per-`(endpoint, node)` epoch compare in `spt_net::net::registry`; the eviction that clears peer-side epoch memory = `registryhost::repair_evict_superseded` + `RegistryHost::consume_repair_evictions`.\n 211│- **Source:** minted at M8 ratification (decision 24), recognized as a class during the 2026-06-07 pump diagnosis / re-pair overwrite design — not yet field-hit in its residual form.\n 212│\n    │...\n 224│- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\n*225│- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\n 226│- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\n 227│- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\n 228│\n    │...\n 236│- **Failure:** canonicalized `\\\\?\\C:\\...` serializes to `//?/C:/...` and fails `read_to_string`.\n*237│- **Invariant:** strip the `\\\\?\\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.\n 238│- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.\n 239│- **Sister cite:** `src/common/owlery.rs:377-384`.\n 240│\n    │...\n 271│- **Failure:** `Instant::now() - Duration::from_secs(N)` panics `overflow when subtracting duration from instant` when the process's monotonic clock is younger than `N` — i.e. the host booted less than `N` ago. The peer pump primed its cadence legs with `Instant::now() - 86_400s` to mean \"everything due now\"; on a Windows runner with sub-24h uptime the pump thread panicked at startup, so the subnet never converged (CI `pump_and_dispatch_self_drive_the_subnet` failed, run 27082417706). It is *environmen...\n*272│- **Invariant:** NEVER compute an instant in the past by subtracting from `Instant::now()`. Represent \"never run / due now\" as `Option<Instant> = None` and gate on forward `now.duration_since(past)` only (`peerloop::due`). No backward instant arithmetic anywhere in scheduling.\n 273│- **Test seam caveat:** the convergence E2E only reproduces on a sub-offset-uptime host (it passed everywhere with >24h uptime). The deterministic guard is the `due(None, ..)`/`due(Some(now), ..)` unit on the extracted gate — it asserts first-tick-due with zero instant subtraction, independent of host uptime.\n 274│- **spt-core mapping:** `spt-daemon::peerloop::due` (the sole cadence gate behind `due_reg`/`due_notif`/`due_sync`/`due_upd`); cadence legs are `Option<Instant>` seeded `None`.\n 275│- **Source:** spt-core CI failure, 2026-06-07 — Windows runner `hfenduleam` (just booted) panicked the peer pump at the v0.1.1 release gate.\n    │...\n 278│- **Failure:** the elevation-gated commands (`subnet create` / `subnet join` / `show-code`) refuse when unelevated and tell the user to \"run as administrator / root\". The user types the obvious `sudo spt subnet create FOO` → `sudo: spt: command not found`. `spt` is a user-local install (`~/.local/bin`, `~/.cargo/bin`), and sudo's `secure_path` (a `/etc/sudoers` default) does NOT include those dirs, so a bare command name doesn't resolve under sudo. The guidance is a trap: it names an action that cannot...\n*279│- **Invariant:** elevation guidance on Unix emits the binary's **absolute path** under sudo — `sudo /home/u/.local/bin/spt subnet create FOO` — reconstructed from `current_exe()` + the real argv and shell-quoted. An absolute program path is executed directly; `secure_path` only governs bare-name PATH lookup, so the absolute form always resolves. On an interactive Unix TTY the command auto-elevates (re-execs itself under sudo, the elevated child does the work and `main` de-elevates back); non-interact...\n 280│- **Companion UX:** the post-de-elevation `DEELEVATED: running as uid N` line is internal state-safety noise — omit it from the user-facing CLI path (it confused the same field user). The detached daemon's own de-elevation log line is fine (it lands in the daemon log, not the terminal).\n 281│- **Test seam caveat:** the sudo re-exec needs a real `sudo` + TTY (not hermetic). The deterministic guard is the pure `elevation::sudo_argv` / `print_hint_command` (assert an absolute exe path, never a bare name, + shell-quoting on the printed line) and the `decide_elevation_path` matrix (which picks inline-sudo only on an interactive Unix TTY); the exec leg is manual/kitsubito-verified.\n 282│- **spt-core mapping:** `spt::elevation::{sudo_argv, print_hint_command, decide_elevation_path}` (pure — generalized from the M12-W4 self-elevation seam, 5.11), `spt::cli::{try_auto_elevate, with_elevation_hint}` wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`; `spt::main` de-elevation drop silenced.\n    │...\n 285│### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\n*286│- **Failure class:** a privilege-gated command (`subnet create` / `join` / `show-code`) self-elevates by re-launching itself with privilege (Windows UAC `runas`, Linux `pkexec` / a terminal-emulator `sudo`, or inline `sudo`). A careless re-launch is a security hole: widening the privilege scope (adding args), resolving the binary by a bare name (a PATH/`secure_path` hijack runs an attacker's `spt`), interpolating a crafted arg into a shell string (`sh -c \"… $id …\"` injects a second command), or re-el...\n*287│- **Invariant:** self-elevation re-runs the **EXACT** original invocation with the binary's **ABSOLUTE** exe path — never adding/altering args, never a PATH-resolved bare name, never a shell-interpolated string. Every launcher passes an **argv array** (`Command::new(prog).args([...])`, never `sh -c`); the Windows `ShellExecuteW` params string (which is inherently one string) MSVC-quotes each verbatim arg so `CommandLineToArgvW` round-trips it as a single token. The elevated child drops state back to th...\n 288│- **Test seam caveat:** the real launch needs a UAC/polkit/sudo prompt (not hermetic) — manual-verify. The deterministic guards are the pure `decide_elevation_path` matrix (loop-safety: `AlreadyElevated` on every os; the os×env path order) and the argv builders (`sudo_argv` / `pkexec_argv` / `terminal_argv` assert absolute-exe + verbatim args + array; `windows_runas_params` asserts MSVC-quoting with no `cmd /c` interpolation; the crafted-arg test asserts a shell-metachar arg stays one element / one qu...\n 289│- **spt-core mapping:** `spt::elevation::{decide_elevation_path, sudo_argv, pkexec_argv, terminal_argv, windows_runas_params, print_hint_command, ElevatePath}` (pure), `spt::cli::{try_auto_elevate, launch_uac_window, pause_elevated_console_if_fresh, program_on_path, first_terminal_emulator}` (impure launchers) wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`. Companions: 5.10 (the Unix abs-path-under-sudo facet) and 5.7 (the elevated child's de-elevation drop, which this compos...\n 290│- **Source:** M12-W4 design (subnet QR + self-elevating window), doyle ruling `M12-W4-RULING.md` Q6 — a privilege-escalation feature carries a mandatory hazard REQ.\n    │...\n 293│### 5.12 Native-PTY spawn of a bare program runs the wrong (non-PE) file on Windows  `[REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]`\n*294│- **Failure:** `portable-pty`'s ConPTY spawn resolves a bare program name with a `which` that takes the FIRST `PATH` match. A node/npm CLI installs as BOTH an extensionless shebang shim (`ccs`, for Git Bash) and a Windows launcher (`ccs.cmd`) in the same dir; portable-pty picks the extensionless `ccs`, and `CreateProcessW` then tries to execute that non-PE file and fails with **os error 193** (\"%1 is not a valid Win32 application\"). Live failure: `spt endpoint run claude-spt:ccs` → `CreateProcessW C:\\n...\n\n## adr/\n### 0001-clean-room-fork-from-claude-skill-owl.md\n  8│\n *9│spt-core is the rebuild of `claude_skill_owl` (today's modern SPT) into a harness-independent core. The sister project works but is fragile: each feature release introduces 2–5 regressions, often in unprecedented ways, because the substrate grew rapidly from a much narrower starting point and carries the resulting tech debt. spt-core also commits day-one to several architectural shapes (endpoint type system, networking, terminal wrapper as backbone, untethering from Claude Code) that change foundationa...\n 10│\n 11│## Decision\n 12│\n### 0002-bake-networking-into-core-collapse-spt-node.md\n  8│\n *9│This ADR retired GitHub as the *required/default* cross-node sync. It is now re-admitted as an **opt-in alternative transport for the context repo only** (`tracked/`), enabled by `spt context-github-setup`:\n 10│\n 11│- **Default stays P2P** + no-account — the core promise is unchanged.\n*12│- **Hub mode (opt-in):** every node pull→merge→pushes against a private GitHub remote (always-online sync hub + live GUI view), authed by a **shared deploy key** distributed as subnet secret material (only the setup node needs a GitHub account). **Prefer-GitHub, P2P backstop.**\n 13│- **Scope:** hub mode carries the **context repo only**. Messaging, registry, remote-drive, pairing, and presence always ride P2P/Iroh; gh-on never removes the P2P requirement. So the original decision (networking baked-in, first-class, P2P) stands in full — the hub is a context-sync convenience layered on top, not a return to GitHub-as-infrastructure.\n 14│\n 15│## Context\n 16│\n*17│The sister project (`claude_skill_owl`) decided networking should live in a separate `spt-node` daemon, explicitly to decouple the network stack from SPT's Claude-Code-tied core binary, expose SPT-protocol-as-product, and let networking evolve on its own release cycle. That decision predates spt-core.\n 18│\n 19│spt-core is a ground-up, harness-independent rebuild. The primary reason `spt-node` was separate — decoupling networking from a CC-tied binary — no longer exists, because spt-core is already decoupled from any harness. Meanwhile the product goal for networking sharpened: WAN cross-machine messaging should be **first-class and zero-config — \"just works.\"**\n 20│\n   │...\n 22│\n*23│Bake the WAN transport stack (Iroh + mDNS LAN discovery + pairing) into spt-core's library crates, behind a compile-time `net` feature flag (default-on in the reference binary, optional for embedded library consumers).\n 24│\n 25│\"Just works WAN, reachable even when no session is running\" requires a persistent per-machine network presence — the exact job `spt-node` was created to do. spt-core already commits to a single always-on, one-per-machine `spt-daemon` to supervise PTYs (the terminal wrapper). **Collapse the network-host role into that same daemon (option B1).** One process owns both PTYs and the node identity + WAN endpoint.\n 26│\n   │...\n 45│\n*46│- **SERIOUS #6 — one collapsed daemon = one failure domain.** A networking bug can take out terminal hosting; a PTY-broker bug can take out WAN reachability. Accepted (the collapse is locked), but v1 owes a **fault-injection acceptance matrix**: crash/hang Iroh, mDNS, registry sync, PTY broker, and manifest invocation independently; measure blast radius + recovery. Track at M4. → Begun M4-D4: **`docs/FAULT-MATRIX.md`** is the living matrix (proven rows with test evidence; planned rows tracked as debt).\n 47│- **Interaction with ADR-0004 (FATAL #4):** preserving live WAN streams across a brain restart forces Iroh endpoint / QUIC stream state into the \"stable broker,\" enlarging the kernel ADR-0002 wanted minimal. Resolved in ADR-0004's ownership table — \"stable\" now means *update cadence*, not *narrowness*.\n### 0003-multi-instance-node-anchored-identity-model.md\n  8│\n *9│The original decision below framed \"operate a LiveAgent on any node from any other machine\" primarily as **remote PTY drive**. That is backwards-weighted. Corrected model:\n 10│\n 11│- **Primary — instance:** the *same* endpoint (same harness+adapter) running **natively** on a machine (local files, local compute), with the **Psyche mind synced** across instances. Files/session-history are per-node and cannot teleport; the mind follows. This is the core meaning of \"same agent on multiple machines.\"\n 12│- **Instance dormancy:** typically one instance is actively-driven; others are **dormant** (live, state-preserved, re-activatable in place). Switching machines makes the previously-driven instance dormant — no explicit stop. Registry status is active / dormant / offline.\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n 10│\n*11│1. **Consolidation.** The sister project runs poll listeners as ephemeral per-session background tasks and Psyche wrappers as detached per-live-agent supervisor processes. But poll listeners already interact directly with the agent session (capsule/idle), and Psyche wrappers already invoke harness binaries directly. Once the daemon owns every PTY, keeping these as separate processes is unjustified.\n 12│\n 13│2. **Seamless self-update with a hard no-terminate constraint.** Self-update is a day-one pillar. The constraint: *no endpoint process may terminate or suspend during an spt-core update* — we cannot assume every endpoint can safely suspend. The naive \"drain + restart the daemon\" approach violates this for spt-hosted sessions (the daemon owns their PTY; killing the daemon SIGHUPs the child).\n 14│\n   │...\n 26│\n*27│**Self-update delivery:** peer-propagated over P2P, layered on self-fetch, out-of-band still supported. All binaries signature-verified before handoff (spt-core's own release key) regardless of source — peer-propagation otherwise lets one compromised node poison the subnet. spt-core conducts updates for the whole stack: self first, then ripple-update each registered adapter via the adapter manifest's update declaration (file-pull or delegated command). The plugin's role shrinks to initial bootstrap only.\n 28│\n 29│**Cadence/consent:** not fully automatic by default; gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto choice.\n 30│\n   │...\n 33│- The daemon is the single brain for a machine; crash-recovery and update logic must cover PTYs, networking, registry, spools, listeners, and psyche loops together.\n*34│- A small internal broker process exists beneath the daemon — a deliberate, bounded walk-back of \"literally one process,\" preserving B1's *intent* (one network identity, one supervisor, one firewall prompt) while guaranteeing endpoint survival across updates.\n 35│- Peer-propagated updates make release signing mandatory, not optional.\n 36│- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.\n 37│- The \"deliver to most-recently-active session\" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.\n   │...\n 57│| Accepted local client socket (`api listen`, relay) | **broker** | ✅ #1 (plain TCP) |\n*58│| Iroh endpoint + accepted QUIC streams | **broker** | ✅ #3 (loopback shape) → **implemented M4-D4a** (`spt-daemon::nethost::NetHost`: dedicated tokio runtime in the broker owns the endpoint + conn table; brain drives status/dial over IPC net frames) |\n 59│| mDNS socket / relay session | **broker** | ✅ closed by construction at M4-D4a (2026-06-03): `MdnsAddressLookup` and the relay session are constructed *inside* the broker-owned iroh endpoint — there is no separate socket to own |\n 60│| Listening sockets | **broker** | ✅ #1 |\n 61│| Routing, registry state, pulse/psyche loops, manifest parse, update orchestration | brain (rehydrate from disk) | n/a |\n   │...\n 68│### D. Self-update delivery hardening (resolves SERIOUS #5)\n*69│Signature-verify alone is insufficient. v1 adds: monotonic version field (rollback rejection), release-metadata expiry, channel pinning, key rotation/revocation list, and **adapter content signing** (adapter file-pull / delegated-command updates were uncovered by \"binary signature\"). Track as `REQ-HAZARD-UPDATE-ROLLBACK`.\n 70│\n 71│### E. Open spike gaps (must close before M3 builds the daemon)\n*72│1. ✅ **CLOSED (Spike #3, 2026-06-01).** Live Iroh + file-transfer stream survival across brain restart (FATAL #2). Spike #1 only proved PTY + plain TCP; Spike #3 stood up a broker-owned Iroh endpoint + uni QUIC transfer and proved a live peer download survives a brain restart gapless + exactly-once. Loopback shape validated; off-node transport + the QUIC-ownership *implementation* defer to M4. (`docs/spikes/SPIKE-03-quic-survival.md`.)\n 73│2. ✅ **CLOSED (Spike #4, 2026-06-01).** Linux `forkpty` parity. Spike #1's binary, unchanged, passed all four invariants on `gravity-linux` (Ubuntu 22.04) — `portable-pty` selects `forkpty` on Unix; the ConPTY-DSR branch is inert there. Invariant B passed with the *strict* contiguity check that ConPTY-under-resize fails (Spike #5): `forkpty` is a raw pipe, ConPTY a screen buffer — different stream contracts, promoted into M3a's `SessionSurface` design. (`docs/spikes/SPIKE-04-forkpty-parity.md`.)\n*74│3. ✅ **CLOSED (Spike #5, 2026-06-01).** 100× restart + resize-under-load stress (codex #3). The broker survived 100 rapid brain kill/restart cycles + a resize thread racing the reader with no leak (one child, clean reap), no hang (watchdog deadline), no lost byte (full value-set coverage in the client's observation window). Surfaced two binding ConPTY findings for M3a/REQ-TERM-3: resize triggers a repaint that reorders+dups the stream (terminal-stream contract ≠ exactly-once transfer contract); mid-...\n 75│4. ✅ **CLOSED (Spike #6, 2026-06-01).** Idempotent/exactly-once delivery across brain restart (FATAL-adjacent #14). Durable-ID + WAL + dedup-at-effect proven exactly-once across a crash at any protocol point (before-intent / before-effect / after-effect). Two binding design constraints surfaced for `REQ-HAZARD-RESTART-IDEMPOTENT` / M3b-B5: broker-owned recovery anchor; dedup-at-effect keyed by durable ID. (`docs/spikes/SPIKE-06-idempotent-boundary.md`.)\n 76│\n 77│**Gate status (2026-06-01): ALL FOUR §E GAPS CLOSED PASS** (#3, #4, #5, #6) → the M3-PLAN Phase-0 spike-gate is **complete**. The broker/brain split is validated on both OSes (ConPTY + `forkpty`), across QUIC-stream survival, restart+resize churn, and exactly-once idempotency. M3a and M3b are both unblocked; the QUIC-ownership *implementation* and off-node transport remain explicitly deferred to M4 (only the *shape* was spiked).\n### 0005-totp-seeded-spake2-node-pairing.md\n 43│- **FATAL #10 — trust-store delete is NOT revocation.** The \"revocation = trust-store delete; TOTP gates joining only\" consequence above is false in the threat model: the seed is replicated to every trusted node, so a compromised node/seed can re-pair as a *new* node after deletion. **Required:** a seed **epoch + rotation** protocol — removing a compromised node rotates the subnet seed so the old node/old seed cannot rejoin. Pairing transcript must bind the seed epoch.\n*44│- **SERIOUS #11 — SPAKE2's one-online-guess bound needs subnet-global rate limiting.** A public pre-trust relay + multiple seed-holding nodes enables *distributed* guessing, and a ±1 TOTP acceptance window triples the valid-password space. **Required:** one active pairing ceremony per subnet, shared attempt counter, exponential backoff; justify or drop the ±1 window.\n 45│- **SERIOUS #12 — \"PAKE binds pubkeys\" only if the transcript does.** Must explicitly bind roles, both node pubkeys, subnet ID, TOTP time-step, and confirmation MACs, or unknown-key-share / reflection / wrong-subnet pairing remain. **Required:** full pairing-transcript spec + negative tests (MITM, replay, reflection, wrong subnet, stale time-step).\n 46│\n 47│## Multi-subnet amendment (2026-06-01 — see ADR-0006)\n   │...\n 53│- **Rendezvous-token hashing.** The relay routes the pre-trust ceremony by `H(subnet-name ‖ TOTP-epoch)`, not the plaintext label — the user/agent enters the raw name + code; the hash is internal. (Payload is already E2E-encrypted; this protects the relay-visible routing token from leaking the human subnet label.) Composes with the #12 transcript binding (the subnet ID / seed epoch already in the transcript).\n*54│- **Per-subnet code fetch is gated behind OS elevation** (Windows UAC / Linux root-or-equivalent), or an elevated agent endpoint; else the user falls back to their authenticator app. Replaces the old unqualified \"fetchable from any trusted node\" with an elevation-gated, *per-subnet* fetch (a node now holds several subnets' seeds).\n 55│- **Code fetch is per-subnet** (a node holds *N* seeds): `spt pair show-totp` prompts which subnet (or `--subnet <name>` / `--create-new` to bypass), offered optionally as a QR / `otpauth://` URI.\n 56│- **Forward (cross-user, ADR-0006 seam):** the seed key becomes per-(subnet, user); the prompt becomes 2-stage. The #10 seed epoch+rotation and #11 subnet-global rate-limit requirements apply **per subnet**.\n 57│\n   │...\n 73│  shed at the accept door, and a ceremony timeout aligned with the limiter's\n*74│  stale-slot reclaim — an attacker reaching the listener still gets one\n*75│  online guess per admitted ceremony, serialized and backed off.\n 76│- **Membership mutations are elevation-gated** (the REQ-PAIR-6 machinery,\n 77│  exit 3, gate-before-everything): `subnet create` reveals a fresh subnet's\n 78│  joining secret, and `subnet join` enrolls the whole machine into a subnet's\n*79│  trust fabric — an unprivileged process must do neither (an attacker-run\n*80│  joiner would enroll the box into the attacker's subnet). `subnet status`\n 81│  stays ungated: read-only, membership shape only, never secrets.\n 82│\n 83│Surface rename, same semantics: `spt pair show-totp [--subnet|--create-new]`\n### 0006-multi-subnet-membership-model.md\n 18│\n*19│1. **Node-global identity, per-subnet visibility.** One `ling` exists on a node (one mind, one `tracked/` context); the node advertises it into each subnet it belongs to, subject to visibility (below). The subnet registry is **per-subnet**; the same endpoint can appear in several registries. Bare-id uniqueness holds *within* a subnet via a **join-time collision check**.\n 20│\n 21│2. **Qualified addressing + rippled rename.** Two combinable qualifiers — subnet-qualified `home:ling` and node-qualified `ling@hfenduleam`. When a bare id is ambiguous across visible subnets (two distinct endpoints sharing a name), resolution **refuses and forces qualification** rather than guessing. A new **`spt rename <id> <new_id>`** changes an endpoint's logical ID and ripples to all instances (registry, every perch, the `a-<id>` context branch), collision-checked against every target subnet and r...\n 22│\n   │...\n 24│\n*25│4. **Per-subnet code fetch gated behind OS elevation.** Retrieving a subnet's current code *from a node* requires Windows UAC / Linux root-or-equivalent, or an elevated agent endpoint; otherwise the user falls back to their own authenticator app. Narrows the multi-subnet exposure (mere CLI/agent presence no longer yields any subnet's join-code).\n 26│\n 27│5. **Subnet-exclusive sync.** An endpoint's mind replicates only within its home subnet by default. Endpoint config carries a **subnet-membership list** for sync; the mind replicates across every listed subnet's nodes. Distinct from visibility: being addressable in a subnet does not imply replicating the mind there.\n 28│\n### 0007-subnet-notification-primitive.md\n 15│- **Per-subnet, replicated spool.** Notifs are subnet events; the spool replicates across that subnet's nodes (reusing subnet-registry distribution, ADR-0006). Each notif is subnet-tagged; **dismiss-state replicates subnet-wide**, eventually consistent.\n*16│- **Two states:** *seen* (surfaced at least once, tracked per-endpoint) vs *dismissed* (explicit ack — `spt notif dismiss`, or the agent marks it). Surfacing ≠ dismissal.\n 17│- **First-fire** → the user's most-recently-active endpoint in that subnet (presence resolution). **Resurface** undismissed notifs at existing reported boundaries — state→active (`wake`), `api boundary clear`, `api boundary compact`, new-session-start — gated by *seen-per-endpoint* plus a **cross-endpoint suppression timeout** (default ~1h) so a notif can't bounce between endpoints. Scoped to subnets the endpoint is **visible in** (ADR-0006).\n 18│- **Delivery reuses messaging.** A notif to an agent is a notif-flavored envelope delivered to the agent's perch via the existing path; the agent surfaces it. An optional **`notif_command` manifest template** (on **both** harness-adapter and shell-adapter manifests) renders it endpoint-native (OS toast, GameRobot `alert-symbol`), combinable with agent-surface. If presence resolves to a **Shell** attached to the endpoint, the notif renders via *that shell's* template — the v1 seam and the M5 generalizat...\n 19│- **`spt notify`** lets any agent issue a subnet-wide notif (v1: reaches the user on their active endpoint). The **envelope `from`** carries the issuer's endpoint id + node + subnet (subnet surfaced only to multi-subnet receivers).\n### 0008-live-activity-buffer-pty-digest.md\n 12│\n*13│Add a **live activity buffer (PTY digest)** as a **distinct artifact** — not scrollback (raw bytes, R-TERM-2), not transcript history (full conversation behind communes/resume, Paths A/B). Its purpose is at-a-glance current activity, not replay.\n 14│\n 15│The parsing-boundary distinction that resolves the tension:\n 16│- **The adapter supplies the patterns; spt-core only runs them.** The manifest provides `input_pattern` + `agent_pattern` (each opens a span capturing everything, newlines included, until the next pattern match of any kind) and `tool_pattern` (a **list** — one per tool, plus a final **catchall** for user-installed tools, capturing tool-name + arg fragment). This is exactly Path A's \"adapter owns the parser\" rule and the \"spt-core owns the mechanism, adapter owns the pattern\" principle (identical to opa...\n   │...\n 35│\n*36│**Why (the Gateway grill, 2026-06-12):** the PTY byte stream of an alternate-screen TUI is a **rendering protocol, not a content log**. Scroll and resize trigger full repaints — the same content re-enters the stream and corrupts delta computation — and semantic turn boundaries drown in repaint traffic. The original §Decision rationale (\"a light rolling regex over bytes ≠ fragile transcript parsing\") proved **backwards in practice**: the byte stream is the *more* fragile surface, and transcript par...\n 37│\n 38│**What stands (the product surface, unchanged):** the formula (N-turn window, collapsed tool sprints, presentation-vs-extraction split), structured source-tagged output, both access modes (snapshot pull + structured-delta stream, deltas now keyed to log records), address-gating, Option-C persistence. The no-built-in-parser rule **still holds** — normalization remains adapter-owned (Path A) or adapter-pushed (Path B).\n 39│\n### 0009-endpoint-access-control-model.md\n 71│- The routing/messaging path (M4) gains a whitelist check on inbound: classify reply-vs-new (via `__REPLY_TO__` correlation) before applying the origin-node gate.\n*72│- The registry projection that backs resource advertisement must apply the visibility (and later whitelist) filter per viewer.\n 73│- **Forward (cross-user, ADR-0006 seam):** the whitelist's `users` tier and the \"nodes belonging to whitelisted users\" semantics activate with the per-(subnet, user) model.\n 74│- Builds on ADR-0006 (visibility, multi-subnet) and the existing consent grant store (CONTEXT §Consent & security gates).\n### 0011-branchstore-git-kv-state-substrate.md\n 30│filesystem — as the substrate for **coarse, durable, audited** state. It lives at\n*31│the bottom of the stack (`spt-store`) so context sync, the registry, and daemon\n 32│checkpointing share one abstraction rather than each re-inventing persistence.\n 33│\n 34│**BranchStore is for:**\n   │...\n 48│- **Merge-native sync** — reuse the context precedence merge driver + `--allow-unrelated-histories` for any BranchStore replicated cross-node.\n*49│- **GUI consequence** — state browsing becomes *git rendering*: raw inspection is free via standard git tooling; the product GUI (R-FRONT-1 panes) is a memformat-aware renderer over git, inheriting history / diff / time-travel rather than building a versioning backend.\n 50│\n 51│## Consequences\n 52│\n### 0012-psyche-trust-boundary-stdout-event-outbound.md\n 77│  be a **bounded, stdin-fed, stdout-captured** invocation (mirroring the sister's\n*78│  per-turn `--resume`), like the echo-commune's `run_bounded_stdin`. Tracked as\n 79│  KNOWN-HAZARDS 7.3 / `REQ-HAZARD-PSYCHE-OUTBOUND-PROXY`.\n 80│- **A marker parser must exist.** spt-core has no `<EVENT type=\"reply|notify\">`\n 81│  parse-and-relay path yet (the sister's `parse_markers`, `claude.rs:594`). It is\n### 0013-psyche-reconciled-context-merge.md\n 31│histories** — rejected outright; the context DAG must be shared. Third fork:\n*32│BranchStore backing (git CLI vs gitoxide).\n 33│\n 34│## Decision\n 35│\n   │...\n 40│- **Holding state is durable and replicated.** On detecting a concurrent pair,\n*41│  both versions persist as **tracked conflict artifacts** (deterministic\n 42│  content-hash names, in-repo, so conflict state itself syncs). The local\n 43│  working file stays untouched — the active mind is never clobbered by an\n 44│  unresolved conflict. An event seam fires (a D8 notif producer later).\n   │...\n 46│  active-instance-authoritative rule — the Psyche lives where the mind is being\n*47│  used). No active instance anywhere → deterministic fallback: lowest node id\n 48│  among conflict holders. This kills reconcile storms (two nodes merging the\n 49│  same pair into textually-different results, conflicting again).\n 50│- **The merged write carries `join(vA, vB)` + a bump of the reconciler's own\n### 0014-publish-target-repo-and-lapse-proof-canonical-url.md\n  7│\n *8│**Canonical URL = `https://sabermage.github.io/spt-releases`, with no custom domain configured on Pages.** This URL is what gets baked everywhere: docs cross-links, `llms.txt`, the install one-liner in the README, and every adapter's pack-in install logic.\n  9│\n 10│## Considered options\n 11│\n### 0015-release-signing-two-key-trust-anchor.md\n  2│\n *3│Every shipped binary embeds the release **trusted-key set** that `spt update` verifies against (R-UPD-2), so the keys chosen at v0.1 anchor the self-update trust chain for every deployed node. We mint **two Ed25519 keypairs**: a **primary** (active — signs every release, lives on the maintainer's dev machine) and a **recovery** (generated once, never used, never stored on the dev machine). **Both public keys ship in the binary's trusted set.** Both private seeds (32 bytes each) are backed up independen...\n  4│\n  5│Loss of the primary (e.g. dev machine destroyed) is then a non-event: the next release is signed with the recovery key and rotates in a fresh primary (the verify path already supports key revocation). Without the second key, losing the sole key would kill the self-update chain — nodes keep running but every future release fails verification until users manually re-run the installer.\n  6│\n   │...\n  9│- **Signing key as a CI secret (automated signing)** — rejected for now. The runners are self-hosted dev boxes running arbitrary dev workloads; a release key in their secret store is weaker custody than a manual local sign, and release cadence doesn't justify automation. Revisit if cadence demands it.\n*10│- **Single key + good backups** — rejected; rotation-after-loss requires a *second already-trusted* key, otherwise recovery means out-of-band reinstall for every user.\n 11│\n 12│## Consequences\n 13│\n### 0016-platform-targeted-update-sets.md\n 16│\n*17│Platform keys in the signed metadata use Rust target triples, for example `x86_64-pc-windows-msvc` and `x86_64-unknown-linux-gnu`. Release asset names remain a packaging concern and may keep their public-friendly names; target triples are the protocol identifiers.\n 18│\n 19│This keeps debug rollout fast without turning it into raw peer file-copy: a fast coordinator can assemble and sign every platform artifact, then let the existing subnet update machinery distribute the right bytes to each node. The generic receiver/protocol support belongs in the production daemon because public releases and debug rollouts have the same cross-platform distribution problem. The rollout driver does not: build recipes, cross-build/WSL/runner coordination, dev-key signing, and \"stage this deb...\n 20│\n*21│Release monotonicity is scoped to the pinned channel. Stable releases and debug rollouts use separate counters, so a fast debug sequence cannot consume, block, or roll back the stable release counter; rollback protection compares only against the latest accepted version for the recipient's pinned channel.\n 22│\n 23│Debug rollout targets are opt-in by channel pin, not by subnet membership alone. A debug offer may traverse trusted subnet peers, but a stable-pinned node rejects it at the metadata gate; only nodes pinned to the debug channel and provisioned with the dev signing key can stage and apply it.\n 24│\n*25│Pinning a lab node to debug uses the existing release-key overlay: `$SPT_HOME/identity/release-keys.json` adds the dev public key and sets `\"channel\": \"debug\"` (for example `\"keys\": {\"dev-debug-2026\": \"<ed25519-public-key-hex>\"}`). Removing the key or pinning the channel back to `\"stable\"` takes the node out of debug rollout eligibility.\n 26│\n*27│Channel pinning is exclusive. A debug-pinned node accepts only debug-channel offers and rejects stable offers until re-pinned to stable; a stable-pinned node accepts only stable-channel offers. This keeps rollback accounting and test provenance unambiguous.\n 28│\n 29│Maintainer tooling may provide a helper to write that file correctly, but the canonical mechanism remains the generic release-key overlay. `spt` itself does not need a special debug-mode command; source-repo tooling can offer convenience such as `xtask debug-pin` for local lab setup.\n 30│\n*31│Bad debug builds recover the same way as stable releases: publish a newer signed debug rollout, even if the artifact bytes are a previous known-good binary. Numeric rollback remains rejected; the debug channel never gets a bypass around the anti-rollback invariant.\n 32│\n 33│Debug signing key custody is deliberately weaker than stable release signing. The stable release seed stays manual/password-manager driven per ADR-0015; the debug seed may live in a long-lived `SPT_DEBUG_RELEASE_SEED` environment variable on the fast coordinator so agents can drive rollouts. This is acceptable only because the key's blast radius is limited to nodes explicitly pinned to `debug` and provisioned with that dev public key; rotation is minting a new debug key, updating lab overlays, and deleti...\n 34│\n   │...\n 50│\n*51│The first implementation slice is intentionally narrow: update-set metadata and verification, daemon staging/read path with local platform selection, maintainer `xtask` helpers for debug pinning and debug rollout, and loopback/two-host proof of propagation plus convergence reporting. Remote build orchestration, rich dashboards, adapter inclusion, and stable-release publishing migration are later work.\n 52│\n 53│## Considered Options\n 54│\n 55│- **Single artifact per staged update** — simpler metadata, but forces dev tooling to stage different binaries through platform-specific side paths and makes debug rollout brittle.\n*56│- **Debug-only remote copy/install scripts** — fastest to hack, but bypasses the verification, rollback, channel, and consent gates that peer propagation exists to protect.\n 57│- **Platform-targeted update set** — chosen: one generic substrate shape, recipients fetch only their matching artifact, and both stable releases and debug rollouts reuse the same trust path.\n### 0018-broker-brain-process-isolation-restoration.md\n 10│\n*11│On 2026-06-09, while verifying the v0.3.2 cross-OS update fix on the live fleet, a regression was discovered: **the production daemon runs the broker as a background thread inside the single `spt daemon` process** (`daemon.rs:165-170`: `Broker::bind_in_with_net` → `Arc<Broker>` served on `thread::spawn`; no broker child-spawn exists anywhere in the repo). This is **unintended drift** — no doc, plan, commit, or comment marks the collapse as a deliberate interim (the operator confirmed no such decision...\n 12│\n 13│Consequences of the drift:\n 14│- The brain cannot restart onto a new binary without killing the in-process broker thread, which would close every PTY, orphan every harness child, and drop every listening socket. **The no-endpoint-drop self-update pillar is therefore silently unrealized.**\n   │...\n 23│1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.\n*24│2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** \"the executable path\" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-sp...\n 25│3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`...\n 26│4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update...\n 27│5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).\n*28│6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable ...\n 29│7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.\n 30│8. **Cross-platform uniformity (Q8).** The broker spawns the brain as a child process (`Command::spawn`) + socket IPC — no `exec`, so no Windows/Unix divergence. ConPTY/forkpty handles never leave the broker.\n 31│\n   │...\n 34│- **Decomposition:** `spt daemon run` becomes the broker process entry (binds seed-control + broker socket + NetHost + digest hub; holds children + `EffectJournal`; spawns the brain); a new hidden `spt daemon brain` is the brain entry (connects, runs the logic loops, rehydrates from disk, emits `ready`). `ensure_running`/`is_running`/`daemon stop` contracts unchanged. The de-elevation guard applies at the broker entry.\n*35│- **Requirement registry:** REQ-DAEMON-2 and REQ-UPD-3's `int` evidence must be **re-pointed** to a productionized SPIKE-01/03 E2E proving *process-level* endpoint survival (`[V5]`). Two new hazards are minted: `REQ-HAZARD-BROKER-PROCESS-ISOLATION` (a brain restart must never drop a hosted endpoint) and `REQ-HAZARD-ROLLBACK-STATE-COMPAT`. A KNOWN-HAZARDS §6.7 entry records the regression.\n 36│- **Sequencing:** the restoration is the next milestone, before `spt-claude-code` scoping (operator-accepted 2026-06-09). Rationale: it is the *last* release that needs a manual fleet daemon bounce — paid 3× for v0.3.2 — so every adapter-era release rolls seamlessly; and the adapter is better built on the final topology than atop a daemon under later surgery while it hosts the user's daily driver. The split changes daemon internals, not the M8-frozen CLI/api surface.\n 37│- **Out of scope (unchanged):** the broker-touching update classes (broker-compatible / broker-breaking) remain as ADR-0004 left them; whole-daemon FD-passing stays the deferred \"future polish\" (ADR-0004:38); a durable in-daemon alarm scheduler is a separate gap (alarms are legacy-listener-only today).\n### 0019-digest-adapter-declared-extractor-session-spanning.md\n  4│\n *5│accepted (2026-06-13) · **supersedes the source-mechanism lineage of [ADR-0008](0008-live-activity-buffer-pty-digest.md)** (its 2026-06-01 PTY-pattern decision, the 2026-06-12 \"normalized logs\" amendment, and the M9 \"no manifest seam / rides `[history]`\" reconciliation). The **product surface** defined in ADR-0008 (a rolling, glanceable activity view distinct from scrollback and transcript) still stands and is broadened here.\n  6│\n  7│## Context\n  8│\n   │...\n 21│\n*22│- **Imperative extractor, not a declarative DSL.** `[digest]` declares an adapter-supplied command that maps the harness's native log → the `{role, text, tool, ts}` contract. By default it reads the **same source files as `[history]`** (reference its locate pattern; DRY) with an **own-source escape hatch**; **`api digest-entry`** remains the always-available push fallback. We reject a declarative match/extract DSL: a flat map cannot express real nested logs, and a DSL powerful enough to do so reinvents...\n*23│- **Thread-spanning.** A per-endpoint **session ledger** (heir to the sister project's `psyches/tracked/agents/<id>/sessions.log`) records the agent's session rotations; `api boundary` appends the new `session_id` at each `/clear` / `/compact`. The digest enumerates the last *K* sessions (newest-first, bounded), so the rolling window **bridges** a boundary. A session boundary is **rendered distinctively** (a visible `/clear` divider) so a reader sees the context reset without losing the thread.\n*24│- **Presentation is adapter-defaulted, consumer-overridable.** Window depth, arg-truncation, and sprint-collapse become `[digest]` defaults the adapter declares and any consumer may override at pull/subscribe; spt-core ships fallback defaults. The fixed \"last ~3 turns\" is dropped as a spt-core requirement. Extraction stays the adapter's; *how much* is shown stops being spt-core dogma.\n 25│- **Two-origin merge.** The digest interleaves, by `ts`, the adapter's **extracted log records** (agent activity) and spt's own **context-injection entries** — a new collapsible category with subtypes `psyche_download | echo_commune | owl_message` (the middle subtype was named `echo_mirror` here until releases#113; nothing ever mirrored an echo-commune into a running context, and the name was read as a promise that it did). spt appends its injection entries to the endpoint's `digest.log` (the existing ...\n 26│- **Diagnostics are first-class.** `spt adapter digest-proof <adapter> [--sample <log>]` runs the declared extractor over a real sample and prints the parsed records, the rendered digest, and every dropped/malformed line with its reason. The daemon surfaces the same skip-diagnostics at runtime. The silent drop is removed.\n 27│\n   │...\n 32│- **Breaking for adapter authors** (again): the M9 guidance (\"emit the contract through your `[history]` normalizer\") is replaced by \"declare a `[digest]` extractor (or push via `api digest-entry`).\" The v0.5.0 release note that announced the opposite is wrong and must be corrected; the **harness integration checklist** in the docs is updated as part of this milestone.\n*33│- `digest.log` changes role from a *fallback source* to a *merge input* (it now also carries spt's injection entries).\n 34│- New `REQ-*` ids must be minted before implementation (traceability rule 3); the relevant `REQ-TERM-*` titles/evidence re-point in the same restoration discipline M9 used.\n 35│- ADR-0008's source mechanism is now fully superseded; only its product-surface framing survives (and is broadened from \"recent activity\" to \"recent context\").\n 36│\n   │...\n 42│- **Follow-and-reset window** (digest shows only the post-`/clear` session) — rejected: loses thread continuity, the whole point of session-spanning; a glance right after `/clear` would show an idle agent mid-task.\n*43│- **Build all of the context-mirroring feature now** (GUI + echo-feedback) — rejected: builds consumers whose surfaces do not exist yet; only the data model is expensive to retrofit, so only it lands now.\n### 0020-event-envelope-sole-arriving-format-reply-to-removed.md\n  24│them into the `__REPLY_TO__:` string → for the listener, `render_event_lines`\n *25│parses that string **back** into `(from, body)` to compose `<EVENT>`. A pointless\n  26│round-trip through a relic, and `api poll` hands the relic straight to adapters.\n  27│\n  28│This surfaced when **perri** (building the `spt-claude-code` / `sptc` adapter from\n    │...\n 147│  REQ needed.\n*148│- A new requirement (**REQ-MSG-ENVELOPE**) tracks the envelope-everywhere\n 149│  delivery contract + the relic removal; `doc` evidenced here, `impl/unit/int`\n 150│  activate when the refactor lands.\n 151│- **Downstream (spt-claude-code / sptc):** the adapter parses the `<EVENT>`\n### 0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md\n  27│   carries no adapter — and pushes adapter-side complexity (the seed must be\n *28│   passed an adapter; seed+listen must be chained in one background process so\n  29│   they share a pid; etc.).\n  30│2. **The seed snapshots a single adapter** at seed-time. A machine that hosts\n  31│   more than one harness adapter for the same binary, or that `adapter add`s one\n    │...\n  62│**4. Profile selection: an explicit pointer, with a `registered_at_ms`\n *63│fallback.**\n  64│- A durable **active-profile pointer** — `$SPT_HOME/adapters/active-profiles.toml`,\n  65│  a flat `host_binary → \"adapter[:profile]\"` map — is the primary. Read at bind.\n *66│- **Unset** → fall back to the candidate adapter with the greatest\n  67│  `registered_at_ms` (the recorded registration timestamp on `record.toml`,\n  68│  sturdier than fs-mtime), **base profile** (a specific *profile* is only ever\n  69│  chosen by the pointer); name-ascending on ties. So a fresh single-adapter\n    │...\n  74│  A stale pointer (uninstalled adapter / deleted profile) self-heals: ignored,\n *75│  fall back, warn once. Pruned on `adapter remove`.\n  76│- Stored at the registry root, **sibling to the per-adapter `<name>/` dirs**, so\n  77│  `adapter add/update/remove` (which only rewrite a `<name>/` subdir) can never\n  78│  clobber it.\n    │...\n 113│  mtime, silently flipping the active profile. Kept only as the spirit of the\n*114│  unset-fallback, now hardened to `registered_at_ms` + an explicit pointer.\n 115│- **Auto-write the pointer on install/update**: rejected — re-introduces the\n 116│  update-churn footgun; absence-means-heuristic is the correct default.","truncation":{"content":"# .worktrees/49-267-refresh-discriminator/docs/\n## KNOWN-HAZARDS.md\n 23|### 1.3 Stale `index.lock` wedge from prior git crash\n*24|- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.\n 25|- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.\n 26|- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.\n 27|- **Sister cite:** CHANGELOG v1.11.20 \"Stale `index.lock`\"; `src/common/git.rs`.\n...\n 52|- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.\n*53|- **Invariant:** stdin-provided session_id wins; env is fallback only.\n 54|- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.\n 55|- **Sister cite:** CHANGELOG v1.35.1 \"IN-05\"; `hook_subagent_start.rs:40-51`.\n 56|\n*57|### 2.3 Binary-handoff argv schema must stay backward-compatible\n 58|- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.\n 59|- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.\n*60|- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.\n 61|- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\n 62|\n 63|### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\n...\n 65|- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes \"same gen continuing\" vs \"new gen born\".\n*66|- **spt-core mapping:** carries to the daemon's per-instance generation tracking.\n 67|- **Restoration D3/D4 (ADR-0018):** the generation *counter* custody moved to the broker (D3-2 — it observes every brain spawn, planned or crash, and hands `{generation, start-reason}` at spawn; `gen_start` stays `now()`-fresh, never rehydrated). The brain→brain **`BrainState` *message*** (`{session_id, generation, next_seq, gen_start_ms}`) that previously carried continuity across a handoff is **retired from the production path in D4-2**: a brain the supervisor respawns cold-starts and reconstructs ...\n 68|- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\n 69|\n...\n 79|<!-- [doc->REQ-SHELL-PERSISTENT-BOOT-RESTORE] -->\n*80|- **One process generation per read (releases#287):** modern `shell.launch.json` atomically stores `pid`, native `pid_started_at`, and wall-clock `launched_ms`. A modern `pid: 0` means explicitly pidless/cleared and never falls back to the numeric `shell.pid` mirror. Legacy records without `pid` retain their old numeric-PID plus optional-birth read. Publish the complete JSON identity before updating the compatibility mirror; failed publication preserves the previous identity. Never delete the previous bi...\n 81|- **No spontaneous relaunch of a NONPERSISTENT same-boot death:** the releases#78 force-kill freeze still protects nonpersistent instances, so killing one to free its exe does not arm a watcher underneath an overwrite. Recovery remains demand-driven. **Persistent instances are exempt** (operator ruling, releases#287): a kill is not a request to keep a declared-persistent shell down; use `teardown` or a nonpersistent manifest instead.\n 82|- **Watcher eligibility:** recorded `offline`, no live binary awaiting bind, and either `persistent`, no corpse, or a corpse whose launch predates the machine boot instant. A nonpersistent corpse without a launch stamp or boot oracle remains frozen; persistent instances require neither to become eligible. Birth-safe probes prevent a recycled pid from masquerading as a live shell.\n*83|- **spt-core mapping:** `spt_store::shellinfo::{shell_pid_provably_dead, effective_status, is_shell_online}` (the resolver — the shell-side twin of 2.5's `liveness.rs`, which gave *agent* perches exactly this and which shells never got); consumers in `linkhost::{relink_shell, prepare_drive, run_action}`, `shelldisc::discover`, `activity::observe_links`; the class-(c) eligibility read is `spt_daemon::shellwake::watcher_eligible` (+ `launch_predates_boot`, `BOOT_RESTORE_SLACK_MS`), and the birth stamp it...\n 84|- **Source:** spt-core, flynn's spt-alchemy field report (2026-07-25) — a deterministic recipe, not a race. Class-(c) re-based BAROMETER W2 (releases#78, doyle's ruling on comment 5156810267).\n 85|\n 86|### 2.7 A daemon or machine restart strands `persistent` shells — and a graceful cascade fixture suppresses the failing arm `[REQ-HAZARD-RESTART-STRANDS-PERSISTENT-SHELLS]`\n...\n 92|- **Per-owner enumeration:** the existing `SHELL_RECORD_HEALED:<owner>/<shell_id>` daemon diagnostic names **every** successfully healed instance, not one representative. Healthy and already-healed records produce no event. The restore diagnostics likewise identify each attempted instance and distinguish boot from owner-online triggers. <!-- [doc->REQ-SHELL-HEAL-ENUMERATION] -->\n*93|- **spt-core mapping:** `spt_daemon::shellwake::{heal_stale_online_records, restore_persistent_shells_at_boot, restore_persistent_shells_on_owner_online, OwnerOnlineEdge}`; `shellhost::live_launch_winner`; `spt_store::shellinfo::{ShellLaunch, shell_pid_provably_dead}`. `launch_predates_boot` and `BOOT_RESTORE_SLACK_MS` now govern only nonpersistent watcher eligibility.\n 94|- **Source:** BAROMETER W2 (releases#78); releases#287 operator ruling (2026-09-08), superseding the persistent force-kill freeze and boot-instant conjunct.\n 95|<!-- [doc->REQ-HAZARD-RESTART-STRANDS-PERSISTENT-SHELLS] -->\n 96|\n...\n 113|### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF\n*114|- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.\n 115|- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.\n 116|- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.\n 117|- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.\n...\n 132|<!-- [doc->REQ-PERCH-GC-RESIDUE-PREDICATE] -->\n*133|- **Failure:** the obvious way to write a perch garbage collector is \"reap what is not alive\" — `!spt_store::liveness::is_perch_alive(dir)`. On the population a GC actually walks, that predicate is **backwards**. `is_perch_alive` returns **true** for a directory with no `info.json` (`RawRead::Absent ⇒ true`, interim parity — an absent record means a wrapper-owned listener between polls), and **false** for a daemon-hosted endpoint whose `status` is `offline`. Measured on HFENDULEAM 2026-08-04 across...\n 134|- **Why it is a hazard and not a bug in the resolver:** `is_perch_alive` is correct for its own job. Fail-toward-alive is the safe direction for DELIVERY (2.5) — the cost of a false \"alive\" is one message taking the spool path. It is the wrong direction for DELETION, where a false \"not alive\" is irreversible. The inversion is what happens when a predicate is reused across a polarity boundary it was never sized for.\n 135|- **Invariant:** perch garbage collection classifies on record **presence** only (3.5's predicate: no `info.json` present on any read attempt ⇒ residue; NotFound kept distinct from every other I/O error, because absent is the only answer that authorizes deletion) and **never calls a liveness resolver** — not `is_perch_alive`, not `is_registry_entry_alive`, not a pid probe. The registry is likewise never asked: `clean_stale_entries` deletes dead-pid rows and `unregister_address` fires on an ordinary s...\n 136|- **spt-core mapping:** `spt_store::perchgc::{sweep, PerchClass}`; `spt endpoint gc` renders it. The refusal/report contract is `REQ-PERCH-GC-REFUSAL-REPORT`; the occupancy rules it inherits are 3.4.\n...\n 157|- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n*158|- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n*159|- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.\n 160|- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n 161|\n 162|### 4.4 Deferred rows survive poll drain\n...\n 208|- **Invariant (mitigation by construction, common case):** the common trigger — a full reinstall / identity regeneration — is covered by the **re-pair trust overwrite** (M8 decision 13, REQ-SUBNET-7): a completed ceremony presenting the same label + machine id evicts the superseded identity's trust AND registry rows on the seed-holder, and the peer-side epoch memory **dies with the deleted row** — the re-paired node's fresh epochs land on a clean lease. M8 acceptance 7 verifies this explicitly (the...\n*209|- **Residual (documented, guard deferred):** the narrow slice — epoch file lost while the node *identity* is kept (manual state surgery, partial restore from backup) — has no guard; it waits for a field hit before one is designed (M8 decision 24). `REQ-HAZARD-EPOCH-RESET` is minted inactive (TRACEABILITY rule 5) as the tracking hook. If hit: symptoms are one node's endpoints frozen-stale on every peer while its own views are fresh; recovery today is re-pairing the node (rides the common-case eviction...\n 210|- **spt-core mapping:** epoch mint = `spt_store::epoch::EpochSource` (`identity/epoch.json`); the lease = the per-`(endpoint, node)` epoch compare in `spt_net::net::registry`; the eviction that clears peer-side epoch memory = `registryhost::repair_evict_superseded` + `RegistryHost::consume_repair_evictions`.\n 211|- **Source:** minted at M8 ratification (decision 24), recognized as a class during the 2026-06-07 pump diagnosis / re-pair overwrite design — not yet field-hit in its residual form.\n 212|\n...\n 224|- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\n*225|- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\n 226|- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\n 227|- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\n 228|\n...\n 236|- **Failure:** canonicalized `\\\\?\\C:\\...` serializes to `//?/C:/...` and fails `read_to_string`.\n*237|- **Invariant:** strip the `\\\\?\\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.\n 238|- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.\n 239|- **Sister cite:** `src/common/owlery.rs:377-384`.\n 240|\n...\n 271|- **Failure:** `Instant::now() - Duration::from_secs(N)` panics `overflow when subtracting duration from instant` when the process's monotonic clock is younger than `N` — i.e. the host booted less than `N` ago. The peer pump primed its cadence legs with `Instant::now() - 86_400s` to mean \"everything due now\"; on a Windows runner with sub-24h uptime the pump thread panicked at startup, so the subnet never converged (CI `pump_and_dispatch_self_drive_the_subnet` failed, run 27082417706). It is *environmen...\n*272|- **Invariant:** NEVER compute an instant in the past by subtracting from `Instant::now()`. Represent \"never run / due now\" as `Option<Instant> = None` and gate on forward `now.duration_since(past)` only (`peerloop::due`). No backward instant arithmetic anywhere in scheduling.\n 273|- **Test seam caveat:** the convergence E2E only reproduces on a sub-offset-uptime host (it passed everywhere with >24h uptime). The deterministic guard is the `due(None, ..)`/`due(Some(now), ..)` unit on the extracted gate — it asserts first-tick-due with zero instant subtraction, independent of host uptime.\n 274|- **spt-core mapping:** `spt-daemon::peerloop::due` (the sole cadence gate behind `due_reg`/`due_notif`/`due_sync`/`due_upd`); cadence legs are `Option<Instant>` seeded `None`.\n 275|- **Source:** spt-core CI failure, 2026-06-07 — Windows runner `hfenduleam` (just booted) panicked the peer pump at the v0.1.1 release gate.\n...\n 278|- **Failure:** the elevation-gated commands (`subnet create` / `subnet join` / `show-code`) refuse when unelevated and tell the user to \"run as administrator / root\". The user types the obvious `sudo spt subnet create FOO` → `sudo: spt: command not found`. `spt` is a user-local install (`~/.local/bin`, `~/.cargo/bin`), and sudo's `secure_path` (a `/etc/sudoers` default) does NOT include those dirs, so a bare command name doesn't resolve under sudo. The guidance is a trap: it names an action that cannot...\n*279|- **Invariant:** elevation guidance on Unix emits the binary's **absolute path** under sudo — `sudo /home/u/.local/bin/spt subnet create FOO` — reconstructed from `current_exe()` + the real argv and shell-quoted. An absolute program path is executed directly; `secure_path` only governs bare-name PATH lookup, so the absolute form always resolves. On an interactive Unix TTY the command auto-elevates (re-execs itself under sudo, the elevated child does the work and `main` de-elevates back); non-interact...\n 280|- **Companion UX:** the post-de-elevation `DEELEVATED: running as uid N` line is internal state-safety noise — omit it from the user-facing CLI path (it confused the same field user). The detached daemon's own de-elevation log line is fine (it lands in the daemon log, not the terminal).\n 281|- **Test seam caveat:** the sudo re-exec needs a real `sudo` + TTY (not hermetic). The deterministic guard is the pure `elevation::sudo_argv` / `print_hint_command` (assert an absolute exe path, never a bare name, + shell-quoting on the printed line) and the `decide_elevation_path` matrix (which picks inline-sudo only on an interactive Unix TTY); the exec leg is manual/kitsubito-verified.\n 282|- **spt-core mapping:** `spt::elevation::{sudo_argv, print_hint_command, decide_elevation_path}` (pure — generalized from the M12-W4 self-elevation seam, 5.11), `spt::cli::{try_auto_elevate, with_elevation_hint}` wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`; `spt::main` de-elevation drop silenced.\n...\n 285|### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\n*286|- **Failure class:** a privilege-gated command (`subnet create` / `join` / `show-code`) self-elevates by re-launching itself with privilege (Windows UAC `runas`, Linux `pkexec` / a terminal-emulator `sudo`, or inline `sudo`). A careless re-launch is a security hole: widening the privilege scope (adding args), resolving the binary by a bare name (a PATH/`secure_path` hijack runs an attacker's `spt`), interpolating a crafted arg into a shell string (`sh -c \"… $id …\"` injects a second command), or re-el...\n*287|- **Invariant:** self-elevation re-runs the **EXACT** original invocation with the binary's **ABSOLUTE** exe path — never adding/altering args, never a PATH-resolved bare name, never a shell-interpolated string. Every launcher passes an **argv array** (`Command::new(prog).args([...])`, never `sh -c`); the Windows `ShellExecuteW` params string (which is inherently one string) MSVC-quotes each verbatim arg so `CommandLineToArgvW` round-trips it as a single token. The elevated child drops state back to th...\n 288|- **Test seam caveat:** the real launch needs a UAC/polkit/sudo prompt (not hermetic) — manual-verify. The deterministic guards are the pure `decide_elevation_path` matrix (loop-safety: `AlreadyElevated` on every os; the os×env path order) and the argv builders (`sudo_argv` / `pkexec_argv` / `terminal_argv` assert absolute-exe + verbatim args + array; `windows_runas_params` asserts MSVC-quoting with no `cmd /c` interpolation; the crafted-arg test asserts a shell-metachar arg stays one element / one qu...\n 289|- **spt-core mapping:** `spt::elevation::{decide_elevation_path, sudo_argv, pkexec_argv, terminal_argv, windows_runas_params, print_hint_command, ElevatePath}` (pure), `spt::cli::{try_auto_elevate, launch_uac_window, pause_elevated_console_if_fresh, program_on_path, first_terminal_emulator}` (impure launchers) wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`. Companions: 5.10 (the Unix abs-path-under-sudo facet) and 5.7 (the elevated child's de-elevation drop, which this compos...\n 290|- **Source:** M12-W4 design (subnet QR + self-elevating window), doyle ruling `M12-W4-RULING.md` Q6 — a privilege-escalation feature carries a mandatory hazard REQ.\n...\n 293|### 5.12 Native-PTY spawn of a bare program runs the wrong (non-PE) file on Windows  `[REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]`\n*294|- **Failure:** `portable-pty`'s ConPTY spawn resolves a bare program name with a `which` that takes the FIRST `PATH` match. A node/npm CLI installs as BOTH an extensionless shebang shim (`ccs`, for Git Bash) and a Windows launcher (`ccs.cmd`) in the same dir; portable-pty picks the extensionless `ccs`, and `CreateProcessW` then tries to execute that non-PE file and fails with **os error 193** (\"%1 is not a valid Win32 application\"). Live failure: `spt endpoint run claude-spt:ccs` → `CreateProcessW C:\\n...\n\n## adr/\n### 0001-clean-room-fork-from-claude-skill-owl.md\n 8|\n*9|spt-core is the rebuild of `claude_skill_owl` (today's modern SPT) into a harness-independent core. The sister project works but is fragile: each feature release introduces 2–5 regressions, often in unprecedented ways, because the substrate grew rapidly from a much narrower starting point and carries the resulting tech debt. spt-core also commits day-one to several architectural shapes (endpoint type system, networking, terminal wrapper as backbone, untethering from Claude Code) that change foundationa...\n 10|\n 11|## Decision\n 12|\n### 0002-bake-networking-into-core-collapse-spt-node.md\n 8|\n*9|This ADR retired GitHub as the *required/default* cross-node sync. It is now re-admitted as an **opt-in alternative transport for the context repo only** (`tracked/`), enabled by `spt context-github-setup`:\n 10|\n 11|- **Default stays P2P** + no-account — the core promise is unchanged.\n*12|- **Hub mode (opt-in):** every node pull→merge→pushes against a private GitHub remote (always-online sync hub + live GUI view), authed by a **shared deploy key** distributed as subnet secret material (only the setup node needs a GitHub account). **Prefer-GitHub, P2P backstop.**\n 13|- **Scope:** hub mode carries the **context repo only**. Messaging, registry, remote-drive, pairing, and presence always ride P2P/Iroh; gh-on never removes the P2P requirement. So the original decision (networking baked-in, first-class, P2P) stands in full — the hub is a context-sync convenience layered on top, not a return to GitHub-as-infrastructure.\n 14|\n 15|## Context\n 16|\n*17|The sister project (`claude_skill_owl`) decided networking should live in a separate `spt-node` daemon, explicitly to decouple the network stack from SPT's Claude-Code-tied core binary, expose SPT-protocol-as-product, and let networking evolve on its own release cycle. That decision predates spt-core.\n 18|\n 19|spt-core is a ground-up, harness-independent rebuild. The primary reason `spt-node` was separate — decoupling networking from a CC-tied binary — no longer exists, because spt-core is already decoupled from any harness. Meanwhile the product goal for networking sharpened: WAN cross-machine messaging should be **first-class and zero-config — \"just works.\"**\n 20|\n...\n 22|\n*23|Bake the WAN transport stack (Iroh + mDNS LAN discovery + pairing) into spt-core's library crates, behind a compile-time `net` feature flag (default-on in the reference binary, optional for embedded library consumers).\n 24|\n 25|\"Just works WAN, reachable even when no session is running\" requires a persistent per-machine network presence — the exact job `spt-node` was created to do. spt-core already commits to a single always-on, one-per-machine `spt-daemon` to supervise PTYs (the terminal wrapper). **Collapse the network-host role into that same daemon (option B1).** One process owns both PTYs and the node identity + WAN endpoint.\n 26|\n...\n 45|\n*46|- **SERIOUS #6 — one collapsed daemon = one failure domain.** A networking bug can take out terminal hosting; a PTY-broker bug can take out WAN reachability. Accepted (the collapse is locked), but v1 owes a **fault-injection acceptance matrix**: crash/hang Iroh, mDNS, registry sync, PTY broker, and manifest invocation independently; measure blast radius + recovery. Track at M4. → Begun M4-D4: **`docs/FAULT-MATRIX.md`** is the living matrix (proven rows with test evidence; planned rows tracked as debt).\n 47|- **Interaction with ADR-0004 (FATAL #4):** preserving live WAN streams across a brain restart forces Iroh endpoint / QUIC stream state into the \"stable broker,\" enlarging the kernel ADR-0002 wanted minimal. Resolved in ADR-0004's ownership table — \"stable\" now means *update cadence*, not *narrowness*.\n### 0003-multi-instance-node-anchored-identity-model.md\n 8|\n*9|The original decision below framed \"operate a LiveAgent on any node from any other machine\" primarily as **remote PTY drive**. That is backwards-weighted. Corrected model:\n 10|\n 11|- **Primary — instance:** the *same* endpoint (same harness+adapter) running **natively** on a machine (local files, local compute), with the **Psyche mind synced** across instances. Files/session-history are per-node and cannot teleport; the mind follows. This is the core meaning of \"same agent on multiple machines.\"\n 12|- **Instance dormancy:** typically one instance is actively-driven; others are **dormant** (live, state-preserved, re-activatable in place). Switching machines makes the previously-driven instance dormant — no explicit stop. Registry status is active / dormant / offline.\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n 10|\n*11|1. **Consolidation.** The sister project runs poll listeners as ephemeral per-session background tasks and Psyche wrappers as detached per-live-agent supervisor processes. But poll listeners already interact directly with the agent session (capsule/idle), and Psyche wrappers already invoke harness binaries directly. Once the daemon owns every PTY, keeping these as separate processes is unjustified.\n 12|\n 13|2. **Seamless self-update with a hard no-terminate constraint.** Self-update is a day-one pillar. The constraint: *no endpoint process may terminate or suspend during an spt-core update* — we cannot assume every endpoint can safely suspend. The naive \"drain + restart the daemon\" approach violates this for spt-hosted sessions (the daemon owns their PTY; killing the daemon SIGHUPs the child).\n 14|\n...\n 26|\n*27|**Self-update delivery:** peer-propagated over P2P, layered on self-fetch, out-of-band still supported. All binaries signature-verified before handoff (spt-core's own release key) regardless of source — peer-propagation otherwise lets one compromised node poison the subnet. spt-core conducts updates for the whole stack: self first, then ripple-update each registered adapter via the adapter manifest's update declaration (file-pull or delegated command). The plugin's role shrinks to initial bootstrap only.\n 28|\n 29|**Cadence/consent:** not fully automatic by default; gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto choice.\n 30|\n...\n 33|- The daemon is the single brain for a machine; crash-recovery and update logic must cover PTYs, networking, registry, spools, listeners, and psyche loops together.\n*34|- A small internal broker process exists beneath the daemon — a deliberate, bounded walk-back of \"literally one process,\" preserving B1's *intent* (one network identity, one supervisor, one firewall prompt) while guaranteeing endpoint survival across updates.\n 35|- Peer-propagated updates make release signing mandatory, not optional.\n 36|- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.\n 37|- The \"deliver to most-recently-active session\" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.\n...\n 57|| Accepted local client socket (`api listen`, relay) | **broker** | ✅ #1 (plain TCP) |\n*58|| Iroh endpoint + accepted QUIC streams | **broker** | ✅ #3 (loopback shape) → **implemented M4-D4a** (`spt-daemon::nethost::NetHost`: dedicated tokio runtime in the broker owns the endpoint + conn table; brain drives status/dial over IPC net frames) |\n 59|| mDNS socket / relay session | **broker** | ✅ closed by construction at M4-D4a (2026-06-03): `MdnsAddressLookup` and the relay session are constructed *inside* the broker-owned iroh endpoint — there is no separate socket to own |\n 60|| Listening sockets | **broker** | ✅ #1 |\n 61|| Routing, registry state, pulse/psyche loops, manifest parse, update orchestration | brain (rehydrate from disk) | n/a |\n...\n 68|### D. Self-update delivery hardening (resolves SERIOUS #5)\n*69|Signature-verify alone is insufficient. v1 adds: monotonic version field (rollback rejection), release-metadata expiry, channel pinning, key rotation/revocation list, and **adapter content signing** (adapter file-pull / delegated-command updates were uncovered by \"binary signature\"). Track as `REQ-HAZARD-UPDATE-ROLLBACK`.\n 70|\n 71|### E. Open spike gaps (must close before M3 builds the daemon)\n*72|1. ✅ **CLOSED (Spike #3, 2026-06-01).** Live Iroh + file-transfer stream survival across brain restart (FATAL #2). Spike #1 only proved PTY + plain TCP; Spike #3 stood up a broker-owned Iroh endpoint + uni QUIC transfer and proved a live peer download survives a brain restart gapless + exactly-once. Loopback shape validated; off-node transport + the QUIC-ownership *implementation* defer to M4. (`docs/spikes/SPIKE-03-quic-survival.md`.)\n 73|2. ✅ **CLOSED (Spike #4, 2026-06-01).** Linux `forkpty` parity. Spike #1's binary, unchanged, passed all four invariants on `gravity-linux` (Ubuntu 22.04) — `portable-pty` selects `forkpty` on Unix; the ConPTY-DSR branch is inert there. Invariant B passed with the *strict* contiguity check that ConPTY-under-resize fails (Spike #5): `forkpty` is a raw pipe, ConPTY a screen buffer — different stream contracts, promoted into M3a's `SessionSurface` design. (`docs/spikes/SPIKE-04-forkpty-parity.md`.)\n*74|3. ✅ **CLOSED (Spike #5, 2026-06-01).** 100× restart + resize-under-load stress (codex #3). The broker survived 100 rapid brain kill/restart cycles + a resize thread racing the reader with no leak (one child, clean reap), no hang (watchdog deadline), no lost byte (full value-set coverage in the client's observation window). Surfaced two binding ConPTY findings for M3a/REQ-TERM-3: resize triggers a repaint that reorders+dups the stream (terminal-stream contract ≠ exactly-once transfer contract); mid-...\n 75|4. ✅ **CLOSED (Spike #6, 2026-06-01).** Idempotent/exactly-once delivery across brain restart (FATAL-adjacent #14). Durable-ID + WAL + dedup-at-effect proven exactly-once across a crash at any protocol point (before-intent / before-effect / after-effect). Two binding design constraints surfaced for `REQ-HAZARD-RESTART-IDEMPOTENT` / M3b-B5: broker-owned recovery anchor; dedup-at-effect keyed by durable ID. (`docs/spikes/SPIKE-06-idempotent-boundary.md`.)\n 76|\n 77|**Gate status (2026-06-01): ALL FOUR §E GAPS CLOSED PASS** (#3, #4, #5, #6) → the M3-PLAN Phase-0 spike-gate is **complete**. The broker/brain split is validated on both OSes (ConPTY + `forkpty`), across QUIC-stream survival, restart+resize churn, and exactly-once idempotency. M3a and M3b are both unblocked; the QUIC-ownership *implementation* and off-node transport remain explicitly deferred to M4 (only the *shape* was spiked).\n### 0005-totp-seeded-spake2-node-pairing.md\n 43|- **FATAL #10 — trust-store delete is NOT revocation.** The \"revocation = trust-store delete; TOTP gates joining only\" consequence above is false in the threat model: the seed is replicated to every trusted node, so a compromised node/seed can re-pair as a *new* node after deletion. **Required:** a seed **epoch + rotation** protocol — removing a compromised node rotates the subnet seed so the old node/old seed cannot rejoin. Pairing transcript must bind the seed epoch.\n*44|- **SERIOUS #11 — SPAKE2's one-online-guess bound needs subnet-global rate limiting.** A public pre-trust relay + multiple seed-holding nodes enables *distributed* guessing, and a ±1 TOTP acceptance window triples the valid-password space. **Required:** one active pairing ceremony per subnet, shared attempt counter, exponential backoff; justify or drop the ±1 window.\n 45|- **SERIOUS #12 — \"PAKE binds pubkeys\" only if the transcript does.** Must explicitly bind roles, both node pubkeys, subnet ID, TOTP time-step, and confirmation MACs, or unknown-key-share / reflection / wrong-subnet pairing remain. **Required:** full pairing-transcript spec + negative tests (MITM, replay, reflection, wrong subnet, stale time-step).\n 46|\n 47|## Multi-subnet amendment (2026-06-01 — see ADR-0006)\n...\n 53|- **Rendezvous-token hashing.** The relay routes the pre-trust ceremony by `H(subnet-name ‖ TOTP-epoch)`, not the plaintext label — the user/agent enters the raw name + code; the hash is internal. (Payload is already E2E-encrypted; this protects the relay-visible routing token from leaking the human subnet label.) Composes with the #12 transcript binding (the subnet ID / seed epoch already in the transcript).\n*54|- **Per-subnet code fetch is gated behind OS elevation** (Windows UAC / Linux root-or-equivalent), or an elevated agent endpoint; else the user falls back to their authenticator app. Replaces the old unqualified \"fetchable from any trusted node\" with an elevation-gated, *per-subnet* fetch (a node now holds several subnets' seeds).\n 55|- **Code fetch is per-subnet** (a node holds *N* seeds): `spt pair show-totp` prompts which subnet (or `--subnet <name>` / `--create-new` to bypass), offered optionally as a QR / `otpauth://` URI.\n 56|- **Forward (cross-user, ADR-0006 seam):** the seed key becomes per-(subnet, user); the prompt becomes 2-stage. The #10 seed epoch+rotation and #11 subnet-global rate-limit requirements apply **per subnet**.\n 57|\n...\n 73|  shed at the accept door, and a ceremony timeout aligned with the limiter's\n*74|  stale-slot reclaim — an attacker reaching the listener still gets one\n*75|  online guess per admitted ceremony, serialized and backed off.\n 76|- **Membership mutations are elevation-gated** (the REQ-PAIR-6 machinery,\n 77|  exit 3, gate-before-everything): `subnet create` reveals a fresh subnet's\n 78|  joining secret, and `subnet join` enrolls the whole machine into a subnet's\n*79|  trust fabric — an unprivileged process must do neither (an attacker-run\n*80|  joiner would enroll the box into the attacker's subnet). `subnet status`\n 81|  stays ungated: read-only, membership shape only, never secrets.\n 82|\n 83|Surface rename, same semantics: `spt pair show-totp [--subnet|--create-new]`\n### 0006-multi-subnet-membership-model.md\n 18|\n*19|1. **Node-global identity, per-subnet visibility.** One `ling` exists on a node (one mind, one `tracked/` context); the node advertises it into each subnet it belongs to, subject to visibility (below). The subnet registry is **per-subnet**; the same endpoint can appear in several registries. Bare-id uniqueness holds *within* a subnet via a **join-time collision check**.\n 20|\n 21|2. **Qualified addressing + rippled rename.** Two combinable qualifiers — subnet-qualified `home:ling` and node-qualified `ling@hfenduleam`. When a bare id is ambiguous across visible subnets (two distinct endpoints sharing a name), resolution **refuses and forces qualification** rather than guessing. A new **`spt rename <id> <new_id>`** changes an endpoint's logical ID and ripples to all instances (registry, every perch, the `a-<id>` context branch), collision-checked against every target subnet and r...\n 22|\n...\n 24|\n*25|4. **Per-subnet code fetch gated behind OS elevation.** Retrieving a subnet's current code *from a node* requires Windows UAC / Linux root-or-equivalent, or an elevated agent endpoint; otherwise the user falls back to their own authenticator app. Narrows the multi-subnet exposure (mere CLI/agent presence no longer yields any subnet's join-code).\n 26|\n 27|5. **Subnet-exclusive sync.** An endpoint's mind replicates only within its home subnet by default. Endpoint config carries a **subnet-membership list** for sync; the mind replicates across every listed subnet's nodes. Distinct from visibility: being addressable in a subnet does not imply replicating the mind there.\n 28|\n### 0007-subnet-notification-primitive.md\n 15|- **Per-subnet, replicated spool.** Notifs are subnet events; the spool replicates across that subnet's nodes (reusing subnet-registry distribution, ADR-0006). Each notif is subnet-tagged; **dismiss-state replicates subnet-wide**, eventually consistent.\n*16|- **Two states:** *seen* (surfaced at least once, tracked per-endpoint) vs *dismissed* (explicit ack — `spt notif dismiss`, or the agent marks it). Surfacing ≠ dismissal.\n 17|- **First-fire** → the user's most-recently-active endpoint in that subnet (presence resolution). **Resurface** undismissed notifs at existing reported boundaries — state→active (`wake`), `api boundary clear`, `api boundary compact`, new-session-start — gated by *seen-per-endpoint* plus a **cross-endpoint suppression timeout** (default ~1h) so a notif can't bounce between endpoints. Scoped to subnets the endpoint is **visible in** (ADR-0006).\n 18|- **Delivery reuses messaging.** A notif to an agent is a notif-flavored envelope delivered to the agent's perch via the existing path; the agent surfaces it. An optional **`notif_command` manifest template** (on **both** harness-adapter and shell-adapter manifests) renders it endpoint-native (OS toast, GameRobot `alert-symbol`), combinable with agent-surface. If presence resolves to a **Shell** attached to the endpoint, the notif renders via *that shell's* template — the v1 seam and the M5 generalizat...\n 19|- **`spt notify`** lets any agent issue a subnet-wide notif (v1: reaches the user on their active endpoint). The **envelope `from`** carries the issuer's endpoint id + node + subnet (subnet surfaced only to multi-subnet receivers).\n### 0008-live-activity-buffer-pty-digest.md\n 12|\n*13|Add a **live activity buffer (PTY digest)** as a **distinct artifact** — not scrollback (raw bytes, R-TERM-2), not transcript history (full conversation behind communes/resume, Paths A/B). Its purpose is at-a-glance current activity, not replay.\n 14|\n 15|The parsing-boundary distinction that resolves the tension:\n 16|- **The adapter supplies the patterns; spt-core only runs them.** The manifest provides `input_pattern` + `agent_pattern` (each opens a span capturing everything, newlines included, until the next pattern match of any kind) and `tool_pattern` (a **list** — one per tool, plus a final **catchall** for user-installed tools, capturing tool-name + arg fragment). This is exactly Path A's \"adapter owns the parser\" rule and the \"spt-core owns the mechanism, adapter owns the pattern\" principle (identical to opa...\n...\n 35|\n*36|**Why (the Gateway grill, 2026-06-12):** the PTY byte stream of an alternate-screen TUI is a **rendering protocol, not a content log**. Scroll and resize trigger full repaints — the same content re-enters the stream and corrupts delta computation — and semantic turn boundaries drown in repaint traffic. The original §Decision rationale (\"a light rolling regex over bytes ≠ fragile transcript parsing\") proved **backwards in practice**: the byte stream is the *more* fragile surface, and transcript par...\n 37|\n 38|**What stands (the product surface, unchanged):** the formula (N-turn window, collapsed tool sprints, presentation-vs-extraction split), structured source-tagged output, both access modes (snapshot pull + structured-delta stream, deltas now keyed to log records), address-gating, Option-C persistence. The no-built-in-parser rule **still holds** — normalization remains adapter-owned (Path A) or adapter-pushed (Path B).\n 39|\n### 0009-endpoint-access-control-model.md\n 71|- The routing/messaging path (M4) gains a whitelist check on inbound: classify reply-vs-new (via `__REPLY_TO__` correlation) before applying the origin-node gate.\n*72|- The registry projection that backs resource advertisement must apply the visibility (and later whitelist) filter per viewer.\n 73|- **Forward (cross-user, ADR-0006 seam):** the whitelist's `users` tier and the \"nodes belonging to whitelisted users\" semantics activate with the per-(subnet, user) model.\n 74|- Builds on ADR-0006 (visibility, multi-subnet) and the existing consent grant store (CONTEXT §Consent & security gates).\n### 0011-branchstore-git-kv-state-substrate.md\n 30|filesystem — as the substrate for **coarse, durable, audited** state. It lives at\n*31|the bottom of the stack (`spt-store`) so context sync, the registry, and daemon\n 32|checkpointing share one abstraction rather than each re-inventing persistence.\n 33|\n 34|**BranchStore is for:**\n...\n 48|- **Merge-native sync** — reuse the context precedence merge driver + `--allow-unrelated-histories` for any BranchStore replicated cross-node.\n*49|- **GUI consequence** — state browsing becomes *git rendering*: raw inspection is free via standard git tooling; the product GUI (R-FRONT-1 panes) is a memformat-aware renderer over git, inheriting history / diff / time-travel rather than building a versioning backend.\n 50|\n 51|## Consequences\n 52|\n### 0012-psyche-trust-boundary-stdout-event-outbound.md\n 77|  be a **bounded, stdin-fed, stdout-captured** invocation (mirroring the sister's\n*78|  per-turn `--resume`), like the echo-commune's `run_bounded_stdin`. Tracked as\n 79|  KNOWN-HAZARDS 7.3 / `REQ-HAZARD-PSYCHE-OUTBOUND-PROXY`.\n 80|- **A marker parser must exist.** spt-core has no `<EVENT type=\"reply|notify\">`\n 81|  parse-and-relay path yet (the sister's `parse_markers`, `claude.rs:594`). It is\n### 0013-psyche-reconciled-context-merge.md\n 31|histories** — rejected outright; the context DAG must be shared. Third fork:\n*32|BranchStore backing (git CLI vs gitoxide).\n 33|\n 34|## Decision\n 35|\n...\n 40|- **Holding state is durable and replicated.** On detecting a concurrent pair,\n*41|  both versions persist as **tracked conflict artifacts** (deterministic\n 42|  content-hash names, in-repo, so conflict state itself syncs). The local\n 43|  working file stays untouched — the active mind is never clobbered by an\n 44|  unresolved conflict. An event seam fires (a D8 notif producer later).\n...\n 46|  active-instance-authoritative rule — the Psyche lives where the mind is being\n*47|  used). No active instance anywhere → deterministic fallback: lowest node id\n 48|  among conflict holders. This kills reconcile storms (two nodes merging the\n 49|  same pair into textually-different results, conflicting again).\n 50|- **The merged write carries `join(vA, vB)` + a bump of the reconciler's own\n### 0014-publish-target-repo-and-lapse-proof-canonical-url.md\n 7|\n*8|**Canonical URL = `https://sabermage.github.io/spt-releases`, with no custom domain configured on Pages.** This URL is what gets baked everywhere: docs cross-links, `llms.txt`, the install one-liner in the README, and every adapter's pack-in install logic.\n 9|\n 10|## Considered options\n 11|\n### 0015-release-signing-two-key-trust-anchor.md\n 2|\n*3|Every shipped binary embeds the release **trusted-key set** that `spt update` verifies against (R-UPD-2), so the keys chosen at v0.1 anchor the self-update trust chain for every deployed node. We mint **two Ed25519 keypairs**: a **primary** (active — signs every release, lives on the maintainer's dev machine) and a **recovery** (generated once, never used, never stored on the dev machine). **Both public keys ship in the binary's trusted set.** Both private seeds (32 bytes each) are backed up independen...\n 4|\n 5|Loss of the primary (e.g. dev machine destroyed) is then a non-event: the next release is signed with the recovery key and rotates in a fresh primary (the verify path already supports key revocation). Without the second key, losing the sole key would kill the self-update chain — nodes keep running but every future release fails verification until users manually re-run the installer.\n 6|\n...\n 9|- **Signing key as a CI secret (automated signing)** — rejected for now. The runners are self-hosted dev boxes running arbitrary dev workloads; a release key in their secret store is weaker custody than a manual local sign, and release cadence doesn't justify automation. Revisit if cadence demands it.\n*10|- **Single key + good backups** — rejected; rotation-after-loss requires a *second already-trusted* key, otherwise recovery means out-of-band reinstall for every user.\n 11|\n 12|## Consequences\n 13|\n### 0016-platform-targeted-update-sets.md\n 16|\n*17|Platform keys in the signed metadata use Rust target triples, for example `x86_64-pc-windows-msvc` and `x86_64-unknown-linux-gnu`. Release asset names remain a packaging concern and may keep their public-friendly names; target triples are the protocol identifiers.\n 18|\n 19|This keeps debug rollout fast without turning it into raw peer file-copy: a fast coordinator can assemble and sign every platform artifact, then let the existing subnet update machinery distribute the right bytes to each node. The generic receiver/protocol support belongs in the production daemon because public releases and debug rollouts have the same cross-platform distribution problem. The rollout driver does not: build recipes, cross-build/WSL/runner coordination, dev-key signing, and \"stage this deb...\n 20|\n*21|Release monotonicity is scoped to the pinned channel. Stable releases and debug rollouts use separate counters, so a fast debug sequence cannot consume, block, or roll back the stable release counter; rollback protection compares only against the latest accepted version for the recipient's pinned channel.\n 22|\n 23|Debug rollout targets are opt-in by channel pin, not by subnet membership alone. A debug offer may traverse trusted subnet peers, but a stable-pinned node rejects it at the metadata gate; only nodes pinned to the debug channel and provisioned with the dev signing key can stage and apply it.\n 24|\n*25|Pinning a lab node to debug uses the existing release-key overlay: `$SPT_HOME/identity/release-keys.json` adds the dev public key and sets `\"channel\": \"debug\"` (for example `\"keys\": {\"dev-debug-2026\": \"<ed25519-public-key-hex>\"}`). Removing the key or pinning the channel back to `\"stable\"` takes the node out of debug rollout eligibility.\n 26|\n*27|Channel pinning is exclusive. A debug-pinned node accepts only debug-channel offers and rejects stable offers until re-pinned to stable; a stable-pinned node accepts only stable-channel offers. This keeps rollback accounting and test provenance unambiguous.\n 28|\n 29|Maintainer tooling may provide a helper to write that file correctly, but the canonical mechanism remains the generic release-key overlay. `spt` itself does not need a special debug-mode command; source-repo tooling can offer convenience such as `xtask debug-pin` for local lab setup.\n 30|\n*31|Bad debug builds recover the same way as stable releases: publish a newer signed debug rollout, even if the artifact bytes are a previous known-good binary. Numeric rollback remains rejected; the debug channel never gets a bypass around the anti-rollback invariant.\n 32|\n 33|Debug signing key custody is deliberately weaker than stable release signing. The stable release seed stays manual/password-manager driven per ADR-0015; the debug seed may live in a long-lived `SPT_DEBUG_RELEASE_SEED` environment variable on the fast coordinator so agents can drive rollouts. This is acceptable only because the key's blast radius is limited to nodes explicitly pinned to `debug` and provisioned with that dev public key; rotation is minting a new debug key, updating lab overlays, and deleti...\n 34|\n...\n 50|\n*51|The first implementation slice is intentionally narrow: update-set metadata and verification, daemon staging/read path with local platform selection, maintainer `xtask` helpers for debug pinning and debug rollout, and loopback/two-host proof of propagation plus convergence reporting. Remote build orchestration, rich dashboards, adapter inclusion, and stable-release publishing migration are later work.\n 52|\n 53|## Considered Options\n 54|\n 55|- **Single artifact per staged update** — simpler metadata, but forces dev tooling to stage different binaries through platform-specific side paths and makes debug rollout brittle.\n*56|- **Debug-only remote copy/install scripts** — fastest to hack, but bypasses the verification, rollback, channel, and consent gates that peer propagation exists to protect.\n 57|- **Platform-targeted update set** — chosen: one generic substrate shape, recipients fetch only their matching artifact, and both stable releases and debug rollouts reuse the same trust path.\n### 0018-broker-brain-process-isolation-restoration.md\n 10|\n*11|On 2026-06-09, while verifying the v0.3.2 cross-OS update fix on the live fleet, a regression was discovered: **the production daemon runs the broker as a background thread inside the single `spt daemon` process** (`daemon.rs:165-170`: `Broker::bind_in_with_net` → `Arc<Broker>` served on `thread::spawn`; no broker child-spawn exists anywhere in the repo). This is **unintended drift** — no doc, plan, commit, or comment marks the collapse as a deliberate interim (the operator confirmed no such decision...\n 12|\n 13|Consequences of the drift:\n 14|- The brain cannot restart onto a new binary without killing the in-process broker thread, which would close every PTY, orphan every harness child, and drop every listening socket. **The no-endpoint-drop self-update pillar is therefore silently unrealized.**\n...\n 23|1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.\n*24|2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** \"the executable path\" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-sp...\n 25|3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`...\n 26|4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update...\n 27|5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).\n*28|6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable ...\n 29|7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.\n 30|8. **Cross-platform uniformity (Q8).** The broker spawns the brain as a child process (`Command::spawn`) + socket IPC — no `exec`, so no Windows/Unix divergence. ConPTY/forkpty handles never leave the broker.\n 31|\n...\n 34|- **Decomposition:** `spt daemon run` becomes the broker process entry (binds seed-control + broker socket + NetHost + digest hub; holds children + `EffectJournal`; spawns the brain); a new hidden `spt daemon brain` is the brain entry (connects, runs the logic loops, rehydrates from disk, emits `ready`). `ensure_running`/`is_running`/`daemon stop` contracts unchanged. The de-elevation guard applies at the broker entry.","truncated":true,"truncatedBy":"bytes","totalLines":399,"totalBytes":58667,"outputLines":333,"outputBytes":50909,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":399,"totalBytes":58667,"outputLines":333,"outputBytes":50909,"shownRange":{"start":1,"end":333},"nextOffset":334}}},"isError":false,"timestamp":1789528530831}}
{"type":"message","id":"aaa867fb","parentId":"3687bed4","timestamp":"2026-09-16T03:15:30.847Z","message":{"role":"toolResult","toolCallId":"call_Bx3JMhUCca09pmASNp7G2PcE|fc_0cf46cef591a107c016aaa09d2630c87d28b8a3385299c1562","toolName":"grep","content":[{"type":"text","text":"# .worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/\n## input_ack_deadlock.rs\n*1|//! v0.13.0 P1b (REQ-HAZARD-INPUT-ACK-BACKPRESSURE) — the KEYSTONE, repro-first.\n 2|//!\n 3|//! ## The bug this gate locks down (the applied-ack full-duplex deadlock)\n 4|//!\n...\n 101|const POST_FLOOD_OUTPUT: &[u8] = b\"ACKDL_LIVE_OUTPUT\";\n*102|fn unique_name() -> String {\n 103|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 104|    format!(\"spt-daemon-ackdl-{}-{}.sock\", std::process::id(), n)\n 105|}\n 106|\n*107|fn kill_pid(pid: u32) {\n 108|    #[cfg(windows)]\n 109|    let _ = std::process::Command::new(\"taskkill\")\n 110|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n...\n 121|/// as live fan-out, never in the cold-attach repaint.\n*122|fn seeded_quiet_spawn_req(endpoint: &str, live_trigger: &std::path::Path) -> SpawnReq {\n 123|    let trigger = live_trigger.display();\n 124|    #[cfg(unix)]\n 125|    let (program, args) = (\n...\n 160|// driven_by marker — that resolution must never touch a real home).\n*161|fn init_home() -> TestHome {\n 162|    TestHome::new()\n 163|}\n 164|\n*165|fn hermetic() -> NetConfig {\n 166|    NetConfig {\n 167|        identity: Identity::generate(),\n 168|        relay: RelayPolicy::Disabled,\n...\n 174|\n*175|fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n 176|    let host = NetHost::start(hermetic()).expect(\"net host start\");\n 177|    let broker =\n 178|        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n...\n 185|\n*186|fn connect_retry(name: &str) -> Brain {\n 187|    for _ in 0..300 {\n 188|        if let Ok(b) = Brain::cold_start(name, 1) {\n 189|            return b;\n...\n 197|/// target serves) — return (stream_id, origin). Watchdog'd by the poll count.\n*198|fn wait_for_stream(brain: &mut Brain) -> Option<(u64, String)> {\n 199|    for _ in 0..400 {\n 200|        let reply = brain.net_streams().expect(\"net-streams\");\n 201|        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {\n...\n 211|/// return the flood controller's older row instead of the later VIEWER stream.\n*212|fn wait_for_latest_stream(brain: &mut Brain) -> Option<(u64, String)> {\n 213|    for _ in 0..400 {\n 214|        let reply = brain.net_streams().expect(\"net-streams\");\n 215|        if let Some(s) = reply\n...\n 234|/// time out; a serviceable broker answers.\n*235|fn probe(name: &str, cmd_kind: &'static str, want_kind: &'static str, deadline: Duration) -> bool {\n 236|    let name = name.to_string();\n 237|    let (tx, rx) = std::sync::mpsc::channel::<bool>();\n 238|    thread::spawn(move || {\n...\n 267|\n*268|/// REQ-HAZARD-INPUT-ACK-BACKPRESSURE — the keystone gate. A FLOOD of N=64 operator\n 269|/// `Input` records is driven through the REAL `serve_attach` CONTROLLER path on ONE\n 270|/// conn (the rc → net-stream → serve_attach → send_effect_no_ack route), exceeding\n 271|/// the ~10-frame IPC pipe buffer. Post-fix the broker emits NO applied frame for\n...\n 278|/// watchdog so a regression surfaces as a clean FAILURE, never a hang.\n*279|// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n*280|#[test]\n*281|fn input_flood_through_serve_attach_does_not_deadlock_broker() {\n 282|    let _home = init_home();\n 283|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 284|    let name = unique_name();\n...\n 673|         the immediate write error and operation; HelperDisconnected means the flood \\\n*674|         helper exited without reporting (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\"\n 675|    );\n 676|    // (2) The broker PROCESS stays alive on a fresh conn during/after the flood.\n 677|    assert!(\n...\n 692|         the broker's session table / dispatch must not be globally frozen \\\n*693|         (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\"\n 694|    );\n 695|    // (4) The post-flood VIEWER is serviced, receives a non-empty decodable cold\n 696|    //     repaint, then receives ACKDL_LIVE_OUTPUT as live fan-out after its\n## redispatch.rs\n 44|/// Acquire a scoped temp home for one complete test body.\n*45|fn init_home() -> TestHome {\n 46|    TestHome::new()\n 47|}\n 48|\n 49|static SEQ: AtomicU32 = AtomicU32::new(0);\n*50|fn unique_name() -> String {\n 51|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 52|    format!(\"spt-daemon-rdsp-{}-{}.sock\", std::process::id(), n)\n 53|}\n 54|\n*55|fn hermetic() -> NetConfig {\n 56|    NetConfig {\n 57|        identity: Identity::generate(),\n 58|        relay: RelayPolicy::Disabled,\n...\n 64|\n*65|fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n 66|    let host = NetHost::start(hermetic()).expect(\"net host start\");\n 67|    let broker =\n 68|        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n...\n 75|\n*76|fn connect_retry(name: &str) -> Brain {\n 77|    for _ in 0..300 {\n 78|        if let Ok(b) = Brain::cold_start(name, 1) {\n 79|            return b;\n...\n 85|\n*86|fn echo_spawn_req(endpoint: &str) -> SpawnReq {\n 87|    #[cfg(unix)]\n 88|    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n 89|    #[cfg(windows)]\n...\n 138|/// The A-side (target broker) peer-stream ids, via a probe brain.\n*139|fn peer_stream_ids(probe: &mut Brain) -> Vec<u64> {\n 140|    probe\n 141|        .net_streams()\n 142|        .expect(\"net-streams\")\n...\n 155|/// steal-clears-controller symptom D1 must NOT see).\n*156|fn render_until(\n 157|    operator: &mut Brain,\n 158|    stream_id: u64,\n 159|    decoder: &mut AttachDecoder,\n...\n 202|\n*203|fn screen_of(viewport: &[u8]) -> String {\n 204|    let mut screen = ScreenGrid::new(24, 80);\n 205|    screen.advance(viewport);\n 206|    String::from_utf8_lossy(&screen.render_repaint()).into_owned()\n...\n 208|\n*209|// [int->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] D1 — the field bug, end to end\n 210|// through PRODUCTION rediscovery: a finished historical Attach and a current\n 211|// active Attach on the same endpoint/origin; only the dispatcher generation\n 212|// restarts. The historical stream must neither take nor clear the current\n...\n 216|// and the retired row is GONE from the broker's enumeration\n*217|// (REQ-REDISPATCH-FINISHED-RETIRE observed at the seam that matters).\n*218|#[test]\n*219|fn finished_historical_attach_neither_takes_nor_clears_current_controller() {\n 220|    let _home = init_home();\n 221|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 222|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 350|\n*351|// [int->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] D1b (gate round 1) — the\n 352|// finished-UNRETIRED orderings the Served-report path can never see: the\n 353|// pre-existing backlog and the brain-down-detach window. s1 is attached and\n 354|// driven under gen1, gen1 is KILLED mid-serve (no Served report), THEN the\n...\n 363|// ever being served (retire-on-sight drains the backlog).\n*364|// [int->REQ-REDISPATCH-FINISHED-RETIRE]\n*365|#[test]\n*366|fn finished_unretired_attach_is_skipped_and_drained_not_replayed() {\n 367|    let _home = init_home();\n 368|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 369|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 493|\n*494|// [int->REQ-REDISPATCH-FINISHED-RETIRE] the GUARD leg for the family-aware\n 495|// discriminator (gate round 1 counter-check): a request/reply stream reads\n 496|// `finished=true` the moment its requester FINs with the request — sync's\n 497|// `request_sync` does exactly that — so a finished row must NOT be blanket-\n...\n 502|// brain cycle).\n*503|#[test]\n*504|fn finished_pending_sync_request_still_serves_across_generation_start() {\n 505|    let _home = init_home();\n 506|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 507|    let (name_a, name_b) = (unique_name(), unique_name());\n## redispatch_stall.rs\n 59|\n*60|fn init_home() -> TestHome {\n 61|    TestHome::new()\n 62|}\n 63|\n 64|static SEQ: AtomicU32 = AtomicU32::new(0);\n*65|fn unique_name() -> String {\n 66|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 67|    format!(\"spt-daemon-rdstall-{}-{}.sock\", std::process::id(), n)\n 68|}\n 69|\n*70|fn hermetic() -> NetConfig {\n 71|    NetConfig {\n 72|        identity: Identity::generate(),\n 73|        relay: RelayPolicy::Disabled,\n...\n 79|\n*80|fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n 81|    let host = NetHost::start(hermetic()).expect(\"net host start\");\n 82|    let broker =\n 83|        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n...\n 90|\n*91|fn connect_retry(name: &str) -> Brain {\n 92|    for _ in 0..300 {\n 93|        if let Ok(b) = Brain::cold_start(name, 1) {\n 94|            return b;\n...\n 100|\n*101|fn echo_spawn_req(endpoint: &str) -> SpawnReq {\n 102|    #[cfg(unix)]\n 103|    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n 104|    #[cfg(windows)]\n...\n 152|/// The A-side peer-stream ids, via a probe brain.\n*153|fn peer_stream_ids(probe: &mut Brain) -> Vec<u64> {\n 154|    probe\n 155|        .net_streams()\n 156|        .expect(\"net-streams\")\n...\n 165|/// helper, verbatim). Returns whether an EOF for the stream arrived.\n*166|fn render_until(\n 167|    operator: &mut Brain,\n 168|    stream_id: u64,\n 169|    decoder: &mut AttachDecoder,\n...\n 212|\n*213|fn screen_of(viewport: &[u8]) -> String {\n 214|    let mut screen = ScreenGrid::new(24, 80);\n 215|    screen.advance(viewport);\n 216|    String::from_utf8_lossy(&screen.render_repaint()).into_owned()\n...\n 220|/// opener line. Returns (operator brain, B-side stream id, A-side stream id).\n*221|fn open_traffic_stream(\n 222|    _name_a: &str,\n 223|    name_b: &str,\n 224|    probe: &mut Brain,\n...\n 261|/// seq contiguity from 0 (gapless) and returns the bytes.\n*262|fn drain_gapless(name_a: &str, sid_a: u64, want_bytes: usize, deadline: Duration) -> Vec<u8> {\n 263|    let end = Instant::now() + deadline;\n 264|    'attach: loop {\n 265|        assert!(\n...\n 319|\n*320|// [int->REQ-HAZARD-REDISPATCH-STALL]\n*321|// [int->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n 322|// T1 (poisoned-replay halt) + T3 (producer latency flat), one incident on the\n 323|// production broker subscribe path: a REAL subscriber conn that attaches with\n 324|// a full ring and never reads — its seat's replay wedges on the named-pipe\n...\n 330|// subscriber replays everything gapless from the cursor — no abandonment.\n*331|#[test]\n*332|fn wedged_subscriber_never_stalls_the_producer_and_recovery_replays_gapless() {\n 333|    let _home = init_home();\n 334|    // Shrink the bounded write so the poison window is 400ms, not 15s — the\n 335|    // knob grants wall-clock only, never relaxes an assertion (the\n...\n 405|\n*406|// [int->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n 407|// T4 (forwarding cancel): a REAL forwarding leg — ack'd `net_stream_send`\n 408|// ticks on the stream's send half — keeps serving while the subscriber is\n 409|// healthy; when the subscriber seat POISONS (the same wedged-conn incident as\n...\n 413|// fresh send serves again (recovery, the C2-lease seam this wave builds).\n*414|#[test]\n*415|fn poisoned_subscriber_cancels_the_paired_forwarding_leg_then_recovery_serves() {\n 416|    let _home = init_home();\n 417|    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n 418|    let dir = tempfile::tempdir().expect(\"tempdir\");\n...\n 536|\n*537|// [int->REQ-DISPATCH-FALLBACK-CIRCUIT]\n 538|// T2 (fallback narrowing): a stream whose opener never pins (bytes, no\n 539|// newline) is a TRANSIENT to the dispatcher — the claim requeues bounded and\n 540|// goes terminal after exactly MAX_DISPATCH_ATTEMPTS failures, and NO peek\n...\n 544|// each such retry manufactured a fresh replay subscriber on the conn).\n*545|#[test]\n## attach.rs\n*1|//! D5b regression — remote terminal attach (REQ-INST-8): drive a real PTY\n 2|//! session on daemon A from daemon B, byte-stream viewport over the\n 3|//! broker-owned wire; compute stays on A.\n 4|//!\n...\n 13|//!   on the stream table's handshake-proven `remote_id_hex`;\n*14|//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies\n 15|//!   mid-attach; the broker keeps the QUIC stream, the session, the output\n 16|//!   log, and the effect journal. The successor brain re-serves the SAME\n 17|//!   stream from seq 0 — the worst-case stale cursor: the replayed input\n...\n 47|/// the endpoint perch's `driven_by` marker without ever touching a real home.\n*48|fn init_home() -> TestHome {\n 49|    TestHome::new()\n 50|}\n 51|\n 52|static SEQ: AtomicU32 = AtomicU32::new(0);\n*53|fn unique_name() -> String {\n 54|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 55|    format!(\"spt-daemon-d5b-{}-{}.sock\", std::process::id(), n)\n 56|}\n 57|\n*58|fn hermetic() -> NetConfig {\n 59|    NetConfig {\n 60|        identity: Identity::generate(),\n 61|        relay: RelayPolicy::Disabled,\n...\n 67|\n*68|fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n 69|    let host = NetHost::start(hermetic()).expect(\"net host start\");\n 70|    let broker =\n 71|        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n...\n 78|\n*79|fn connect_retry(name: &str) -> Brain {\n 80|    for _ in 0..300 {\n 81|        if let Ok(b) = Brain::cold_start(name, 1) {\n 82|            return b;\n...\n 90|/// actually expire, retried over broker start-up the same way.\n*91|fn connect_retry_pump(name: &str, io_timeout: Duration) -> Brain {\n 92|    for _ in 0..300 {\n 93|        if let Ok(b) =\n 94|            Brain::cold_start_pump(name, 1, io_timeout, spt_daemon::brain::PumpTrace::Stderr)\n...\n 102|\n*103|fn echo_spawn_req() -> SpawnReq {\n 104|    #[cfg(unix)]\n 105|    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n 106|    #[cfg(windows)]\n...\n 138|/// same id twice is ONE stream served twice, never two streams sharing a number.\n*139|fn wait_for_stream(brain: &mut Brain, served: &[u64]) -> (u64, String) {\n 140|    for _ in 0..400 {\n 141|        let reply = brain.net_streams().expect(\"net-streams\");\n 142|        if let Some(s) = reply\n...\n 168|#[allow(clippy::too_many_arguments)]\n*169|fn attach_drive_detach(\n 170|    operator: &mut Brain,\n 171|    target_broker: &str,\n 172|    mut target: Brain,\n...\n 243|/// the rendered-through cursor (last accepted seq + 1).\n*244|fn render_until(\n 245|    brain: &mut Brain,\n 246|    stream_id: u64,\n 247|    decoder: &mut AttachDecoder,\n...\n 286|/// Count non-overlapping occurrences of `needle` in `hay`.\n*287|fn count(hay: &[u8], needle: &[u8]) -> usize {\n 288|    if needle.is_empty() || hay.len() < needle.len() {\n 289|        return 0;\n 290|    }\n...\n 302|\n*303|// [int->REQ-RC-1] loopback self-dial is REFUSED by the transport — pins the\n 304|// finding that drives `spt rc`'s LOCAL transport choice. A node cannot QUIC-dial\n 305|// its own node id (iroh: \"Connecting to ourself is not supported\"), so local\n 306|// attach CANNOT ride a self-dialed QUIC conn. The one-pump invariant therefore\n...\n 309|// Cross-node attach (the prize) is unaffected: it dials a real remote peer.\n*310|#[test]\n*311|fn loopback_self_dial_is_refused_local_uses_fallback_transport() {\n 312|    let _home = init_home();\n 313|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 314|    let name = unique_name();\n...\n 329|\n*330|// [int->REQ-HOST-RUN-1] [int->REQ-RC-1] spt-hosted harness bringup + cross-node\n 331|// attach, end to end: `harnesshost::launch_harness_brokered_in` spawns a harness's\n 332|// `[session.self]` into broker A's PTY (the spt-hosted bringup — the daemon owns\n 333|// the PTY, not an external launcher), then an operator on broker B attaches\n...\n 337|// the viewport (detach never kills it).\n*338|#[test]\n*339|fn spt_hosted_bringup_then_cross_node_attach_drives_the_pty() {\n 340|    let _home = init_home();\n 341|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 342|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 431|\n*432|// [unit->REQ-INST-8] drive an agent('s session) on machine A from machine B:\n 433|// B's keystrokes reach A's real PTY child over the wire (journaled input\n 434|// path), the child's output streams back as sequenced records, and B renders\n 435|// its own marker echoed remotely. The serve gate sees the handshake-proven\n## refresh_freeze.rs\n 16|\n*17|fn write_json(path: &Path, value: &serde_json::Value) {\n 18|    std::fs::write(path, serde_json::to_vec_pretty(value).unwrap()).unwrap();\n 19|}\n 20|\n*21|fn python() -> PathBuf {\n 22|    let program = std::env::var_os(\"SPT_REFRESH_RIG_PYTHON\").unwrap_or_else(|| \"python\".into());\n 23|    let output = Command::new(program)\n 24|        .args([\"-c\", \"import sys; print(sys.executable)\"])\n...\n 35|\n*36|// [int->REQ-DAEMON-REFRESH]\n*37|// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n*38|#[test]\n*39|fn refresh_resumes_bounded_input_ack_without_replacement_poison() {\n 40|    let binary = std::env::var_os(\"SPT_REFRESH_RIG_SPT\")\n 41|        .map(PathBuf::from)\n 42|        .unwrap_or_else(|| {"}],"details":{"scopePath":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs, .worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs, .worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs, .worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs, .worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon\\tests","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":83,"fileCount":5,"files":[".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs"],"fileMatches":[{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs","count":17},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs","count":20},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs","count":20},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs","count":20},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs","count":6}],"truncated":true,"perFileLimitReached":20,"displayContent":"# .worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/\n## input_ack_deadlock.rs\n  *1│//! v0.13.0 P1b (REQ-HAZARD-INPUT-ACK-BACKPRESSURE) — the KEYSTONE, repro-first.\n   2│//!\n   3│//! ## The bug this gate locks down (the applied-ack full-duplex deadlock)\n   4│//!\n    │...\n 101│const POST_FLOOD_OUTPUT: &[u8] = b\"ACKDL_LIVE_OUTPUT\";\n*102│fn unique_name() -> String {\n 103│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 104│    format!(\"spt-daemon-ackdl-{}-{}.sock\", std::process::id(), n)\n 105│}\n 106│\n*107│fn kill_pid(pid: u32) {\n 108│    #[cfg(windows)]\n 109│    let _ = std::process::Command::new(\"taskkill\")\n 110│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n    │...\n 121│/// as live fan-out, never in the cold-attach repaint.\n*122│fn seeded_quiet_spawn_req(endpoint: &str, live_trigger: &std::path::Path) -> SpawnReq {\n 123│    let trigger = live_trigger.display();\n 124│    #[cfg(unix)]\n 125│    let (program, args) = (\n    │...\n 160│// driven_by marker — that resolution must never touch a real home).\n*161│fn init_home() -> TestHome {\n 162│    TestHome::new()\n 163│}\n 164│\n*165│fn hermetic() -> NetConfig {\n 166│    NetConfig {\n 167│        identity: Identity::generate(),\n 168│        relay: RelayPolicy::Disabled,\n    │...\n 174│\n*175│fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n 176│    let host = NetHost::start(hermetic()).expect(\"net host start\");\n 177│    let broker =\n 178│        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n    │...\n 185│\n*186│fn connect_retry(name: &str) -> Brain {\n 187│    for _ in 0..300 {\n 188│        if let Ok(b) = Brain::cold_start(name, 1) {\n 189│            return b;\n    │...\n 197│/// target serves) — return (stream_id, origin). Watchdog'd by the poll count.\n*198│fn wait_for_stream(brain: &mut Brain) -> Option<(u64, String)> {\n 199│    for _ in 0..400 {\n 200│        let reply = brain.net_streams().expect(\"net-streams\");\n 201│        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {\n    │...\n 211│/// return the flood controller's older row instead of the later VIEWER stream.\n*212│fn wait_for_latest_stream(brain: &mut Brain) -> Option<(u64, String)> {\n 213│    for _ in 0..400 {\n 214│        let reply = brain.net_streams().expect(\"net-streams\");\n 215│        if let Some(s) = reply\n    │...\n 234│/// time out; a serviceable broker answers.\n*235│fn probe(name: &str, cmd_kind: &'static str, want_kind: &'static str, deadline: Duration) -> bool {\n 236│    let name = name.to_string();\n 237│    let (tx, rx) = std::sync::mpsc::channel::<bool>();\n 238│    thread::spawn(move || {\n    │...\n 267│\n*268│/// REQ-HAZARD-INPUT-ACK-BACKPRESSURE — the keystone gate. A FLOOD of N=64 operator\n 269│/// `Input` records is driven through the REAL `serve_attach` CONTROLLER path on ONE\n 270│/// conn (the rc → net-stream → serve_attach → send_effect_no_ack route), exceeding\n 271│/// the ~10-frame IPC pipe buffer. Post-fix the broker emits NO applied frame for\n    │...\n 278│/// watchdog so a regression surfaces as a clean FAILURE, never a hang.\n*279│// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n*280│#[test]\n*281│fn input_flood_through_serve_attach_does_not_deadlock_broker() {\n 282│    let _home = init_home();\n 283│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 284│    let name = unique_name();\n    │...\n 673│         the immediate write error and operation; HelperDisconnected means the flood \\\n*674│         helper exited without reporting (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\"\n 675│    );\n 676│    // (2) The broker PROCESS stays alive on a fresh conn during/after the flood.\n 677│    assert!(\n    │...\n 692│         the broker's session table / dispatch must not be globally frozen \\\n*693│         (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\"\n 694│    );\n 695│    // (4) The post-flood VIEWER is serviced, receives a non-empty decodable cold\n 696│    //     repaint, then receives ACKDL_LIVE_OUTPUT as live fan-out after its\n## redispatch.rs\n  44│/// Acquire a scoped temp home for one complete test body.\n *45│fn init_home() -> TestHome {\n  46│    TestHome::new()\n  47│}\n  48│\n  49│static SEQ: AtomicU32 = AtomicU32::new(0);\n *50│fn unique_name() -> String {\n  51│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n  52│    format!(\"spt-daemon-rdsp-{}-{}.sock\", std::process::id(), n)\n  53│}\n  54│\n *55│fn hermetic() -> NetConfig {\n  56│    NetConfig {\n  57│        identity: Identity::generate(),\n  58│        relay: RelayPolicy::Disabled,\n    │...\n  64│\n *65│fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n  66│    let host = NetHost::start(hermetic()).expect(\"net host start\");\n  67│    let broker =\n  68│        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n    │...\n  75│\n *76│fn connect_retry(name: &str) -> Brain {\n  77│    for _ in 0..300 {\n  78│        if let Ok(b) = Brain::cold_start(name, 1) {\n  79│            return b;\n    │...\n  85│\n *86│fn echo_spawn_req(endpoint: &str) -> SpawnReq {\n  87│    #[cfg(unix)]\n  88│    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n  89│    #[cfg(windows)]\n    │...\n 138│/// The A-side (target broker) peer-stream ids, via a probe brain.\n*139│fn peer_stream_ids(probe: &mut Brain) -> Vec<u64> {\n 140│    probe\n 141│        .net_streams()\n 142│        .expect(\"net-streams\")\n    │...\n 155│/// steal-clears-controller symptom D1 must NOT see).\n*156│fn render_until(\n 157│    operator: &mut Brain,\n 158│    stream_id: u64,\n 159│    decoder: &mut AttachDecoder,\n    │...\n 202│\n*203│fn screen_of(viewport: &[u8]) -> String {\n 204│    let mut screen = ScreenGrid::new(24, 80);\n 205│    screen.advance(viewport);\n 206│    String::from_utf8_lossy(&screen.render_repaint()).into_owned()\n    │...\n 208│\n*209│// [int->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] D1 — the field bug, end to end\n 210│// through PRODUCTION rediscovery: a finished historical Attach and a current\n 211│// active Attach on the same endpoint/origin; only the dispatcher generation\n 212│// restarts. The historical stream must neither take nor clear the current\n    │...\n 216│// and the retired row is GONE from the broker's enumeration\n*217│// (REQ-REDISPATCH-FINISHED-RETIRE observed at the seam that matters).\n*218│#[test]\n*219│fn finished_historical_attach_neither_takes_nor_clears_current_controller() {\n 220│    let _home = init_home();\n 221│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 222│    let (name_a, name_b) = (unique_name(), unique_name());\n    │...\n 350│\n*351│// [int->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] D1b (gate round 1) — the\n 352│// finished-UNRETIRED orderings the Served-report path can never see: the\n 353│// pre-existing backlog and the brain-down-detach window. s1 is attached and\n 354│// driven under gen1, gen1 is KILLED mid-serve (no Served report), THEN the\n    │...\n 363│// ever being served (retire-on-sight drains the backlog).\n*364│// [int->REQ-REDISPATCH-FINISHED-RETIRE]\n*365│#[test]\n*366│fn finished_unretired_attach_is_skipped_and_drained_not_replayed() {\n 367│    let _home = init_home();\n 368│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 369│    let (name_a, name_b) = (unique_name(), unique_name());\n    │...\n 493│\n*494│// [int->REQ-REDISPATCH-FINISHED-RETIRE] the GUARD leg for the family-aware\n 495│// discriminator (gate round 1 counter-check): a request/reply stream reads\n 496│// `finished=true` the moment its requester FINs with the request — sync's\n 497│// `request_sync` does exactly that — so a finished row must NOT be blanket-\n    │...\n 502│// brain cycle).\n*503│#[test]\n*504│fn finished_pending_sync_request_still_serves_across_generation_start() {\n 505│    let _home = init_home();\n 506│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 507│    let (name_a, name_b) = (unique_name(), unique_name());\n## redispatch_stall.rs\n  59│\n *60│fn init_home() -> TestHome {\n  61│    TestHome::new()\n  62│}\n  63│\n  64│static SEQ: AtomicU32 = AtomicU32::new(0);\n *65│fn unique_name() -> String {\n  66│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n  67│    format!(\"spt-daemon-rdstall-{}-{}.sock\", std::process::id(), n)\n  68│}\n  69│\n *70│fn hermetic() -> NetConfig {\n  71│    NetConfig {\n  72│        identity: Identity::generate(),\n  73│        relay: RelayPolicy::Disabled,\n    │...\n  79│\n *80│fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n  81│    let host = NetHost::start(hermetic()).expect(\"net host start\");\n  82│    let broker =\n  83│        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n    │...\n  90│\n *91│fn connect_retry(name: &str) -> Brain {\n  92│    for _ in 0..300 {\n  93│        if let Ok(b) = Brain::cold_start(name, 1) {\n  94│            return b;\n    │...\n 100│\n*101│fn echo_spawn_req(endpoint: &str) -> SpawnReq {\n 102│    #[cfg(unix)]\n 103│    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n 104│    #[cfg(windows)]\n    │...\n 152│/// The A-side peer-stream ids, via a probe brain.\n*153│fn peer_stream_ids(probe: &mut Brain) -> Vec<u64> {\n 154│    probe\n 155│        .net_streams()\n 156│        .expect(\"net-streams\")\n    │...\n 165│/// helper, verbatim). Returns whether an EOF for the stream arrived.\n*166│fn render_until(\n 167│    operator: &mut Brain,\n 168│    stream_id: u64,\n 169│    decoder: &mut AttachDecoder,\n    │...\n 212│\n*213│fn screen_of(viewport: &[u8]) -> String {\n 214│    let mut screen = ScreenGrid::new(24, 80);\n 215│    screen.advance(viewport);\n 216│    String::from_utf8_lossy(&screen.render_repaint()).into_owned()\n    │...\n 220│/// opener line. Returns (operator brain, B-side stream id, A-side stream id).\n*221│fn open_traffic_stream(\n 222│    _name_a: &str,\n 223│    name_b: &str,\n 224│    probe: &mut Brain,\n    │...\n 261│/// seq contiguity from 0 (gapless) and returns the bytes.\n*262│fn drain_gapless(name_a: &str, sid_a: u64, want_bytes: usize, deadline: Duration) -> Vec<u8> {\n 263│    let end = Instant::now() + deadline;\n 264│    'attach: loop {\n 265│        assert!(\n    │...\n 319│\n*320│// [int->REQ-HAZARD-REDISPATCH-STALL]\n*321│// [int->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n 322│// T1 (poisoned-replay halt) + T3 (producer latency flat), one incident on the\n 323│// production broker subscribe path: a REAL subscriber conn that attaches with\n 324│// a full ring and never reads — its seat's replay wedges on the named-pipe\n    │...\n 330│// subscriber replays everything gapless from the cursor — no abandonment.\n*331│#[test]\n*332│fn wedged_subscriber_never_stalls_the_producer_and_recovery_replays_gapless() {\n 333│    let _home = init_home();\n 334│    // Shrink the bounded write so the poison window is 400ms, not 15s — the\n 335│    // knob grants wall-clock only, never relaxes an assertion (the\n    │...\n 405│\n*406│// [int->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n 407│// T4 (forwarding cancel): a REAL forwarding leg — ack'd `net_stream_send`\n 408│// ticks on the stream's send half — keeps serving while the subscriber is\n 409│// healthy; when the subscriber seat POISONS (the same wedged-conn incident as\n    │...\n 413│// fresh send serves again (recovery, the C2-lease seam this wave builds).\n*414│#[test]\n*415│fn poisoned_subscriber_cancels_the_paired_forwarding_leg_then_recovery_serves() {\n 416│    let _home = init_home();\n 417│    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n 418│    let dir = tempfile::tempdir().expect(\"tempdir\");\n    │...\n 536│\n*537│// [int->REQ-DISPATCH-FALLBACK-CIRCUIT]\n 538│// T2 (fallback narrowing): a stream whose opener never pins (bytes, no\n 539│// newline) is a TRANSIENT to the dispatcher — the claim requeues bounded and\n 540│// goes terminal after exactly MAX_DISPATCH_ATTEMPTS failures, and NO peek\n    │...\n 544│// each such retry manufactured a fresh replay subscriber on the conn).\n*545│#[test]\n## attach.rs\n  *1│//! D5b regression — remote terminal attach (REQ-INST-8): drive a real PTY\n   2│//! session on daemon A from daemon B, byte-stream viewport over the\n   3│//! broker-owned wire; compute stays on A.\n   4│//!\n    │...\n  13│//!   on the stream table's handshake-proven `remote_id_hex`;\n *14│//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies\n  15│//!   mid-attach; the broker keeps the QUIC stream, the session, the output\n  16│//!   log, and the effect journal. The successor brain re-serves the SAME\n  17│//!   stream from seq 0 — the worst-case stale cursor: the replayed input\n    │...\n  47│/// the endpoint perch's `driven_by` marker without ever touching a real home.\n *48│fn init_home() -> TestHome {\n  49│    TestHome::new()\n  50│}\n  51│\n  52│static SEQ: AtomicU32 = AtomicU32::new(0);\n *53│fn unique_name() -> String {\n  54│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n  55│    format!(\"spt-daemon-d5b-{}-{}.sock\", std::process::id(), n)\n  56│}\n  57│\n *58│fn hermetic() -> NetConfig {\n  59│    NetConfig {\n  60│        identity: Identity::generate(),\n  61│        relay: RelayPolicy::Disabled,\n    │...\n  67│\n *68│fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n  69│    let host = NetHost::start(hermetic()).expect(\"net host start\");\n  70│    let broker =\n  71│        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n    │...\n  78│\n *79│fn connect_retry(name: &str) -> Brain {\n  80│    for _ in 0..300 {\n  81│        if let Ok(b) = Brain::cold_start(name, 1) {\n  82│            return b;\n    │...\n  90│/// actually expire, retried over broker start-up the same way.\n *91│fn connect_retry_pump(name: &str, io_timeout: Duration) -> Brain {\n  92│    for _ in 0..300 {\n  93│        if let Ok(b) =\n  94│            Brain::cold_start_pump(name, 1, io_timeout, spt_daemon::brain::PumpTrace::Stderr)\n    │...\n 102│\n*103│fn echo_spawn_req() -> SpawnReq {\n 104│    #[cfg(unix)]\n 105│    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n 106│    #[cfg(windows)]\n    │...\n 138│/// same id twice is ONE stream served twice, never two streams sharing a number.\n*139│fn wait_for_stream(brain: &mut Brain, served: &[u64]) -> (u64, String) {\n 140│    for _ in 0..400 {\n 141│        let reply = brain.net_streams().expect(\"net-streams\");\n 142│        if let Some(s) = reply\n    │...\n 168│#[allow(clippy::too_many_arguments)]\n*169│fn attach_drive_detach(\n 170│    operator: &mut Brain,\n 171│    target_broker: &str,\n 172│    mut target: Brain,\n    │...\n 243│/// the rendered-through cursor (last accepted seq + 1).\n*244│fn render_until(\n 245│    brain: &mut Brain,\n 246│    stream_id: u64,\n 247│    decoder: &mut AttachDecoder,\n    │...\n 286│/// Count non-overlapping occurrences of `needle` in `hay`.\n*287│fn count(hay: &[u8], needle: &[u8]) -> usize {\n 288│    if needle.is_empty() || hay.len() < needle.len() {\n 289│        return 0;\n 290│    }\n    │...\n 302│\n*303│// [int->REQ-RC-1] loopback self-dial is REFUSED by the transport — pins the\n 304│// finding that drives `spt rc`'s LOCAL transport choice. A node cannot QUIC-dial\n 305│// its own node id (iroh: \"Connecting to ourself is not supported\"), so local\n 306│// attach CANNOT ride a self-dialed QUIC conn. The one-pump invariant therefore\n    │...\n 309│// Cross-node attach (the prize) is unaffected: it dials a real remote peer.\n*310│#[test]\n*311│fn loopback_self_dial_is_refused_local_uses_fallback_transport() {\n 312│    let _home = init_home();\n 313│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 314│    let name = unique_name();\n    │...\n 329│\n*330│// [int->REQ-HOST-RUN-1] [int->REQ-RC-1] spt-hosted harness bringup + cross-node\n 331│// attach, end to end: `harnesshost::launch_harness_brokered_in` spawns a harness's\n 332│// `[session.self]` into broker A's PTY (the spt-hosted bringup — the daemon owns\n 333│// the PTY, not an external launcher), then an operator on broker B attaches\n    │...\n 337│// the viewport (detach never kills it).\n*338│#[test]\n*339│fn spt_hosted_bringup_then_cross_node_attach_drives_the_pty() {\n 340│    let _home = init_home();\n 341│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 342│    let (name_a, name_b) = (unique_name(), unique_name());\n    │...\n 431│\n*432│// [unit->REQ-INST-8] drive an agent('s session) on machine A from machine B:\n 433│// B's keystrokes reach A's real PTY child over the wire (journaled input\n 434│// path), the child's output streams back as sequenced records, and B renders\n 435│// its own marker echoed remotely. The serve gate sees the handshake-proven\n## refresh_freeze.rs\n 16│\n*17│fn write_json(path: &Path, value: &serde_json::Value) {\n 18│    std::fs::write(path, serde_json::to_vec_pretty(value).unwrap()).unwrap();\n 19│}\n 20│\n*21│fn python() -> PathBuf {\n 22│    let program = std::env::var_os(\"SPT_REFRESH_RIG_PYTHON\").unwrap_or_else(|| \"python\".into());\n 23│    let output = Command::new(program)\n 24│        .args([\"-c\", \"import sys; print(sys.executable)\"])\n   │...\n 35│\n*36│// [int->REQ-DAEMON-REFRESH]\n*37│// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n*38│#[test]\n*39│fn refresh_resumes_bounded_input_ack_without_replacement_poison() {\n 40│    let binary = std::env::var_os(\"SPT_REFRESH_RIG_SPT\")\n 41│        .map(PathBuf::from)\n 42│        .unwrap_or_else(|| {"},"isError":false,"timestamp":1789528530842}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_6B6MSfCdIkRDu3boCiaLzfX9|fc_0cf46cef591a107c016aaa09dd110087d2927e42ecda1c1a3a","toolName":"read","startedAt":"2026-09-16T03:15:41.140Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs:1-85,281-460"},"intent":"Reading flood workload bounds"},"id":"beab7f32","parentId":"aaa867fb","timestamp":"2026-09-16T03:15:41.140Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4NY2t1GkAzE75TVZdLtMKn8f|fc_0cf46cef591a107c016aaa09dd111087d28296165f821a92fa","toolName":"read","startedAt":"2026-09-16T03:15:41.140Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs:432-810"},"intent":"Reading restart regression coverage"},"id":"ae5eb58a","parentId":"beab7f32","timestamp":"2026-09-16T03:15:41.140Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_EHK7m5gq7yN3NuGNuNCkx0he|fc_0cf46cef591a107c016aaa09dd111887d2a56070d101991b6e","toolName":"read","startedAt":"2026-09-16T03:15:41.140Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs:1-230"},"intent":"Reading gated refresh rig"},"id":"62b4a223","parentId":"ae5eb58a","timestamp":"2026-09-16T03:15:41.140Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_M9QYnxQebEeuOT2cZGwFx52y|fc_0cf46cef591a107c016aaa09dd111c87d2a7398ed0f1b034bf","toolName":"grep","startedAt":"2026-09-16T03:15:41.140Z","args":{"path":".worktrees/49-267-refresh-discriminator/traceable-reqs.toml;.worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md;.worktrees/49-267-refresh-discriminator/docs/adr"},"intent":"Finding precise requirement definitions"},"id":"bdca1732","parentId":"62b4a223","timestamp":"2026-09-16T03:15:41.140Z"}
{"type":"message","id":"f9eb2d3c","parentId":"bdca1732","timestamp":"2026-09-16T03:15:41.140Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_6B6MSfCdIkRDu3boCiaLzfX9|fc_0cf46cef591a107c016aaa09dd110087d2927e42ecda1c1a3a","name":"read","arguments":{"i":"Reading flood workload bounds","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs:1-85,281-460"},"intent":"Reading flood workload bounds"},{"type":"toolCall","id":"call_4NY2t1GkAzE75TVZdLtMKn8f|fc_0cf46cef591a107c016aaa09dd111087d28296165f821a92fa","name":"read","arguments":{"i":"Reading restart regression coverage","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs:432-810"},"intent":"Reading restart regression coverage"},{"type":"toolCall","id":"call_EHK7m5gq7yN3NuGNuNCkx0he|fc_0cf46cef591a107c016aaa09dd111887d2a56070d101991b6e","name":"read","arguments":{"i":"Reading gated refresh rig","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs:1-230"},"intent":"Reading gated refresh rig"},{"type":"toolCall","id":"call_M9QYnxQebEeuOT2cZGwFx52y|fc_0cf46cef591a107c016aaa09dd111c87d2a7398ed0f1b034bf","name":"grep","arguments":{"i":"Finding precise requirement definitions","pattern":"REQ-DAEMON-REFRESH|REQ-HAZARD-INPUT-ACK-BACKPRESSURE|REQ-HAZARD-REDISPATCH|REQ-REDISPATCH|promotion|by: Some","path":".worktrees/49-267-refresh-discriminator/traceable-reqs.toml;.worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md;.worktrees/49-267-refresh-discriminator/docs/adr","case":true,"gitignore":false,"skip":null},"intent":"Finding precise requirement definitions"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":18374,"output":285,"cacheRead":11904,"cacheWrite":0,"totalTokens":30563,"cost":{"input":0.18374000000000001,"output":0.01425,"cacheRead":0.011904,"cacheWrite":0,"total":0.20989400000000002}},"stopReason":"toolUse","timestamp":1789528530906,"responseId":"resp_0cf46cef591a107c016aaa09d32ce087d2bb552b52f011c2b8","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0cf46cef591a107c016aaa09dd110087d2927e42ecda1c1a3a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading flood workload bounds\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs:1-85,281-460\"}","call_id":"call_6B6MSfCdIkRDu3boCiaLzfX9","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"read","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa09dd111087d28296165f821a92fa","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading restart regression coverage\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs:432-810\"}","call_id":"call_4NY2t1GkAzE75TVZdLtMKn8f","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"read","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa09dd111887d2a56070d101991b6e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading gated refresh rig\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs:1-230\"}","call_id":"call_EHK7m5gq7yN3NuGNuNCkx0he","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"read","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa09dd111c87d2a7398ed0f1b034bf","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding precise requirement definitions\",\"pattern\":\"REQ-DAEMON-REFRESH|REQ-HAZARD-INPUT-ACK-BACKPRESSURE|REQ-HAZARD-REDISPATCH|REQ-REDISPATCH|promotion|by: Some\",\"path\":\".worktrees/49-267-refresh-discriminator/traceable-reqs.toml;.worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md;.worktrees/49-267-refresh-discriminator/docs/adr\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_M9QYnxQebEeuOT2cZGwFx52y","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"grep","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}}]},"duration":10231.656200000085,"ttft":10188.810499999672,"completedAt":1789528541138,"contextSnapshot":{"promptTokens":30278,"nonMessageTokens":10304,"compactionEpoch":0}}}
{"type":"message","id":"d64c3504","parentId":"f9eb2d3c","timestamp":"2026-09-16T03:15:41.144Z","message":{"role":"toolResult","toolCallId":"call_EHK7m5gq7yN3NuGNuNCkx0he|fc_0cf46cef591a107c016aaa09dd111887d2a56070d101991b6e","toolName":"read","content":[{"type":"text","text":"//! #307 real Windows refresh regression: the operator's existing rc stream must\n//! recover tagged child ACKs, without poisoning replacement controllers.\n//!\n//! Windows is the measured failure surface. The Python driver is stdlib-only;\n//! the retained native launcher contains its complete child tree from birth.\n//! Build the candidate CLI first (`cargo build -p spt --bin spt`). Override\n//! SPT_REFRESH_RIG_SPT to compare explicitly selected producer bytes, and\n//! SPT_REFRESH_RIG_OUTPUT to retain receipts at a caller-selected fresh path.\n//! HEAVY AT BIRTH: real daemon, brain, ConPTY child and network-backed local rc.\n\n#![cfg(windows)]\n\nuse std::path::{Path, PathBuf};\nuse std::process::{Command, Stdio};\nuse std::time::{Duration, Instant};\n\nfn write_json(path: &Path, value: &serde_json::Value) {\n    std::fs::write(path, serde_json::to_vec_pretty(value).unwrap()).unwrap();\n}\n\nfn python() -> PathBuf {\n    let program = std::env::var_os(\"SPT_REFRESH_RIG_PYTHON\").unwrap_or_else(|| \"python\".into());\n    let output = Command::new(program)\n        .args([\"-c\", \"import sys; print(sys.executable)\"])\n        .output()\n        .expect(\"Python is required for the real refresh rig\");\n    assert!(\n        output.status.success(),\n        \"Python executable discovery failed\"\n    );\n    let executable = PathBuf::from(String::from_utf8(output.stdout).unwrap().trim());\n    assert!(executable.is_absolute() && executable.is_file());\n    executable\n}\n\n// [int->REQ-DAEMON-REFRESH]\n// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n#[test]\nfn refresh_resumes_bounded_input_ack_without_replacement_poison() {\n    let binary = std::env::var_os(\"SPT_REFRESH_RIG_SPT\")\n        .map(PathBuf::from)\n        .unwrap_or_else(|| {\n            std::env::current_exe()\n                .unwrap()\n                .parent()\n                .unwrap()\n                .parent()\n                .unwrap()\n                .join(\"spt.exe\")\n        });\n    let binary = std::fs::canonicalize(binary)\n        .expect(\"build candidate spt with cargo build -p spt --bin spt before this cell\");\n    let bytes = std::fs::read(&binary).unwrap();\n    let hash = spt_daemon::sha256_hex(&bytes);\n    drop(bytes);\n    if let Ok(expected) = std::env::var(\"SPT_REFRESH_RIG_EXPECTED_SHA256\") {\n        assert_eq!(\n            hash, expected,\n            \"candidate bytes differ from the admitted pin\"\n        );\n    }\n    let retained = std::env::var_os(\"SPT_REFRESH_RIG_OUTPUT\")\n        .map(PathBuf::from)\n        .unwrap_or_else(|| tempfile::tempdir().expect(\"receipt root\").keep());\n    if !retained.exists() {\n        std::fs::create_dir_all(&retained).unwrap();\n    }\n    let retained = std::fs::canonicalize(retained).unwrap();\n    let output = retained.join(\"observation\");\n    assert!(!output.exists(), \"never adopt an existing observation home\");\n    let support = Path::new(env!(\"CARGO_MANIFEST_DIR\")).join(\"tests/support/refresh_freeze\");\n    let config = retained.join(\"config.json\");\n    assert!(!config.exists(), \"receipt directory must be fresh\");\n    write_json(\n        &config,\n        &serde_json::json!({\n            \"output\": output,\n            \"binary\": binary,\n            \"sha256\": hash,\n            \"socket_helper\": env!(\"CARGO_BIN_EXE_refresh_rig_fixture\"),\n            \"observation_seconds\": 150,\n            \"teardown_seconds\": 60,\n            \"ack_bound_ms\": 2000,\n            \"recovery_bound_ms\": 15000,\n            \"baseline_seconds\": 30,\n            \"post_refresh_seconds\": 25\n        }),\n    );\n    let argv = retained.join(\"argv.json\");\n    write_json(\n        &argv,\n        &serde_json::json!([\"-u\", support.join(\"rig.py\"), \"--config\", config]),\n    );\n    let environment = retained.join(\"environment.json\");\n    write_json(\n        &environment,\n        &serde_json::json!({\n            \"SPT_HOME\": output.join(\"home\"),\n            \"SPT_INSTALL_NO_FIREWALL\": \"1\",\n            \"SPT_TEST_EPHEMERAL_ADVISORY_PORTS\": \"1\",\n            \"SPT_PUMP_TRACE\": \"1\"\n        }),\n    );\n    let native = retained.join(\"native.json\");\n    let mut child = Command::new(\"pwsh\")\n        .args([\"-NoProfile\", \"-File\"])\n        .arg(support.join(\"launch-v2.ps1\"))\n        .args([\"-Label\", \"refresh-freeze\", \"-Seconds\", \"220\", \"-RecordFile\"])\n        .arg(&native)\n        .arg(\"-ArgsFile\")\n        .arg(&argv)\n        .arg(\"-Exe\")\n        .arg(python())\n        .arg(\"-OutFile\")\n        .arg(retained.join(\"driver.stdout.log\"))\n        .arg(\"-ErrFile\")\n        .arg(retained.join(\"driver.stderr.log\"))\n        .args([\n            \"-Admission\",\n            \"process-tree\",\n            \"-Scope\",\n            \"step\",\n            \"-EnvironmentFile\",\n        ])\n        .arg(environment)\n        .stdin(Stdio::null())\n        .spawn()\n        .expect(\"start bounded native-owned regression rig\");\n    let deadline = Instant::now() + Duration::from_secs(230);\n    let status = loop {\n        if let Some(status) = child.try_wait().unwrap() {\n            break status;\n        }\n        if Instant::now() >= deadline {\n            // This is the held launcher Child, not an unauthenticated PID lookup.\n            // Closing its native job handle kills only its admitted child tree.\n            child.kill().expect(\"terminate unresponsive owned launcher\");\n            let _ = child.wait();\n            panic!(\n                \"native rig deadline exceeded; retained evidence: {}\",\n                retained.display()\n            );\n        }\n        std::thread::sleep(Duration::from_millis(100));\n    };\n    let native: serde_json::Value =\n        serde_json::from_slice(&std::fs::read(native).unwrap()).unwrap();\n    assert_eq!(\n        native[\"termination\"], \"CONFIRMED_GONE\",\n        \"unconfirmed teardown: {native}\"\n    );\n    assert_eq!(\n        native[\"job\"][\"active_processes\"], 0,\n        \"owned process leak: {native}\"\n    );\n    let receipt_path = output.join(\"receipt.json\");\n    let receipt = std::fs::read_to_string(&receipt_path)\n        .unwrap_or_else(|error| format!(\"receipt unavailable: {error}\"));\n    assert!(\n        status.success(),\n        \"refresh must resume bounded child ACK RTT, with zero replacement write-poison and only detach slot closures; native={status}; receipt={}\\n{receipt}\",\n        receipt_path.display()\n    );\n    let receipt: serde_json::Value =\n        serde_json::from_str(&receipt).expect(\"valid regression receipt\");\n    assert_eq!(\n        receipt[\"status\"], \"PASS\",\n        \"exit0 without passing observable predicate: {receipt}\"\n    );\n    eprintln!(\"REFRESH_REGRESSION_PASS: {}\", receipt_path.display());\n}"}],"details":{"totalLines":171,"displayContent":{"text":"//! #307 real Windows refresh regression: the operator's existing rc stream must\r\n//! recover tagged child ACKs, without poisoning replacement controllers.\r\n//!\r\n//! Windows is the measured failure surface. The Python driver is stdlib-only;\r\n//! the retained native launcher contains its complete child tree from birth.\r\n//! Build the candidate CLI first (`cargo build -p spt --bin spt`). Override\r\n//! SPT_REFRESH_RIG_SPT to compare explicitly selected producer bytes, and\r\n//! SPT_REFRESH_RIG_OUTPUT to retain receipts at a caller-selected fresh path.\r\n//! HEAVY AT BIRTH: real daemon, brain, ConPTY child and network-backed local rc.\r\n\r\n#![cfg(windows)]\r\n\r\nuse std::path::{Path, PathBuf};\r\nuse std::process::{Command, Stdio};\r\nuse std::time::{Duration, Instant};\r\n\r\nfn write_json(path: &Path, value: &serde_json::Value) {\r\n    std::fs::write(path, serde_json::to_vec_pretty(value).unwrap()).unwrap();\r\n}\r\n\r\nfn python() -> PathBuf {\r\n    let program = std::env::var_os(\"SPT_REFRESH_RIG_PYTHON\").unwrap_or_else(|| \"python\".into());\r\n    let output = Command::new(program)\r\n        .args([\"-c\", \"import sys; print(sys.executable)\"])\r\n        .output()\r\n        .expect(\"Python is required for the real refresh rig\");\r\n    assert!(\r\n        output.status.success(),\r\n        \"Python executable discovery failed\"\r\n    );\r\n    let executable = PathBuf::from(String::from_utf8(output.stdout).unwrap().trim());\r\n    assert!(executable.is_absolute() && executable.is_file());\r\n    executable\r\n}\r\n\r\n// [int->REQ-DAEMON-REFRESH]\r\n// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n#[test]\r\nfn refresh_resumes_bounded_input_ack_without_replacement_poison() {\r\n    let binary = std::env::var_os(\"SPT_REFRESH_RIG_SPT\")\r\n        .map(PathBuf::from)\r\n        .unwrap_or_else(|| {\r\n            std::env::current_exe()\r\n                .unwrap()\r\n                .parent()\r\n                .unwrap()\r\n                .parent()\r\n                .unwrap()\r\n                .join(\"spt.exe\")\r\n        });\r\n    let binary = std::fs::canonicalize(binary)\r\n        .expect(\"build candidate spt with cargo build -p spt --bin spt before this cell\");\r\n    let bytes = std::fs::read(&binary).unwrap();\r\n    let hash = spt_daemon::sha256_hex(&bytes);\r\n    drop(bytes);\r\n    if let Ok(expected) = std::env::var(\"SPT_REFRESH_RIG_EXPECTED_SHA256\") {\r\n        assert_eq!(\r\n            hash, expected,\r\n            \"candidate bytes differ from the admitted pin\"\r\n        );\r\n    }\r\n    let retained = std::env::var_os(\"SPT_REFRESH_RIG_OUTPUT\")\r\n        .map(PathBuf::from)\r\n        .unwrap_or_else(|| tempfile::tempdir().expect(\"receipt root\").keep());\r\n    if !retained.exists() {\r\n        std::fs::create_dir_all(&retained).unwrap();\r\n    }\r\n    let retained = std::fs::canonicalize(retained).unwrap();\r\n    let output = retained.join(\"observation\");\r\n    assert!(!output.exists(), \"never adopt an existing observation home\");\r\n    let support = Path::new(env!(\"CARGO_MANIFEST_DIR\")).join(\"tests/support/refresh_freeze\");\r\n    let config = retained.join(\"config.json\");\r\n    assert!(!config.exists(), \"receipt directory must be fresh\");\r\n    write_json(\r\n        &config,\r\n        &serde_json::json!({\r\n            \"output\": output,\r\n            \"binary\": binary,\r\n            \"sha256\": hash,\r\n            \"socket_helper\": env!(\"CARGO_BIN_EXE_refresh_rig_fixture\"),\r\n            \"observation_seconds\": 150,\r\n            \"teardown_seconds\": 60,\r\n            \"ack_bound_ms\": 2000,\r\n            \"recovery_bound_ms\": 15000,\r\n            \"baseline_seconds\": 30,\r\n            \"post_refresh_seconds\": 25\r\n        }),\r\n    );\r\n    let argv = retained.join(\"argv.json\");\r\n    write_json(\r\n        &argv,\r\n        &serde_json::json!([\"-u\", support.join(\"rig.py\"), \"--config\", config]),\r\n    );\r\n    let environment = retained.join(\"environment.json\");\r\n    write_json(\r\n        &environment,\r\n        &serde_json::json!({\r\n            \"SPT_HOME\": output.join(\"home\"),\r\n            \"SPT_INSTALL_NO_FIREWALL\": \"1\",\r\n            \"SPT_TEST_EPHEMERAL_ADVISORY_PORTS\": \"1\",\r\n            \"SPT_PUMP_TRACE\": \"1\"\r\n        }),\r\n    );\r\n    let native = retained.join(\"native.json\");\r\n    let mut child = Command::new(\"pwsh\")\r\n        .args([\"-NoProfile\", \"-File\"])\r\n        .arg(support.join(\"launch-v2.ps1\"))\r\n        .args([\"-Label\", \"refresh-freeze\", \"-Seconds\", \"220\", \"-RecordFile\"])\r\n        .arg(&native)\r\n        .arg(\"-ArgsFile\")\r\n        .arg(&argv)\r\n        .arg(\"-Exe\")\r\n        .arg(python())\r\n        .arg(\"-OutFile\")\r\n        .arg(retained.join(\"driver.stdout.log\"))\r\n        .arg(\"-ErrFile\")\r\n        .arg(retained.join(\"driver.stderr.log\"))\r\n        .args([\r\n            \"-Admission\",\r\n            \"process-tree\",\r\n            \"-Scope\",\r\n            \"step\",\r\n            \"-EnvironmentFile\",\r\n        ])\r\n        .arg(environment)\r\n        .stdin(Stdio::null())\r\n        .spawn()\r\n        .expect(\"start bounded native-owned regression rig\");\r\n    let deadline = Instant::now() + Duration::from_secs(230);\r\n    let status = loop {\r\n        if let Some(status) = child.try_wait().unwrap() {\r\n            break status;\r\n        }\r\n        if Instant::now() >= deadline {\r\n            // This is the held launcher Child, not an unauthenticated PID lookup.\r\n            // Closing its native job handle kills only its admitted child tree.\r\n            child.kill().expect(\"terminate unresponsive owned launcher\");\r\n            let _ = child.wait();\r\n            panic!(\r\n                \"native rig deadline exceeded; retained evidence: {}\",\r\n                retained.display()\r\n            );\r\n        }\r\n        std::thread::sleep(Duration::from_millis(100));\r\n    };\r\n    let native: serde_json::Value =\r\n        serde_json::from_slice(&std::fs::read(native).unwrap()).unwrap();\r\n    assert_eq!(\r\n        native[\"termination\"], \"CONFIRMED_GONE\",\r\n        \"unconfirmed teardown: {native}\"\r\n    );\r\n    assert_eq!(\r\n        native[\"job\"][\"active_processes\"], 0,\r\n        \"owned process leak: {native}\"\r\n    );\r\n    let receipt_path = output.join(\"receipt.json\");\r\n    let receipt = std::fs::read_to_string(&receipt_path)\r\n        .unwrap_or_else(|error| format!(\"receipt unavailable: {error}\"));\r\n    assert!(\r\n        status.success(),\r\n        \"refresh must resume bounded child ACK RTT, with zero replacement write-poison and only detach slot closures; native={status}; receipt={}\\n{receipt}\",\r\n        receipt_path.display()\r\n    );\r\n    let receipt: serde_json::Value =\r\n        serde_json::from_str(&receipt).expect(\"valid regression receipt\");\r\n    assert_eq!(\r\n        receipt[\"status\"], \"PASS\",\r\n        \"exit0 without passing observable predicate: {receipt}\"\r\n    );\r\n    eprintln!(\"REFRESH_REGRESSION_PASS: {}\", receipt_path.display());\r\n}\r","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171]},"fileSize":6708,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon\\tests\\refresh_freeze.rs"}}},"isError":false,"timestamp":1789528541143}}
{"type":"message","id":"6fa9cd25","parentId":"d64c3504","timestamp":"2026-09-16T03:15:41.170Z","message":{"role":"toolResult","toolCallId":"call_4NY2t1GkAzE75TVZdLtMKn8f|fc_0cf46cef591a107c016aaa09dd111087d28296165f821a92fa","toolName":"read","content":[{"type":"text","text":"\n// [unit->REQ-INST-8] drive an agent('s session) on machine A from machine B:\n// B's keystrokes reach A's real PTY child over the wire (journaled input\n// path), the child's output streams back as sequenced records, and B renders\n// its own marker echoed remotely. The serve gate sees the handshake-proven\n// origin; B detaching ends the viewport with the session still running on A.\n#[test]\nfn remote_attach_drives_a_real_pty_cross_daemon() {\n    let _home = init_home();\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let (name_a, name_b) = (unique_name(), unique_name());\n    let broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\n\n    // Target A: spawn the echo child.\n    let mut target = connect_retry(&name_a);\n    let sid = target\n        .spawn_session(echo_spawn_req())\n        .expect(\"spawn echo child\");\n    let a_addr = target.net_status().expect(\"a status\").addr;\n\n    // Operator B: dial A (CROSS-NODE QUIC conn), then drive + detach over the\n    // SHARED attach pump. The serve gate sees the QUIC handshake identity.\n    let mut operator = connect_retry(&name_b);\n    let b_node = operator\n        .net_status()\n        .expect(\"b status\")\n        .node_id_hex\n        .expect(\"node id\");\n    let conn = operator\n        .net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1)))\n        .expect(\"dial\");\n    let (_viewport, mut target) = attach_drive_detach(\n        &mut operator,\n        &name_a,\n        target,\n        conn.conn_id,\n        sid,\n        \"ling\",\n        &b_node,\n        b\"REMOTE_DRIVE_OK\",\n    );\n    assert_eq!(\n        broker_a.session_count(),\n        1,\n        \"the session outlives the viewport\"\n    );\n    target.kill_session().expect(\"cleanup kill\");\n}\n\n// [int->REQ-RC-1] LOCAL attach rides the byte-identical pump (M12 W1.5, B1):\n// a single broker spawns a real PTY session; the operator acquires a\n// broker-minted in-process LOOPBACK conn (`net_dial_loopback` — NOT a QUIC\n// self-dial, which iroh refuses) and drives the PTY through the SAME\n// `serve_attach`+render pump a cross-node operator uses. The proof that \"both\n// transports → one pump\" is BY CONSTRUCTION: this test and the cross-node\n// `remote_attach_drives_a_real_pty_cross_daemon` call the identical\n// `attach_drive_detach` helper; they differ only in how `conn_id` is acquired\n// (`net_dial_loopback` vs `net_dial`) and in the expected origin. The loopback\n// origin is THIS node's own id, gating same-node (access short-circuit Allow in\n// production; default-open in this hermetic home — both Allow). The session\n// outlives the viewport.\n#[test]\nfn local_attach_via_loopback_conn_rides_the_same_pump() {\n    let _home = init_home();\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let name = unique_name();\n    let broker = net_broker(&name, &dir.path().join(\"solo\"));\n\n    // One broker plays BOTH target and operator (the local case). Spawn the\n    // echo child, then attach to it over a loopback conn from a second brain\n    // connection to the SAME broker.\n    let mut target = connect_retry(&name);\n    let sid = target\n        .spawn_session(echo_spawn_req())\n        .expect(\"spawn echo child\");\n\n    // The operator acquires the broker's loopback conn; its `remote_id_hex` is\n    // this node's OWN broker-minted id — the origin the peer row carries and the\n    // serve gates same-node against.\n    let mut operator = connect_retry(&name);\n    let dialed = operator\n        .net_dial_loopback()\n        .expect(\"operator loopback dial\");\n    let own_origin = dialed.remote_id_hex.clone();\n    let (_viewport, mut target) = attach_drive_detach(\n        &mut operator,\n        &name,\n        target,\n        dialed.conn_id,\n        sid,\n        \"ling\",\n        &own_origin,\n        b\"LOCAL_DRIVE_OK\",\n    );\n    assert_eq!(\n        broker.session_count(),\n        1,\n        \"the session outlives the local viewport\"\n    );\n    target.kill_session().expect(\"cleanup kill\");\n}\n\n// [unit->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT] the replay-vs-forward deadlock pin:\n// when the session's OutputLog ring is ALREADY NON-EMPTY at attach (an\n// already-producing session — exactly the endpoint-run shape, whose transient\n// cold-start spawn conn drops after `Spawned`), `become_controller` replays MANY\n// frames into the SERVING brain while `serve_attach` must forward each back to the\n// operator. Before the two-conn split both rode ONE broker IPC connection, so the\n// broker's single-threaded per-conn handler — stuck mid-replay (broker→serve) —\n// could not read the serve→broker forwards: mutual backpressure DEADLOCK, operator\n// got ZERO bytes. With `serve_attach` forwarding on a SEPARATE conn the full\n// populated ring replays and is delivered in order. The prior loopback E2Es only\n// ever attached to an idle/just-spawned child (empty ring → 0-frame replay), so\n// none exercised this — the regression the suite lacked.\n#[test]\nfn loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock() {\n    let _home = init_home();\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let name = unique_name();\n    let _broker = net_broker(&name, &dir.path().join(\"solo\"));\n\n    // A child that emits a BURST of lines (many ring chunks) then blocks on stdin\n    // so it never exits before we attach. `TICK39` (the last line) proves the whole\n    // populated ring replayed + forwarded in order — not just the first chunk.\n    #[cfg(unix)]\n    let (program, args) = (\n        \"sh\".to_string(),\n        vec![\n            \"-c\".to_string(),\n            \"i=0; while [ $i -lt 39 ]; do echo TICK$i; i=$((i+1)); done; sleep 0.2; echo TICK39; cat\".to_string(),\n        ],\n    );\n    #[cfg(windows)]\n    let (program, args) = (\n        \"cmd\".to_string(),\n        vec![\n            \"/c\".to_string(),\n            \"(for /L %i in (0,1,38) do @echo TICK%i) & ping -n 2 127.0.0.1 >nul & echo TICK39 & findstr \\\".\\\"\".to_string(),\n        ],\n    );\n\n    // The SPAWNING conn drains the burst live into the ring, then we DROP it — so a\n    // fresh attach must replay the now non-empty ring (the endpoint-run shape).\n    let mut spawner = connect_retry(&name);\n    let sid = spawner\n        .spawn_session(SpawnReq {\n            program,\n            args,\n            rows: 24,\n            cols: 80,\n            endpoint: \"burst-ep\".into(),\n            cwd: None,\n            env: Default::default(),\n            translation_binary: None,\n            adapter: String::new(),\n            install_dir: None,\n        })\n        .expect(\"spawn burst child\");\n    let mut drained = Vec::new();\n    spawner\n        .read_output_until(b\"TICK39\", &mut drained)\n        .expect(\"the burst lands in the ring\");\n    drop(spawner); // spawn conn gone — a fresh attach must replay the populated ring\n\n    // A fresh operator attaches over loopback (the real `spt rc` transport).\n    let mut operator = connect_retry(&name);\n    let dialed = operator.net_dial_loopback().expect(\"loopback dial\");\n    let own_origin = dialed.remote_id_hex.clone();\n    let stream_b = request_attach(\n        &mut operator,\n        dialed.conn_id,\n        sid,\n        0,\n        MintedOp::new(Minter::Rc, 2),\n        AttachIntent::Control,\n    )\n    .expect(\"request\");\n    operator\n        .net_stream_subscribe(stream_b, 0)\n        .expect(\"operator subscribe\");\n\n    let mut target = connect_retry(&name);\n    let (stream_a, origin) = wait_for_stream(&mut target, &[]);\n    assert_eq!(origin, own_origin);\n    let serve_broker = name.clone();\n    let server = thread::spawn(move || {\n        serve_attach(\n            &mut target,\n            &serve_broker,\n            stream_a,\n            \"burst-ep\",\n            &origin,\n            0,\n            None,\n            None,\n        )\n        .expect(\"serve\")\n    });\n\n    // The operator must RENDER the full pre-populated ring through the LAST marker.\n    // This is exactly what the deadlock starved to ZERO bytes before the fix.\n    let mut decoder = AttachDecoder::new();\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\n    render_until(\n        &mut operator,\n        stream_b,\n        &mut decoder,\n        &mut cursor,\n        &mut viewport,\n        b\"TICK39\",\n    );\n    assert!(\n        count(&viewport, b\"TICK39\") >= 1,\n        \"the full pre-populated ring replayed + forwarded to the operator (no deadlock)\"\n    );\n\n    operator\n        .net_stream_send(stream_b, &[], None, true)\n        .expect(\"detach\");\n    let _ = server.join().expect(\"serve thread\");\n}\n\n// THE #4×#6 INTERSECTION (REQ-BROKER-SCREEN-GRID × REQ-RC-CROSS-NODE-ATTACH),\n// the merge-danger seam: a CROSS-NODE cold attach BY ENDPOINT (#4's\n// `request_attach_endpoint` → the owning node resolves endpoint→session\n// server-side) to an ALREADY-RUNNING ALT-SCREEN session must receive the #6\n// CLEAN current-screen REPAINT — not the raw ring — end-to-end through\n// `serve_attach`. The child emits main-screen scrollback then enters the alt\n// screen and paints; a fresh endpoint-resolved cold attach (from_seq=0) must land\n// on the controller's legacy-baseline path (which accepts the repaint's forward\n// pseudo-seq without a reject-gap fatal), render the alt viewport, and NOT dump the\n// main-screen scrollback (the exact #6/#7/#8 corruption). Proves the two seams\n// compose over the wire, not just in isolation. Unix-only: the child emits raw VT\n// via `printf` (the repaint path itself is OS-neutral).\n// [int->REQ-BROKER-SCREEN-GRID]\n#[cfg(unix)] // the child emits raw VT via `sh`/`printf`; the repaint path is OS-neutral\n#[test]\nfn cross_node_cold_attach_to_alt_screen_gets_clean_repaint() {\n    let _home = init_home();\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let name = unique_name();\n    let _broker = net_broker(&name, &dir.path().join(\"solo\"));\n\n    // A child that emits main scrollback, ENTERS the alt screen + paints a TUI\n    // viewport, then blocks on `cat` (so the session stays live for the attach).\n    let mut spawner = connect_retry(&name);\n    let sid = spawner\n        .spawn_session(SpawnReq {\n            program: \"sh\".into(),\n            args: vec![\n                \"-c\".into(),\n                // The OSC title before the alt switch: the E-2 leg — a cold\n                // attach must replay the CURRENT title, not drop it (vte's\n                // default) — rides the same round-trip.\n                \"printf '\\\\033]2;E2_TITLE\\\\007MAIN_SCROLLBACK_LINE\\\\n\\\\033[?1049h\\\\033[2J\\\\033[H'; sleep 0.2; printf 'ALT_VIEWPORT_MARKER'; cat\"\n                    .into(),\n            ],\n            rows: 24,\n            cols: 80,\n            endpoint: \"altep\".into(),\n            cwd: None,\n            env: Default::default(),\n            translation_binary: None,\n            adapter: String::new(),\n            install_dir: None,\n        })\n        .expect(\"spawn alt-screen child\");\n    // Drain until the alt-screen paint lands in the ring/grid, then drop the spawn\n    // conn — a fresh attach must repaint the running (alt-screen) session.\n    let mut drained = Vec::new();\n    spawner\n        .read_output_until(b\"ALT_VIEWPORT_MARKER\", &mut drained)\n        .expect(\"the alt-screen paint lands\");\n    drop(spawner);\n\n    // A fresh operator does a CROSS-NODE cold attach BY ENDPOINT (#4): session_id=0\n    // + endpoint_id=Some(\"altep\"); the owning node resolves it to its own session.\n    let mut operator = connect_retry(&name);\n    let dialed = operator.net_dial_loopback().expect(\"loopback dial\");\n    let own_origin = dialed.remote_id_hex.clone();\n    let stream_b = request_attach_endpoint(\n        &mut operator,\n        dialed.conn_id,\n        0, // owning node ignores this — it resolves by endpoint_id\n        0, // from_seq = 0 → COLD attach → the #6 repaint\n        MintedOp::new(Minter::Rc, 2),\n        AttachIntent::Control,\n        Some(\"altep\"),\n        None,\n        false,\n        None,\n    )\n    .expect(\"cross-node request by endpoint\");\n    operator\n        .net_stream_subscribe(stream_b, 0)\n        .expect(\"operator subscribe\");\n\n    let mut target = connect_retry(&name);\n    let (stream_a, origin) = wait_for_stream(&mut target, &[]);\n    assert_eq!(origin, own_origin);\n    let serve_broker = name.clone();\n    // `remote_session = Some(sid)` is exactly what the #4 dispatcher resolves the\n    // endpoint (\"altep\") to BEFORE the serve loop — the cross-node leg overriding\n    // the wire's placeholder session_id (0). Passing it here exercises the\n    // endpoint-resolved cross-node serve path (not the local wire-session path).\n    let server = thread::spawn(move || {\n        serve_attach(\n            &mut target,\n            &serve_broker,\n            stream_a,\n            \"altep\",\n            &origin,\n            0,\n            Some(sid),\n            None,\n        )\n            .expect(\"serve\")\n    });\n\n    // Render the operator's received bytes; the repaint must carry the alt viewport.\n    let mut decoder = AttachDecoder::new();\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\n    render_until(\n        &mut operator,\n        stream_b,\n        &mut decoder,\n        &mut cursor,\n        &mut viewport,\n        b\"ALT_VIEWPORT_MARKER\",\n    );\n    operator\n        .net_stream_send(stream_b, &[], None, true)\n        .expect(\"detach\");\n    let _ = server.join().expect(\"serve thread\");\n\n    // The raw wire carries the alt viewport but NOT the main scrollback…\n    assert!(\n        count(&viewport, b\"ALT_VIEWPORT_MARKER\") >= 1,\n        \"the cross-node cold attach received the alt-screen repaint\"\n    );\n    assert!(\n        count(&viewport, b\"MAIN_SCROLLBACK_LINE\") == 0,\n        \"the main-screen scrollback must NOT ride the cross-node alt repaint (the #6 \\\n         corruption a raw-ring replay would produce)\"\n    );\n    // …the E-2 title replay rides the same repaint (red-first: vte's default\n    // dropped OSC entirely — a cold attach painted a perfect screen with a\n    // stale title)…\n    assert!(\n        count(&viewport, b\"\\x1b]2;E2_TITLE\\x07\") >= 1,\n        \"the child's last-set window title is replayed on the cold attach\"\n    );\n    // …and the authoritative rendered screen agrees.\n    let mut screen = ScreenGrid::new(24, 80);\n    screen.advance(&viewport);\n    let painted = String::from_utf8_lossy(&screen.render_repaint()).into_owned();\n    assert!(\n        painted.contains(\"ALT_VIEWPORT_MARKER\"),\n        \"rendered screen shows the alt viewport across the cross-node path: {painted:?}\"\n    );\n    assert!(\n        !painted.contains(\"MAIN_SCROLLBACK_LINE\"),\n        \"rendered screen has no main-scrollback leak: {painted:?}\"\n    );\n\n    let mut killer = connect_retry(&name);\n    killer.attach(sid, 0).expect(\"attach for cleanup\");\n    killer.kill_session().expect(\"cleanup kill\");\n}\n\n// [int->REQ-HOST-RUN-2] the broker spawns the PTY child in the requested\n// PROJECT cwd: a `SpawnReq.cwd` rides the wire through the broker into the\n// portable-pty CommandBuilder, and a child that prints its cwd reports the dir\n// we asked for — end to end (wire field → broker spawn → PTY cwd). Asserted on\n// the unique tempdir leaf (robust against path canonicalization). The child\n// prints its cwd then blocks (cat/findstr) so the read never races an exit.\n#[test]\nfn broker_spawns_the_pty_child_in_the_requested_cwd() {\n    let _home = init_home();\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let proj = dir.path().join(\"the-project-root\");\n    std::fs::create_dir_all(&proj).expect(\"project dir\");\n…\n}\n\n[Showing lines 431-813 of 2144. Use :814 to continue]"}],"details":{"truncation":{"content":"\r\n// [unit->REQ-INST-8] drive an agent('s session) on machine A from machine B:\r\n// B's keystrokes reach A's real PTY child over the wire (journaled input\r\n// path), the child's output streams back as sequenced records, and B renders\r\n// its own marker echoed remotely. The serve gate sees the handshake-proven\r\n// origin; B detaching ends the viewport with the session still running on A.\r\n#[test]\r\nfn remote_attach_drives_a_real_pty_cross_daemon() {\r\n    let _home = init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let (name_a, name_b) = (unique_name(), unique_name());\r\n    let broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\r\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\r\n\r\n    // Target A: spawn the echo child.\r\n    let mut target = connect_retry(&name_a);\r\n    let sid = target\r\n        .spawn_session(echo_spawn_req())\r\n        .expect(\"spawn echo child\");\r\n    let a_addr = target.net_status().expect(\"a status\").addr;\r\n\r\n    // Operator B: dial A (CROSS-NODE QUIC conn), then drive + detach over the\r\n    // SHARED attach pump. The serve gate sees the QUIC handshake identity.\r\n    let mut operator = connect_retry(&name_b);\r\n    let b_node = operator\r\n        .net_status()\r\n        .expect(\"b status\")\r\n        .node_id_hex\r\n        .expect(\"node id\");\r\n    let conn = operator\r\n        .net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1)))\r\n        .expect(\"dial\");\r\n    let (_viewport, mut target) = attach_drive_detach(\r\n        &mut operator,\r\n        &name_a,\r\n        target,\r\n        conn.conn_id,\r\n        sid,\r\n        \"ling\",\r\n        &b_node,\r\n        b\"REMOTE_DRIVE_OK\",\r\n    );\r\n    assert_eq!(\r\n        broker_a.session_count(),\r\n        1,\r\n        \"the session outlives the viewport\"\r\n    );\r\n    target.kill_session().expect(\"cleanup kill\");\r\n}\r\n\r\n// [int->REQ-RC-1] LOCAL attach rides the byte-identical pump (M12 W1.5, B1):\r\n// a single broker spawns a real PTY session; the operator acquires a\r\n// broker-minted in-process LOOPBACK conn (`net_dial_loopback` — NOT a QUIC\r\n// self-dial, which iroh refuses) and drives the PTY through the SAME\r\n// `serve_attach`+render pump a cross-node operator uses. The proof that \"both\r\n// transports → one pump\" is BY CONSTRUCTION: this test and the cross-node\r\n// `remote_attach_drives_a_real_pty_cross_daemon` call the identical\r\n// `attach_drive_detach` helper; they differ only in how `conn_id` is acquired\r\n// (`net_dial_loopback` vs `net_dial`) and in the expected origin. The loopback\r\n// origin is THIS node's own id, gating same-node (access short-circuit Allow in\r\n// production; default-open in this hermetic home — both Allow). The session\r\n// outlives the viewport.\r\n#[test]\r\nfn local_attach_via_loopback_conn_rides_the_same_pump() {\r\n    let _home = init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let name = unique_name();\r\n    let broker = net_broker(&name, &dir.path().join(\"solo\"));\r\n\r\n    // One broker plays BOTH target and operator (the local case). Spawn the\r\n    // echo child, then attach to it over a loopback conn from a second brain\r\n    // connection to the SAME broker.\r\n    let mut target = connect_retry(&name);\r\n    let sid = target\r\n        .spawn_session(echo_spawn_req())\r\n        .expect(\"spawn echo child\");\r\n\r\n    // The operator acquires the broker's loopback conn; its `remote_id_hex` is\r\n    // this node's OWN broker-minted id — the origin the peer row carries and the\r\n    // serve gates same-node against.\r\n    let mut operator = connect_retry(&name);\r\n    let dialed = operator\r\n        .net_dial_loopback()\r\n        .expect(\"operator loopback dial\");\r\n    let own_origin = dialed.remote_id_hex.clone();\r\n    let (_viewport, mut target) = attach_drive_detach(\r\n        &mut operator,\r\n        &name,\r\n        target,\r\n        dialed.conn_id,\r\n        sid,\r\n        \"ling\",\r\n        &own_origin,\r\n        b\"LOCAL_DRIVE_OK\",\r\n    );\r\n    assert_eq!(\r\n        broker.session_count(),\r\n        1,\r\n        \"the session outlives the local viewport\"\r\n    );\r\n    target.kill_session().expect(\"cleanup kill\");\r\n}\r\n\r\n// [unit->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT] the replay-vs-forward deadlock pin:\r\n// when the session's OutputLog ring is ALREADY NON-EMPTY at attach (an\r\n// already-producing session — exactly the endpoint-run shape, whose transient\r\n// cold-start spawn conn drops after `Spawned`), `become_controller` replays MANY\r\n// frames into the SERVING brain while `serve_attach` must forward each back to the\r\n// operator. Before the two-conn split both rode ONE broker IPC connection, so the\r\n// broker's single-threaded per-conn handler — stuck mid-replay (broker→serve) —\r\n// could not read the serve→broker forwards: mutual backpressure DEADLOCK, operator\r\n// got ZERO bytes. With `serve_attach` forwarding on a SEPARATE conn the full\r\n// populated ring replays and is delivered in order. The prior loopback E2Es only\r\n// ever attached to an idle/just-spawned child (empty ring → 0-frame replay), so\r\n// none exercised this — the regression the suite lacked.\r\n#[test]\r\nfn loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock() {\r\n    let _home = init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let name = unique_name();\r\n    let _broker = net_broker(&name, &dir.path().join(\"solo\"));\r\n\r\n    // A child that emits a BURST of lines (many ring chunks) then blocks on stdin\r\n    // so it never exits before we attach. `TICK39` (the last line) proves the whole\r\n    // populated ring replayed + forwarded in order — not just the first chunk.\r\n    #[cfg(unix)]\r\n    let (program, args) = (\r\n        \"sh\".to_string(),\r\n        vec![\r\n            \"-c\".to_string(),\r\n            \"i=0; while [ $i -lt 39 ]; do echo TICK$i; i=$((i+1)); done; sleep 0.2; echo TICK39; cat\".to_string(),\r\n        ],\r\n    );\r\n    #[cfg(windows)]\r\n    let (program, args) = (\r\n        \"cmd\".to_string(),\r\n        vec![\r\n            \"/c\".to_string(),\r\n            \"(for /L %i in (0,1,38) do @echo TICK%i) & ping -n 2 127.0.0.1 >nul & echo TICK39 & findstr \\\".\\\"\".to_string(),\r\n        ],\r\n    );\r\n\r\n    // The SPAWNING conn drains the burst live into the ring, then we DROP it — so a\r\n    // fresh attach must replay the now non-empty ring (the endpoint-run shape).\r\n    let mut spawner = connect_retry(&name);\r\n    let sid = spawner\r\n        .spawn_session(SpawnReq {\r\n            program,\r\n            args,\r\n            rows: 24,\r\n            cols: 80,\r\n            endpoint: \"burst-ep\".into(),\r\n            cwd: None,\r\n            env: Default::default(),\r\n            translation_binary: None,\r\n            adapter: String::new(),\r\n            install_dir: None,\r\n        })\r\n        .expect(\"spawn burst child\");\r\n    let mut drained = Vec::new();\r\n    spawner\r\n        .read_output_until(b\"TICK39\", &mut drained)\r\n        .expect(\"the burst lands in the ring\");\r\n    drop(spawner); // spawn conn gone — a fresh attach must replay the populated ring\r\n\r\n    // A fresh operator attaches over loopback (the real `spt rc` transport).\r\n    let mut operator = connect_retry(&name);\r\n    let dialed = operator.net_dial_loopback().expect(\"loopback dial\");\r\n    let own_origin = dialed.remote_id_hex.clone();\r\n    let stream_b = request_attach(\r\n        &mut operator,\r\n        dialed.conn_id,\r\n        sid,\r\n        0,\r\n        MintedOp::new(Minter::Rc, 2),\r\n        AttachIntent::Control,\r\n    )\r\n    .expect(\"request\");\r\n    operator\r\n        .net_stream_subscribe(stream_b, 0)\r\n        .expect(\"operator subscribe\");\r\n\r\n    let mut target = connect_retry(&name);\r\n    let (stream_a, origin) = wait_for_stream(&mut target, &[]);\r\n    assert_eq!(origin, own_origin);\r\n    let serve_broker = name.clone();\r\n    let server = thread::spawn(move || {\r\n        serve_attach(\r\n            &mut target,\r\n            &serve_broker,\r\n            stream_a,\r\n            \"burst-ep\",\r\n            &origin,\r\n            0,\r\n            None,\r\n            None,\r\n        )\r\n        .expect(\"serve\")\r\n    });\r\n\r\n    // The operator must RENDER the full pre-populated ring through the LAST marker.\r\n    // This is exactly what the deadlock starved to ZERO bytes before the fix.\r\n    let mut decoder = AttachDecoder::new();\r\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\r\n    render_until(\r\n        &mut operator,\r\n        stream_b,\r\n        &mut decoder,\r\n        &mut cursor,\r\n        &mut viewport,\r\n        b\"TICK39\",\r\n    );\r\n    assert!(\r\n        count(&viewport, b\"TICK39\") >= 1,\r\n        \"the full pre-populated ring replayed + forwarded to the operator (no deadlock)\"\r\n    );\r\n\r\n    operator\r\n        .net_stream_send(stream_b, &[], None, true)\r\n        .expect(\"detach\");\r\n    let _ = server.join().expect(\"serve thread\");\r\n}\r\n\r\n// THE #4×#6 INTERSECTION (REQ-BROKER-SCREEN-GRID × REQ-RC-CROSS-NODE-ATTACH),\r\n// the merge-danger seam: a CROSS-NODE cold attach BY ENDPOINT (#4's\r\n// `request_attach_endpoint` → the owning node resolves endpoint→session\r\n// server-side) to an ALREADY-RUNNING ALT-SCREEN session must receive the #6\r\n// CLEAN current-screen REPAINT — not the raw ring — end-to-end through\r\n// `serve_attach`. The child emits main-screen scrollback then enters the alt\r\n// screen and paints; a fresh endpoint-resolved cold attach (from_seq=0) must land\r\n// on the controller's legacy-baseline path (which accepts the repaint's forward\r\n// pseudo-seq without a reject-gap fatal), render the alt viewport, and NOT dump the\r\n// main-screen scrollback (the exact #6/#7/#8 corruption). Proves the two seams\r\n// compose over the wire, not just in isolation. Unix-only: the child emits raw VT\r\n// via `printf` (the repaint path itself is OS-neutral).\r\n// [int->REQ-BROKER-SCREEN-GRID]\r\n#[cfg(unix)] // the child emits raw VT via `sh`/`printf`; the repaint path is OS-neutral\r\n#[test]\r\nfn cross_node_cold_attach_to_alt_screen_gets_clean_repaint() {\r\n    let _home = init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let name = unique_name();\r\n    let _broker = net_broker(&name, &dir.path().join(\"solo\"));\r\n\r\n    // A child that emits main scrollback, ENTERS the alt screen + paints a TUI\r\n    // viewport, then blocks on `cat` (so the session stays live for the attach).\r\n    let mut spawner = connect_retry(&name);\r\n    let sid = spawner\r\n        .spawn_session(SpawnReq {\r\n            program: \"sh\".into(),\r\n            args: vec![\r\n                \"-c\".into(),\r\n                // The OSC title before the alt switch: the E-2 leg — a cold\r\n                // attach must replay the CURRENT title, not drop it (vte's\r\n                // default) — rides the same round-trip.\r\n                \"printf '\\\\033]2;E2_TITLE\\\\007MAIN_SCROLLBACK_LINE\\\\n\\\\033[?1049h\\\\033[2J\\\\033[H'; sleep 0.2; printf 'ALT_VIEWPORT_MARKER'; cat\"\r\n                    .into(),\r\n            ],\r\n            rows: 24,\r\n            cols: 80,\r\n            endpoint: \"altep\".into(),\r\n            cwd: None,\r\n            env: Default::default(),\r\n            translation_binary: None,\r\n            adapter: String::new(),\r\n            install_dir: None,\r\n        })\r\n        .expect(\"spawn alt-screen child\");\r\n    // Drain until the alt-screen paint lands in the ring/grid, then drop the spawn\r\n    // conn — a fresh attach must repaint the running (alt-screen) session.\r\n    let mut drained = Vec::new();\r\n    spawner\r\n        .read_output_until(b\"ALT_VIEWPORT_MARKER\", &mut drained)\r\n        .expect(\"the alt-screen paint lands\");\r\n    drop(spawner);\r\n\r\n    // A fresh operator does a CROSS-NODE cold attach BY ENDPOINT (#4): session_id=0\r\n    // + endpoint_id=Some(\"altep\"); the owning node resolves it to its own session.\r\n    let mut operator = connect_retry(&name);\r\n    let dialed = operator.net_dial_loopback().expect(\"loopback dial\");\r\n    let own_origin = dialed.remote_id_hex.clone();\r\n    let stream_b = request_attach_endpoint(\r\n        &mut operator,\r\n        dialed.conn_id,\r\n        0, // owning node ignores this — it resolves by endpoint_id\r\n        0, // from_seq = 0 → COLD attach → the #6 repaint\r\n        MintedOp::new(Minter::Rc, 2),\r\n        AttachIntent::Control,\r\n        Some(\"altep\"),\r\n        None,\r\n        false,\r\n        None,\r\n    )\r\n    .expect(\"cross-node request by endpoint\");\r\n    operator\r\n        .net_stream_subscribe(stream_b, 0)\r\n        .expect(\"operator subscribe\");\r\n\r\n    let mut target = connect_retry(&name);\r\n    let (stream_a, origin) = wait_for_stream(&mut target, &[]);\r\n    assert_eq!(origin, own_origin);\r\n    let serve_broker = name.clone();\r\n    // `remote_session = Some(sid)` is exactly what the #4 dispatcher resolves the\r\n    // endpoint (\"altep\") to BEFORE the serve loop — the cross-node leg overriding\r\n    // the wire's placeholder session_id (0). Passing it here exercises the\r\n    // endpoint-resolved cross-node serve path (not the local wire-session path).\r\n    let server = thread::spawn(move || {\r\n        serve_attach(\r\n            &mut target,\r\n            &serve_broker,\r\n            stream_a,\r\n            \"altep\",\r\n            &origin,\r\n            0,\r\n            Some(sid),\r\n            None,\r\n        )\r\n            .expect(\"serve\")\r\n    });\r\n\r\n    // Render the operator's received bytes; the repaint must carry the alt viewport.\r\n    let mut decoder = AttachDecoder::new();\r\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\r\n    render_until(\r\n        &mut operator,\r\n        stream_b,\r\n        &mut decoder,\r\n        &mut cursor,\r\n        &mut viewport,\r\n        b\"ALT_VIEWPORT_MARKER\",\r\n    );\r\n    operator\r\n        .net_stream_send(stream_b, &[], None, true)\r\n        .expect(\"detach\");\r\n    let _ = server.join().expect(\"serve thread\");\r\n\r\n    // The raw wire carries the alt viewport but NOT the main scrollback…\r\n    assert!(\r\n        count(&viewport, b\"ALT_VIEWPORT_MARKER\") >= 1,\r\n        \"the cross-node cold attach received the alt-screen repaint\"\r\n    );\r\n    assert!(\r\n        count(&viewport, b\"MAIN_SCROLLBACK_LINE\") == 0,\r\n        \"the main-screen scrollback must NOT ride the cross-node alt repaint (the #6 \\\r\n         corruption a raw-ring replay would produce)\"\r\n    );\r\n    // …the E-2 title replay rides the same repaint (red-first: vte's default\r\n    // dropped OSC entirely — a cold attach painted a perfect screen with a\r\n    // stale title)…\r\n    assert!(\r\n        count(&viewport, b\"\\x1b]2;E2_TITLE\\x07\") >= 1,\r\n        \"the child's last-set window title is replayed on the cold attach\"\r\n    );\r\n    // …and the authoritative rendered screen agrees.\r\n    let mut screen = ScreenGrid::new(24, 80);\r\n    screen.advance(&viewport);\r\n    let painted = String::from_utf8_lossy(&screen.render_repaint()).into_owned();\r\n    assert!(\r\n        painted.contains(\"ALT_VIEWPORT_MARKER\"),\r\n        \"rendered screen shows the alt viewport across the cross-node path: {painted:?}\"\r\n    );\r\n    assert!(\r\n        !painted.contains(\"MAIN_SCROLLBACK_LINE\"),\r\n        \"rendered screen has no main-scrollback leak: {painted:?}\"\r\n    );\r\n\r\n    let mut killer = connect_retry(&name);\r\n    killer.attach(sid, 0).expect(\"attach for cleanup\");\r\n    killer.kill_session().expect(\"cleanup kill\");\r\n}\r\n\r\n// [int->REQ-HOST-RUN-2] the broker spawns the PTY child in the requested\r\n// PROJECT cwd: a `SpawnReq.cwd` rides the wire through the broker into the\r\n// portable-pty CommandBuilder, and a child that prints its cwd reports the dir\r\n// we asked for — end to end (wire field → broker spawn → PTY cwd). Asserted on\r\n// the unique tempdir leaf (robust against path canonicalization). The child\r\n// prints its cwd then blocks (cat/findstr) so the read never races an exit.\r\n#[test]\r\nfn broker_spawns_the_pty_child_in_the_requested_cwd() {\r\n    let _home = init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let proj = dir.path().join(\"the-project-root\");\r\n    std::fs::create_dir_all(&proj).expect(\"project dir\");\r","truncated":true,"truncatedBy":"lines","totalLines":1714,"totalBytes":15894,"outputLines":383,"outputBytes":15894,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":2144,"displayContent":{"text":"\r\n// [unit->REQ-INST-8] drive an agent('s session) on machine A from machine B:\r\n// B's keystrokes reach A's real PTY child over the wire (journaled input\r\n// path), the child's output streams back as sequenced records, and B renders\r\n// its own marker echoed remotely. The serve gate sees the handshake-proven\r\n// origin; B detaching ends the viewport with the session still running on A.\r\n#[test]\r\nfn remote_attach_drives_a_real_pty_cross_daemon() {\r\n    let _home = init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let (name_a, name_b) = (unique_name(), unique_name());\r\n    let broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\r\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\r\n\r\n    // Target A: spawn the echo child.\r\n    let mut target = connect_retry(&name_a);\r\n    let sid = target\r\n        .spawn_session(echo_spawn_req())\r\n        .expect(\"spawn echo child\");\r\n    let a_addr = target.net_status().expect(\"a status\").addr;\r\n\r\n    // Operator B: dial A (CROSS-NODE QUIC conn), then drive + detach over the\r\n    // SHARED attach pump. The serve gate sees the QUIC handshake identity.\r\n    let mut operator = connect_retry(&name_b);\r\n    let b_node = operator\r\n        .net_status()\r\n        .expect(\"b status\")\r\n        .node_id_hex\r\n        .expect(\"node id\");\r\n    let conn = operator\r\n        .net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1)))\r\n        .expect(\"dial\");\r\n    let (_viewport, mut target) = attach_drive_detach(\r\n        &mut operator,\r\n        &name_a,\r\n        target,\r\n        conn.conn_id,\r\n        sid,\r\n        \"ling\",\r\n        &b_node,\r\n        b\"REMOTE_DRIVE_OK\",\r\n    );\r\n    assert_eq!(\r\n        broker_a.session_count(),\r\n        1,\r\n        \"the session outlives the viewport\"\r\n    );\r\n    target.kill_session().expect(\"cleanup kill\");\r\n}\r\n\r\n// [int->REQ-RC-1] LOCAL attach rides the byte-identical pump (M12 W1.5, B1):\r\n// a single broker spawns a real PTY session; the operator acquires a\r\n// broker-minted in-process LOOPBACK conn (`net_dial_loopback` — NOT a QUIC\r\n// self-dial, which iroh refuses) and drives the PTY through the SAME\r\n// `serve_attach`+render pump a cross-node operator uses. The proof that \"both\r\n// transports → one pump\" is BY CONSTRUCTION: this test and the cross-node\r\n// `remote_attach_drives_a_real_pty_cross_daemon` call the identical\r\n// `attach_drive_detach` helper; they differ only in how `conn_id` is acquired\r\n// (`net_dial_loopback` vs `net_dial`) and in the expected origin. The loopback\r\n// origin is THIS node's own id, gating same-node (access short-circuit Allow in\r\n// production; default-open in this hermetic home — both Allow). The session\r\n// outlives the viewport.\r\n#[test]\r\nfn local_attach_via_loopback_conn_rides_the_same_pump() {\r\n    let _home = init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let name = unique_name();\r\n    let broker = net_broker(&name, &dir.path().join(\"solo\"));\r\n\r\n    // One broker plays BOTH target and operator (the local case). Spawn the\r\n    // echo child, then attach to it over a loopback conn from a second brain\r\n    // connection to the SAME broker.\r\n    let mut target = connect_retry(&name);\r\n    let sid = target\r\n        .spawn_session(echo_spawn_req())\r\n        .expect(\"spawn echo child\");\r\n\r\n    // The operator acquires the broker's loopback conn; its `remote_id_hex` is\r\n    // this node's OWN broker-minted id — the origin the peer row carries and the\r\n    // serve gates same-node against.\r\n    let mut operator = connect_retry(&name);\r\n    let dialed = operator\r\n        .net_dial_loopback()\r\n        .expect(\"operator loopback dial\");\r\n    let own_origin = dialed.remote_id_hex.clone();\r\n    let (_viewport, mut target) = attach_drive_detach(\r\n        &mut operator,\r\n        &name,\r\n        target,\r\n        dialed.conn_id,\r\n        sid,\r\n        \"ling\",\r\n        &own_origin,\r\n        b\"LOCAL_DRIVE_OK\",\r\n    );\r\n    assert_eq!(\r\n        broker.session_count(),\r\n        1,\r\n        \"the session outlives the local viewport\"\r\n    );\r\n    target.kill_session().expect(\"cleanup kill\");\r\n}\r\n\r\n// [unit->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT] the replay-vs-forward deadlock pin:\r\n// when the session's OutputLog ring is ALREADY NON-EMPTY at attach (an\r\n// already-producing session — exactly the endpoint-run shape, whose transient\r\n// cold-start spawn conn drops after `Spawned`), `become_controller` replays MANY\r\n// frames into the SERVING brain while `serve_attach` must forward each back to the\r\n// operator. Before the two-conn split both rode ONE broker IPC connection, so the\r\n// broker's single-threaded per-conn handler — stuck mid-replay (broker→serve) —\r\n// could not read the serve→broker forwards: mutual backpressure DEADLOCK, operator\r\n// got ZERO bytes. With `serve_attach` forwarding on a SEPARATE conn the full\r\n// populated ring replays and is delivered in order. The prior loopback E2Es only\r\n// ever attached to an idle/just-spawned child (empty ring → 0-frame replay), so\r\n// none exercised this — the regression the suite lacked.\r\n#[test]\r\nfn loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock() {\r\n    let _home = init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let name = unique_name();\r\n    let _broker = net_broker(&name, &dir.path().join(\"solo\"));\r\n\r\n    // A child that emits a BURST of lines (many ring chunks) then blocks on stdin\r\n    // so it never exits before we attach. `TICK39` (the last line) proves the whole\r\n    // populated ring replayed + forwarded in order — not just the first chunk.\r\n    #[cfg(unix)]\r\n    let (program, args) = (\r\n        \"sh\".to_string(),\r\n        vec![\r\n            \"-c\".to_string(),\r\n            \"i=0; while [ $i -lt 39 ]; do echo TICK$i; i=$((i+1)); done; sleep 0.2; echo TICK39; cat\".to_string(),\r\n        ],\r\n    );\r\n    #[cfg(windows)]\r\n    let (program, args) = (\r\n        \"cmd\".to_string(),\r\n        vec![\r\n            \"/c\".to_string(),\r\n            \"(for /L %i in (0,1,38) do @echo TICK%i) & ping -n 2 127.0.0.1 >nul & echo TICK39 & findstr \\\".\\\"\".to_string(),\r\n        ],\r\n    );\r\n\r\n    // The SPAWNING conn drains the burst live into the ring, then we DROP it — so a\r\n    // fresh attach must replay the now non-empty ring (the endpoint-run shape).\r\n    let mut spawner = connect_retry(&name);\r\n    let sid = spawner\r\n        .spawn_session(SpawnReq {\r\n            program,\r\n            args,\r\n            rows: 24,\r\n            cols: 80,\r\n            endpoint: \"burst-ep\".into(),\r\n            cwd: None,\r\n            env: Default::default(),\r\n            translation_binary: None,\r\n            adapter: String::new(),\r\n            install_dir: None,\r\n        })\r\n        .expect(\"spawn burst child\");\r\n    let mut drained = Vec::new();\r\n    spawner\r\n        .read_output_until(b\"TICK39\", &mut drained)\r\n        .expect(\"the burst lands in the ring\");\r\n    drop(spawner); // spawn conn gone — a fresh attach must replay the populated ring\r\n\r\n    // A fresh operator attaches over loopback (the real `spt rc` transport).\r\n    let mut operator = connect_retry(&name);\r\n    let dialed = operator.net_dial_loopback().expect(\"loopback dial\");\r\n    let own_origin = dialed.remote_id_hex.clone();\r\n    let stream_b = request_attach(\r\n        &mut operator,\r\n        dialed.conn_id,\r\n        sid,\r\n        0,\r\n        MintedOp::new(Minter::Rc, 2),\r\n        AttachIntent::Control,\r\n    )\r\n    .expect(\"request\");\r\n    operator\r\n        .net_stream_subscribe(stream_b, 0)\r\n        .expect(\"operator subscribe\");\r\n\r\n    let mut target = connect_retry(&name);\r\n    let (stream_a, origin) = wait_for_stream(&mut target, &[]);\r\n    assert_eq!(origin, own_origin);\r\n    let serve_broker = name.clone();\r\n    let server = thread::spawn(move || {\r\n        serve_attach(\r\n            &mut target,\r\n            &serve_broker,\r\n            stream_a,\r\n            \"burst-ep\",\r\n            &origin,\r\n            0,\r\n            None,\r\n            None,\r\n        )\r\n        .expect(\"serve\")\r\n    });\r\n\r\n    // The operator must RENDER the full pre-populated ring through the LAST marker.\r\n    // This is exactly what the deadlock starved to ZERO bytes before the fix.\r\n    let mut decoder = AttachDecoder::new();\r\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\r\n    render_until(\r\n        &mut operator,\r\n        stream_b,\r\n        &mut decoder,\r\n        &mut cursor,\r\n        &mut viewport,\r\n        b\"TICK39\",\r\n    );\r\n    assert!(\r\n        count(&viewport, b\"TICK39\") >= 1,\r\n        \"the full pre-populated ring replayed + forwarded to the operator (no deadlock)\"\r\n    );\r\n\r\n    operator\r\n        .net_stream_send(stream_b, &[], None, true)\r\n        .expect(\"detach\");\r\n    let _ = server.join().expect(\"serve thread\");\r\n}\r\n\r\n// THE #4×#6 INTERSECTION (REQ-BROKER-SCREEN-GRID × REQ-RC-CROSS-NODE-ATTACH),\r\n// the merge-danger seam: a CROSS-NODE cold attach BY ENDPOINT (#4's\r\n// `request_attach_endpoint` → the owning node resolves endpoint→session\r\n// server-side) to an ALREADY-RUNNING ALT-SCREEN session must receive the #6\r\n// CLEAN current-screen REPAINT — not the raw ring — end-to-end through\r\n// `serve_attach`. The child emits main-screen scrollback then enters the alt\r\n// screen and paints; a fresh endpoint-resolved cold attach (from_seq=0) must land\r\n// on the controller's legacy-baseline path (which accepts the repaint's forward\r\n// pseudo-seq without a reject-gap fatal), render the alt viewport, and NOT dump the\r\n// main-screen scrollback (the exact #6/#7/#8 corruption). Proves the two seams\r\n// compose over the wire, not just in isolation. Unix-only: the child emits raw VT\r\n// via `printf` (the repaint path itself is OS-neutral).\r\n// [int->REQ-BROKER-SCREEN-GRID]\r\n#[cfg(unix)] // the child emits raw VT via `sh`/`printf`; the repaint path is OS-neutral\r\n#[test]\r\nfn cross_node_cold_attach_to_alt_screen_gets_clean_repaint() {\r\n    let _home = init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let name = unique_name();\r\n    let _broker = net_broker(&name, &dir.path().join(\"solo\"));\r\n\r\n    // A child that emits main scrollback, ENTERS the alt screen + paints a TUI\r\n    // viewport, then blocks on `cat` (so the session stays live for the attach).\r\n    let mut spawner = connect_retry(&name);\r\n    let sid = spawner\r\n        .spawn_session(SpawnReq {\r\n            program: \"sh\".into(),\r\n            args: vec![\r\n                \"-c\".into(),\r\n                // The OSC title before the alt switch: the E-2 leg — a cold\r\n                // attach must replay the CURRENT title, not drop it (vte's\r\n                // default) — rides the same round-trip.\r\n                \"printf '\\\\033]2;E2_TITLE\\\\007MAIN_SCROLLBACK_LINE\\\\n\\\\033[?1049h\\\\033[2J\\\\033[H'; sleep 0.2; printf 'ALT_VIEWPORT_MARKER'; cat\"\r\n                    .into(),\r\n            ],\r\n            rows: 24,\r\n            cols: 80,\r\n            endpoint: \"altep\".into(),\r\n            cwd: None,\r\n            env: Default::default(),\r\n            translation_binary: None,\r\n            adapter: String::new(),\r\n            install_dir: None,\r\n        })\r\n        .expect(\"spawn alt-screen child\");\r\n    // Drain until the alt-screen paint lands in the ring/grid, then drop the spawn\r\n    // conn — a fresh attach must repaint the running (alt-screen) session.\r\n    let mut drained = Vec::new();\r\n    spawner\r\n        .read_output_until(b\"ALT_VIEWPORT_MARKER\", &mut drained)\r\n        .expect(\"the alt-screen paint lands\");\r\n    drop(spawner);\r\n\r\n    // A fresh operator does a CROSS-NODE cold attach BY ENDPOINT (#4): session_id=0\r\n    // + endpoint_id=Some(\"altep\"); the owning node resolves it to its own session.\r\n    let mut operator = connect_retry(&name);\r\n    let dialed = operator.net_dial_loopback().expect(\"loopback dial\");\r\n    let own_origin = dialed.remote_id_hex.clone();\r\n    let stream_b = request_attach_endpoint(\r\n        &mut operator,\r\n        dialed.conn_id,\r\n        0, // owning node ignores this — it resolves by endpoint_id\r\n        0, // from_seq = 0 → COLD attach → the #6 repaint\r\n        MintedOp::new(Minter::Rc, 2),\r\n        AttachIntent::Control,\r\n        Some(\"altep\"),\r\n        None,\r\n        false,\r\n        None,\r\n    )\r\n    .expect(\"cross-node request by endpoint\");\r\n    operator\r\n        .net_stream_subscribe(stream_b, 0)\r\n        .expect(\"operator subscribe\");\r\n\r\n    let mut target = connect_retry(&name);\r\n    let (stream_a, origin) = wait_for_stream(&mut target, &[]);\r\n    assert_eq!(origin, own_origin);\r\n    let serve_broker = name.clone();\r\n    // `remote_session = Some(sid)` is exactly what the #4 dispatcher resolves the\r\n    // endpoint (\"altep\") to BEFORE the serve loop — the cross-node leg overriding\r\n    // the wire's placeholder session_id (0). Passing it here exercises the\r\n    // endpoint-resolved cross-node serve path (not the local wire-session path).\r\n    let server = thread::spawn(move || {\r\n        serve_attach(\r\n            &mut target,\r\n            &serve_broker,\r\n            stream_a,\r\n            \"altep\",\r\n            &origin,\r\n            0,\r\n            Some(sid),\r\n            None,\r\n        )\r\n            .expect(\"serve\")\r\n    });\r\n\r\n    // Render the operator's received bytes; the repaint must carry the alt viewport.\r\n    let mut decoder = AttachDecoder::new();\r\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\r\n    render_until(\r\n        &mut operator,\r\n        stream_b,\r\n        &mut decoder,\r\n        &mut cursor,\r\n        &mut viewport,\r\n        b\"ALT_VIEWPORT_MARKER\",\r\n    );\r\n    operator\r\n        .net_stream_send(stream_b, &[], None, true)\r\n        .expect(\"detach\");\r\n    let _ = server.join().expect(\"serve thread\");\r\n\r\n    // The raw wire carries the alt viewport but NOT the main scrollback…\r\n    assert!(\r\n        count(&viewport, b\"ALT_VIEWPORT_MARKER\") >= 1,\r\n        \"the cross-node cold attach received the alt-screen repaint\"\r\n    );\r\n    assert!(\r\n        count(&viewport, b\"MAIN_SCROLLBACK_LINE\") == 0,\r\n        \"the main-screen scrollback must NOT ride the cross-node alt repaint (the #6 \\\r\n         corruption a raw-ring replay would produce)\"\r\n    );\r\n    // …the E-2 title replay rides the same repaint (red-first: vte's default\r\n    // dropped OSC entirely — a cold attach painted a perfect screen with a\r\n    // stale title)…\r\n    assert!(\r\n        count(&viewport, b\"\\x1b]2;E2_TITLE\\x07\") >= 1,\r\n        \"the child's last-set window title is replayed on the cold attach\"\r\n    );\r\n    // …and the authoritative rendered screen agrees.\r\n    let mut screen = ScreenGrid::new(24, 80);\r\n    screen.advance(&viewport);\r\n    let painted = String::from_utf8_lossy(&screen.render_repaint()).into_owned();\r\n    assert!(\r\n        painted.contains(\"ALT_VIEWPORT_MARKER\"),\r\n        \"rendered screen shows the alt viewport across the cross-node path: {painted:?}\"\r\n    );\r\n    assert!(\r\n        !painted.contains(\"MAIN_SCROLLBACK_LINE\"),\r\n        \"rendered screen has no main-scrollback leak: {painted:?}\"\r\n    );\r\n\r\n    let mut killer = connect_retry(&name);\r\n    killer.attach(sid, 0).expect(\"attach for cleanup\");\r\n    killer.kill_session().expect(\"cleanup kill\");\r\n}\r\n\r\n// [int->REQ-HOST-RUN-2] the broker spawns the PTY child in the requested\r\n// PROJECT cwd: a `SpawnReq.cwd` rides the wire through the broker into the\r\n// portable-pty CommandBuilder, and a child that prints its cwd reports the dir\r\n// we asked for — end to end (wire field → broker spawn → PTY cwd). Asserted on\r\n// the unique tempdir leaf (robust against path canonicalization). The child\r\n// prints its cwd then blocks (cat/findstr) so the read never races an exit.\r\n#[test]\r\nfn broker_spawns_the_pty_child_in_the_requested_cwd() {\r\n    let _home = init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let proj = dir.path().join(\"the-project-root\");\r\n    std::fs::create_dir_all(&proj).expect(\"project dir\");\r\n…\n}","startLine":431,"lineNumbers":[431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,null,858]},"fileSize":89554,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon\\tests\\attach.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2144,"totalBytes":15894,"outputLines":383,"outputBytes":15894,"shownRange":{"start":431,"end":813},"nextOffset":814}}},"isError":false,"timestamp":1789528541169}}
{"type":"message","id":"7b566f76","parentId":"6fa9cd25","timestamp":"2026-09-16T03:15:41.189Z","message":{"role":"toolResult","toolCallId":"call_6B6MSfCdIkRDu3boCiaLzfX9|fc_0cf46cef591a107c016aaa09dd110087d2927e42ecda1c1a3a","toolName":"read","content":[{"type":"text","text":"//! v0.13.0 P1b (REQ-HAZARD-INPUT-ACK-BACKPRESSURE) — the KEYSTONE, repro-first.\n//!\n//! ## The bug this gate locks down (the applied-ack full-duplex deadlock)\n//!\n//! PRE-FIX, an input FLOOD on ONE brain↔broker connection PERMANENTLY deadlocked\n//! the WHOLE broker. `serve_attach` processes a whole `NetStreamData` batch of N\n//! operator `Input` records in its inner `for rec in decoder.push()` loop, calling\n//! `brain.send_effect(op_id, &bytes)` N times WITHOUT returning to `read_event()`\n//! — so the brain writes N `KIND_INPUT` frames back-to-back and drains NOTHING.\n//! The broker's single-threaded per-conn handler answered EACH `KIND_INPUT` with\n//! `send_frame(applied_envelope)` on the SAME conn (the B5 exactly-once ack). With\n//! the brain not reading, the broker→brain return direction fills (~10 frames =\n//! the IPC pipe buffer) → `send_frame` BLOCKS → the handler stops reading → the\n//! brain's writes block too → mutual full-duplex DEADLOCK. The handler is wedged\n//! forever: a concurrent attach is never serviced, and the original controller's\n//! own detach can never be processed (it stays latched).\n//!\n//! POST-FIX, the fire-and-forward operator path (`serve_attach`'s controller arm)\n//! calls `Brain::send_effect_no_ack` (ack=false) → `dispatch_input` emits NO\n//! applied frame → the broker's per-conn handler NEVER writes back while servicing\n//! the flood → the return direction can't fill → the handler always drains → no\n//! deadlock. Exactly-once is preserved (dedup at the broker's applied-set,\n//! independent of the ack). `shellchan` keeps `send_effect` (ack=true) and its\n//! `Applied`-wait. THIS test asserts the fixed behavior.\n//!\n//! ## Non-vacuousness (how reverting the fix turns this RED)\n//!\n//! Revert `serve_attach`'s controller arm to `brain.send_effect(op_id, &bytes)`\n//! (ack=true) — or drop the `if req.ack` gate in `dispatch_input` so it always\n//! sends the applied frame. Then the FLOOD_N-record flood fills the target\n//! brain↔broker return direction; the per-conn handler wedges on `send_frame`, and\n//! the full-duplex deadlock backs up the flood operator's OWN socket writes so even\n//! the SEND side stalls (`flood_sent=false`) — caught by the 20s flood watchdog, and\n//! assert (1) below flips RED. Every wait is behind a watchdog so the deadlock\n//! surfaces as a clean FAILURE, never a suite hang.\n//!\n//! ## PLATFORM (the banked stance this wedge family uses — W1b / P0)\n//!\n//! The deterministic RED repro is forkpty-only (gravity-linux, cfg(unix)): forkpty's\n//! small kernel pipe buffer fills at the REQ's pinned ~10 frames, so FLOOD_N=64\n//! reliably wedges the pre-fix handler. Windows named pipes buffer far more, so 64\n//! small applied frames do not fill the return direction → the pre-fix wedge is\n//! benign at this N on Windows and the test PASSES (asserting the POSITIVE invariant\n//! the fix guarantees: the flood drains + the broker stays serviceable). A much\n//! larger N (≈2000) WAS verified to force the full-duplex backup even on Windows\n//! (`flood_sent=false` on revert, 2026-06-19), but at that size the loopback net\n//! send-window throttles even the FIXED path in this in-process rig, so 64 is the\n//! clean cross-platform GREEN value and gravity-linux carries the deterministic RED.\n//!\n//! Note on the per-conn liveness oracle: the broker serves each connection on its\n//! OWN handler thread, so a fresh conn's `KIND_SESSIONS` can still answer even while\n//! a DIFFERENT conn's handler is deadlocked (it only blocks if it needs that conn's\n//! held lock). The load-bearing RED signal is therefore `flood_sent` (the deadlocked\n//! conn itself), with `sessions_answered`/`broker_alive` as corroborating \"the rest\n//! of the broker is alive\" oracles.\n//!\n//! ## RUNNER CONTRACT — single-threaded (mirrors inject_control_wedge.rs)\n//!\n//! This test spins a REAL net-backed broker + PTY child under a scoped\n//! `SPT_HOME`. Run it isolated:\n//!   - `cargo nextest run -p spt-daemon --test input_ack_deadlock`  (process-per-test), or\n//!   - `cargo test -p spt-daemon --test input_ack_deadlock -- --test-threads=1`.\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::attach::{request_attach, send_attach_input, send_attach_resize, serve_attach};\nuse spt_daemon::brain::{Brain, BrokerEvent, PumpTrace};\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::effect::{MintedOp, Minter};\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{\n    decode_bytes, SpawnReq, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_SESSIONS,\n    KIND_SESSIONS_REPLY,\n};\nuse spt_daemon::nethost::{NetConfig, NetHost};\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\nuse spt_daemon::Broker;\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\nuse spt_proto::identity::Identity;\nuse spt_test_support::TestHome;\n\n…\nfn input_flood_through_serve_attach_does_not_deadlock_broker() {\n    let _home = init_home();\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let name = unique_name();\n    let endpoint = \"ack-deadlock-ep\";\n    let live_trigger = dir.path().join(\"emit-live-output\");\n\n    let broker = net_broker(&name, &dir.path().join(\"ackdl\"));\n\n    // ── The seeded-then-quiet child: emit ONE marker, then wait on a file trigger.\n    //    The asserted seq wait — never a blind sleep — proves the marker reached\n    //    the broker before the flood starts. During the flood the child neither\n    //    reads nor writes, preserving the clean applied-ack deadlock substrate.\n    let mut spawner = connect_retry(&name);\n    let sid = spawner\n        .spawn_session(seeded_quiet_spawn_req(endpoint, &live_trigger))\n        .expect(\"spawn seeded quiet child\");\n    let seed_deadline = Instant::now() + Duration::from_secs(10);\n    while broker.session_output_seq(sid).unwrap_or(0) == 0 && Instant::now() < seed_deadline {\n        thread::sleep(Duration::from_millis(10));\n    }\n    assert!(\n        broker.session_output_seq(sid).unwrap_or(0) > 0,\n        \"pre-flood ACKDL_OUTPUT seed must be retained before the deadlock substrate starts\"\n    );\n\n    // ── (A) The FLOOD controller: dial loopback, request a CONTROLLER attach, then\n    //    fire FLOOD_N input records back-to-back on the ONE stream WITHOUT reading.\n    //    The target's serve_attach reads them in one decoder.push() batch and calls\n    //    send_effect_no_ack per record (post-fix). The child is now silent: no\n    //    concurrent output can confound the applied-ack deadlock substrate.\n    //\n    //    This whole leg runs on its own thread behind a result channel so a wedge\n    //    (the pre-fix deadlock) surfaces as a watchdog timeout, never a hang. ──\n    #[derive(Debug)]\n    enum FloodVerdict {\n        Sent,\n        SetupFailed(String),\n        SendFailed { op: u64, error: String },\n        WatchdogTimeout,\n        HelperDisconnected,\n    }\n\n    let (flood_tx, flood_rx) = std::sync::mpsc::channel::<FloodVerdict>();\n    let flood_name = name.clone();\n    let flood_ep = endpoint.to_string();\n    let flooder = thread::spawn(move || {\n        // PUMP conn (Split): its background reader continuously DRAINS everything the\n        // broker pushes back on this conn (net-stream protocol frames, control), so a\n        // flood of FLOOD_N input sends that NEVER reads inline cannot self-stall on\n        // its own conn's full-duplex backup — isolating the signal to the TARGET\n        // conn's pre-fix applied-ack deadlock. The attach handshake's request-reply\n        // (net_dial_loopback / net_open_stream) reads from the pump's event queue, so\n        // it works on a pump conn. Each `send_attach_input` is op_id=None (fire-and-\n        // forget, no broker ack wait), so the sends are gated only by the loopback\n        // net send-window — which the pre-fix target deadlock backs up.\n        let mut operator = match Brain::cold_start_pump(\n            &flood_name,\n            1,\n            Duration::from_secs(2),\n            spt_daemon::brain::PumpTrace::Stderr,\n        ) {\n            Ok(b) => b,\n            Err(error) => {\n                let _ = flood_tx.send(FloodVerdict::SetupFailed(format!(\n                    \"cold_start_pump: {error:#}\"\n                )));\n                return;\n            }\n        };\n        let dialed = match operator.net_dial_loopback() {\n            Ok(d) => d,\n            Err(error) => {\n                let _ = flood_tx.send(FloodVerdict::SetupFailed(format!(\n                    \"net_dial_loopback: {error:#}\"\n                )));\n                return;\n            }\n        };\n        let own_origin = dialed.remote_id_hex.clone();\n        // CONTROLLER intent: this is the path the fix changed (the operator/rc\n        // fire-and-forward route → send_effect_no_ack).\n        let stream = match request_attach(\n            &mut operator,\n            dialed.conn_id,\n            sid,\n            0,\n            MintedOp::new(Minter::Rc, 1),\n            AttachIntent::Control,\n        ) {\n            Ok(s) => s,\n            Err(error) => {\n                let _ = flood_tx.send(FloodVerdict::SetupFailed(format!(\n                    \"request_attach: {error:#}\"\n                )));\n                return;\n            }\n        };\n        if let Err(error) = operator.net_stream_subscribe(stream, 0) {\n            let _ = flood_tx.send(FloodVerdict::SetupFailed(format!(\n                \"net_stream_subscribe: {error:#}\"\n            )));\n            return;\n        }\n\n        // The target side serves the inbound attach (the REAL serve_attach pump —\n        // its controller arm drives send_effect_no_ack for every flooded record).\n        let mut target = connect_retry(&flood_name);\n        let (stream_a, origin) = match wait_for_stream(&mut target) {\n            Some(s) => s,\n            None => {\n                let _ = flood_tx.send(FloodVerdict::SetupFailed(\n                    \"wait_for_stream returned no inbound attach\".to_string(),\n                ));\n                return;\n            }\n        };\n        assert_eq!(origin, own_origin, \"loopback origin is this node's own id\");\n        let serve_broker = flood_name.clone();\n        let server = thread::spawn(move || {\n            let _ = serve_attach(\n                &mut target,\n                &serve_broker,\n                stream_a,\n                &flood_ep,\n                &origin,\n                0,\n                None,\n                None,\n            );\n        });\n\n        // THE FLOOD: FLOOD_N input records, back-to-back, NO reads in between — the\n        // exact \"whole batch without returning to read_event\" shape serve_attach\n        // processes. Pre-fix the per-record applied frames fill the target brain's\n        // return direction and wedge the broker's per-conn handler.\n        for op in 1..=FLOOD_N {\n            let line = format!(\"FLOODINPUT-{op}\\r\");\n            if let Err(error) =\n                send_attach_input(&mut operator, stream, line.as_bytes(), op)\n            {\n                let _ = flood_tx.send(FloodVerdict::SendFailed {\n                    op,\n                    error: format!(\"{error:#}\"),\n                });\n                return;\n            }\n        }\n        let _ = flood_tx.send(FloodVerdict::Sent);\n\n        // Detach: close the send side (best-effort — the broker's draining handler\n        // processes it and releases the controller seat). Then ABANDON the conn +\n        // serve thread WITHOUT a drain loop: this Whole carrier supports only an\n        // unbounded read; asking it for a deadline is refused by name. An unbounded\n        // drain could hang the helper. The main thread's child-kill + broker drop\n        // unwinds the abandoned serve thread (serve threads are abandoned by design\n        // here — never block the gate on one).\n        let _ = operator.net_stream_send(stream, &[], None, true);\n        drop(operator);\n        drop(server);\n    });\n\n    // Watchdog the flood itself: all FLOOD_N records must SEND within the deadline.\n    // Pre-fix the operator's own socket writes back up once the broker's handler\n    // wedges (mutual full-duplex), so even the SEND side stalls — caught here.\n    let flood_verdict = match flood_rx.recv_timeout(Duration::from_secs(20)) {\n        Ok(verdict) => verdict,\n        Err(std::sync::mpsc::RecvTimeoutError::Timeout) => FloodVerdict::WatchdogTimeout,\n        Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {\n            FloodVerdict::HelperDisconnected\n        }\n    };\n    let (flood_sent, flood_detail) = match &flood_verdict {\n        FloodVerdict::Sent => (true, \"Sent\".to_string()),\n        FloodVerdict::SetupFailed(error) => (false, format!(\"SetupFailed: {error}\")),\n        FloodVerdict::SendFailed { op, error } => {\n            (false, format!(\"SendFailed at op {op}: {error}\"))\n        }\n        FloodVerdict::WatchdogTimeout => (false, \"WatchdogTimeout\".to_string()),\n        FloodVerdict::HelperDisconnected => (false, \"HelperDisconnected\".to_string()),\n    };\n…\n}"}],"details":{"displayContent":{"text":"//! v0.13.0 P1b (REQ-HAZARD-INPUT-ACK-BACKPRESSURE) — the KEYSTONE, repro-first.\n//!\n//! ## The bug this gate locks down (the applied-ack full-duplex deadlock)\n//!\n//! PRE-FIX, an input FLOOD on ONE brain↔broker connection PERMANENTLY deadlocked\n//! the WHOLE broker. `serve_attach` processes a whole `NetStreamData` batch of N\n//! operator `Input` records in its inner `for rec in decoder.push()` loop, calling\n//! `brain.send_effect(op_id, &bytes)` N times WITHOUT returning to `read_event()`\n//! — so the brain writes N `KIND_INPUT` frames back-to-back and drains NOTHING.\n//! The broker's single-threaded per-conn handler answered EACH `KIND_INPUT` with\n//! `send_frame(applied_envelope)` on the SAME conn (the B5 exactly-once ack). With\n//! the brain not reading, the broker→brain return direction fills (~10 frames =\n//! the IPC pipe buffer) → `send_frame` BLOCKS → the handler stops reading → the\n//! brain's writes block too → mutual full-duplex DEADLOCK. The handler is wedged\n//! forever: a concurrent attach is never serviced, and the original controller's\n//! own detach can never be processed (it stays latched).\n//!\n//! POST-FIX, the fire-and-forward operator path (`serve_attach`'s controller arm)\n//! calls `Brain::send_effect_no_ack` (ack=false) → `dispatch_input` emits NO\n//! applied frame → the broker's per-conn handler NEVER writes back while servicing\n//! the flood → the return direction can't fill → the handler always drains → no\n//! deadlock. Exactly-once is preserved (dedup at the broker's applied-set,\n//! independent of the ack). `shellchan` keeps `send_effect` (ack=true) and its\n//! `Applied`-wait. THIS test asserts the fixed behavior.\n//!\n//! ## Non-vacuousness (how reverting the fix turns this RED)\n//!\n//! Revert `serve_attach`'s controller arm to `brain.send_effect(op_id, &bytes)`\n//! (ack=true) — or drop the `if req.ack` gate in `dispatch_input` so it always\n//! sends the applied frame. Then the FLOOD_N-record flood fills the target\n//! brain↔broker return direction; the per-conn handler wedges on `send_frame`, and\n//! the full-duplex deadlock backs up the flood operator's OWN socket writes so even\n//! the SEND side stalls (`flood_sent=false`) — caught by the 20s flood watchdog, and\n//! assert (1) below flips RED. Every wait is behind a watchdog so the deadlock\n//! surfaces as a clean FAILURE, never a suite hang.\n//!\n//! ## PLATFORM (the banked stance this wedge family uses — W1b / P0)\n//!\n//! The deterministic RED repro is forkpty-only (gravity-linux, cfg(unix)): forkpty's\n//! small kernel pipe buffer fills at the REQ's pinned ~10 frames, so FLOOD_N=64\n//! reliably wedges the pre-fix handler. Windows named pipes buffer far more, so 64\n//! small applied frames do not fill the return direction → the pre-fix wedge is\n//! benign at this N on Windows and the test PASSES (asserting the POSITIVE invariant\n//! the fix guarantees: the flood drains + the broker stays serviceable). A much\n//! larger N (≈2000) WAS verified to force the full-duplex backup even on Windows\n//! (`flood_sent=false` on revert, 2026-06-19), but at that size the loopback net\n//! send-window throttles even the FIXED path in this in-process rig, so 64 is the\n//! clean cross-platform GREEN value and gravity-linux carries the deterministic RED.\n//!\n//! Note on the per-conn liveness oracle: the broker serves each connection on its\n//! OWN handler thread, so a fresh conn's `KIND_SESSIONS` can still answer even while\n//! a DIFFERENT conn's handler is deadlocked (it only blocks if it needs that conn's\n//! held lock). The load-bearing RED signal is therefore `flood_sent` (the deadlocked\n//! conn itself), with `sessions_answered`/`broker_alive` as corroborating \"the rest\n//! of the broker is alive\" oracles.\n//!\n//! ## RUNNER CONTRACT — single-threaded (mirrors inject_control_wedge.rs)\n//!\n//! This test spins a REAL net-backed broker + PTY child under a scoped\n//! `SPT_HOME`. Run it isolated:\n//!   - `cargo nextest run -p spt-daemon --test input_ack_deadlock`  (process-per-test), or\n//!   - `cargo test -p spt-daemon --test input_ack_deadlock -- --test-threads=1`.\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::attach::{request_attach, send_attach_input, send_attach_resize, serve_attach};\nuse spt_daemon::brain::{Brain, BrokerEvent, PumpTrace};\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::effect::{MintedOp, Minter};\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{\n    decode_bytes, SpawnReq, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_SESSIONS,\n    KIND_SESSIONS_REPLY,\n};\nuse spt_daemon::nethost::{NetConfig, NetHost};\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\nuse spt_daemon::Broker;\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\nuse spt_proto::identity::Identity;\nuse spt_test_support::TestHome;\n\n…\nfn input_flood_through_serve_attach_does_not_deadlock_broker() {\n    let _home = init_home();\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let name = unique_name();\n    let endpoint = \"ack-deadlock-ep\";\n    let live_trigger = dir.path().join(\"emit-live-output\");\n\n    let broker = net_broker(&name, &dir.path().join(\"ackdl\"));\n\n    // ── The seeded-then-quiet child: emit ONE marker, then wait on a file trigger.\n    //    The asserted seq wait — never a blind sleep — proves the marker reached\n    //    the broker before the flood starts. During the flood the child neither\n    //    reads nor writes, preserving the clean applied-ack deadlock substrate.\n    let mut spawner = connect_retry(&name);\n    let sid = spawner\n        .spawn_session(seeded_quiet_spawn_req(endpoint, &live_trigger))\n        .expect(\"spawn seeded quiet child\");\n    let seed_deadline = Instant::now() + Duration::from_secs(10);\n    while broker.session_output_seq(sid).unwrap_or(0) == 0 && Instant::now() < seed_deadline {\n        thread::sleep(Duration::from_millis(10));\n    }\n    assert!(\n        broker.session_output_seq(sid).unwrap_or(0) > 0,\n        \"pre-flood ACKDL_OUTPUT seed must be retained before the deadlock substrate starts\"\n    );\n\n    // ── (A) The FLOOD controller: dial loopback, request a CONTROLLER attach, then\n    //    fire FLOOD_N input records back-to-back on the ONE stream WITHOUT reading.\n    //    The target's serve_attach reads them in one decoder.push() batch and calls\n    //    send_effect_no_ack per record (post-fix). The child is now silent: no\n    //    concurrent output can confound the applied-ack deadlock substrate.\n    //\n    //    This whole leg runs on its own thread behind a result channel so a wedge\n    //    (the pre-fix deadlock) surfaces as a watchdog timeout, never a hang. ──\n    #[derive(Debug)]\n    enum FloodVerdict {\n        Sent,\n        SetupFailed(String),\n        SendFailed { op: u64, error: String },\n        WatchdogTimeout,\n        HelperDisconnected,\n    }\n\n    let (flood_tx, flood_rx) = std::sync::mpsc::channel::<FloodVerdict>();\n    let flood_name = name.clone();\n    let flood_ep = endpoint.to_string();\n    let flooder = thread::spawn(move || {\n        // PUMP conn (Split): its background reader continuously DRAINS everything the\n        // broker pushes back on this conn (net-stream protocol frames, control), so a\n        // flood of FLOOD_N input sends that NEVER reads inline cannot self-stall on\n        // its own conn's full-duplex backup — isolating the signal to the TARGET\n        // conn's pre-fix applied-ack deadlock. The attach handshake's request-reply\n        // (net_dial_loopback / net_open_stream) reads from the pump's event queue, so\n        // it works on a pump conn. Each `send_attach_input` is op_id=None (fire-and-\n        // forget, no broker ack wait), so the sends are gated only by the loopback\n        // net send-window — which the pre-fix target deadlock backs up.\n        let mut operator = match Brain::cold_start_pump(\n            &flood_name,\n            1,\n            Duration::from_secs(2),\n            spt_daemon::brain::PumpTrace::Stderr,\n        ) {\n            Ok(b) => b,\n            Err(error) => {\n                let _ = flood_tx.send(FloodVerdict::SetupFailed(format!(\n                    \"cold_start_pump: {error:#}\"\n                )));\n                return;\n            }\n        };\n        let dialed = match operator.net_dial_loopback() {\n            Ok(d) => d,\n            Err(error) => {\n                let _ = flood_tx.send(FloodVerdict::SetupFailed(format!(\n                    \"net_dial_loopback: {error:#}\"\n                )));\n                return;\n            }\n        };\n        let own_origin = dialed.remote_id_hex.clone();\n        // CONTROLLER intent: this is the path the fix changed (the operator/rc\n        // fire-and-forward route → send_effect_no_ack).\n        let stream = match request_attach(\n            &mut operator,\n            dialed.conn_id,\n            sid,\n            0,\n            MintedOp::new(Minter::Rc, 1),\n            AttachIntent::Control,\n        ) {\n            Ok(s) => s,\n            Err(error) => {\n                let _ = flood_tx.send(FloodVerdict::SetupFailed(format!(\n                    \"request_attach: {error:#}\"\n                )));\n                return;\n            }\n        };\n        if let Err(error) = operator.net_stream_subscribe(stream, 0) {\n            let _ = flood_tx.send(FloodVerdict::SetupFailed(format!(\n                \"net_stream_subscribe: {error:#}\"\n            )));\n            return;\n        }\n\n        // The target side serves the inbound attach (the REAL serve_attach pump —\n        // its controller arm drives send_effect_no_ack for every flooded record).\n        let mut target = connect_retry(&flood_name);\n        let (stream_a, origin) = match wait_for_stream(&mut target) {\n            Some(s) => s,\n            None => {\n                let _ = flood_tx.send(FloodVerdict::SetupFailed(\n                    \"wait_for_stream returned no inbound attach\".to_string(),\n                ));\n                return;\n            }\n        };\n        assert_eq!(origin, own_origin, \"loopback origin is this node's own id\");\n        let serve_broker = flood_name.clone();\n        let server = thread::spawn(move || {\n            let _ = serve_attach(\n                &mut target,\n                &serve_broker,\n                stream_a,\n                &flood_ep,\n                &origin,\n                0,\n                None,\n                None,\n            );\n        });\n\n        // THE FLOOD: FLOOD_N input records, back-to-back, NO reads in between — the\n        // exact \"whole batch without returning to read_event\" shape serve_attach\n        // processes. Pre-fix the per-record applied frames fill the target brain's\n        // return direction and wedge the broker's per-conn handler.\n        for op in 1..=FLOOD_N {\n            let line = format!(\"FLOODINPUT-{op}\\r\");\n            if let Err(error) =\n                send_attach_input(&mut operator, stream, line.as_bytes(), op)\n            {\n                let _ = flood_tx.send(FloodVerdict::SendFailed {\n                    op,\n                    error: format!(\"{error:#}\"),\n                });\n                return;\n            }\n        }\n        let _ = flood_tx.send(FloodVerdict::Sent);\n\n        // Detach: close the send side (best-effort — the broker's draining handler\n        // processes it and releases the controller seat). Then ABANDON the conn +\n        // serve thread WITHOUT a drain loop: this Whole carrier supports only an\n        // unbounded read; asking it for a deadline is refused by name. An unbounded\n        // drain could hang the helper. The main thread's child-kill + broker drop\n        // unwinds the abandoned serve thread (serve threads are abandoned by design\n        // here — never block the gate on one).\n        let _ = operator.net_stream_send(stream, &[], None, true);\n        drop(operator);\n        drop(server);\n    });\n\n    // Watchdog the flood itself: all FLOOD_N records must SEND within the deadline.\n    // Pre-fix the operator's own socket writes back up once the broker's handler\n    // wedges (mutual full-duplex), so even the SEND side stalls — caught here.\n    let flood_verdict = match flood_rx.recv_timeout(Duration::from_secs(20)) {\n        Ok(verdict) => verdict,\n        Err(std::sync::mpsc::RecvTimeoutError::Timeout) => FloodVerdict::WatchdogTimeout,\n        Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {\n            FloodVerdict::HelperDisconnected\n        }\n    };\n    let (flood_sent, flood_detail) = match &flood_verdict {\n        FloodVerdict::Sent => (true, \"Sent\".to_string()),\n        FloodVerdict::SetupFailed(error) => (false, format!(\"SetupFailed: {error}\")),\n        FloodVerdict::SendFailed { op, error } => {\n            (false, format!(\"SendFailed at op {op}: {error}\"))\n        }\n        FloodVerdict::WatchdogTimeout => (false, \"WatchdogTimeout\".to_string()),\n        FloodVerdict::HelperDisconnected => (false, \"HelperDisconnected\".to_string()),\n    };\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,null,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,null,706]},"fileSize":33635,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon\\tests\\input_ack_deadlock.rs"}}},"isError":false,"timestamp":1789528541188}}
{"type":"message","id":"bb82125b","parentId":"7b566f76","timestamp":"2026-09-16T03:15:41.209Z","message":{"role":"toolResult","toolCallId":"call_M9QYnxQebEeuOT2cZGwFx52y|fc_0cf46cef591a107c016aaa09dd111c87d2a7398ed0f1b034bf","toolName":"grep","content":[{"type":"text","text":"# .worktrees/49-267-refresh-discriminator/\n## traceable-reqs.toml\n 1174|id = \"REQ-HAZARD-ROLLBACK-STATE-COMPAT\"\n*1175|title = \"A brain must not irreversibly migrate durable state before update ready-promotion: the readiness-gated auto-rollback (ADR-0018 Q7) spawns the N-1 binary against durable state the new brain may have written, so every pre-ready write must stay N-1-readable (schema migrations gated behind ready-promotion, or written N-1-tolerant/additive). Else the first in-place schema migration silently bricks rollback (KNOWN-HAZARDS 6.8). Free now — a 2026-06-09 audit confirmed zero state-migration code exists...\n 1176|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc activated restoration D1 (KNOWN-HAZARDS 6.8 + ADR-0018 + design doc V1). impl/unit activated restoration D6-3 (the one D6 toml activation): impl = rollback_compat.rs (the pre-ready durable-file registry PRE_READY_DURABLE_FILES — the enumerated guard surface) + the two-phase AppliedRecord (relcache.rs, additive durable state); unit = the rollback_compat tripwire (every current pre-ready file shape is additive / N-1-readable; a non-additive pre-ready chang...\n 1177|# --- Added 2026-06-11 (v0.4.2): the v0.4.1 fleet-roll Linux brain-respawn blocker (ADR-0018 Q3 amendment); rule 3 register-before-satisfy, rule 5 activate per task ---\n 1178|[[requirements]]\n 1179|id = \"REQ-HAZARD-BRAIN-RESPAWN-PATH\"\n*1180|title = \"The broker respawns the brain onto the APPLIED bytes, not the renamed old binary: the candidate-binary default is the canonical exe path captured ONCE at broker start, never a per-spawn std::env::current_exe() — on Linux current_exe (readlink /proc/self/exe) is inode-tracking and follows the `apply` rename (spt -> spt.old-N), so a resident broker would respawn the brain onto OLD bytes while recording `applied` (Windows GetModuleFileName is path-at-start, so Windows was green; ADR-0018 Q3 silen...\n*1181|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # activated v0.4.2 (2026-06-11): minted from the v0.4.1 fleet-roll blocker. doc = KNOWN-HAZARDS 6.11 + ADR-0018 Q3 amendment; int = brain_respawn_rename.rs (real broker process, in-place rename P->P.old-N + new bytes at P, respawn must run B not A — fails Linux pre-fix). Stages: impl = brainproc.rs canonical-exe capture at spawn_brain_supervisor + promotion bytes-gate in supervise_brain Promoted arm (TrialEnv ready_exe_hash/staged_artifact_hash); unit =...\n 1182|# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---\n 1183|[[requirements]]\n 1184|id = \"REQ-HAZARD-PSYCHE-OUTBOUND-PROXY\"\n...\n 1579|title = \"In an `spt rc` session neither ctrl+V nor right-click pastes (CC explicitly supports ctrl+V). ROOT (doyle /diagnose): RawGuard does only enable_raw_mode (no bracketed paste / no mouse capture / no clipboard interception); the Windows console delivers a paste as synthetic per-char KEY EVENTs (no crossterm Event::Paste), and ctrl+V translates to bare ^V forwarded to CC — but CC runs DAEMON-SIDE with NO access to the operator's LOCAL clipboard, so remote paste is fundamentally CLIENT-ORIGINATED. ...\n*1580|required_stages = [\"doc\", \"impl\", \"unit\"]  # ACTIVATED v0.13.0 P1 (todlando, last build item; doyle gates). DEPENDS ON P0 (@0675b37, landed) — a paste chunk rides P0's single-writer thread without wedging. NO int (live clipboard + console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent; pure pieces unit-tested). impl = (cfg(windows), folds into the bug-2 event path) `wrap_bracketed_paste` (ESC[200~ + content + ESC[201~) + ctrl+V intercept (Char('v')+CONTROL, when not detach-armed) + right-mouse paste in `s...\n 1581|\n 1582|# ── P1b (operator HITL re-open 2026-06-19, doyle /diagnose on the enhanced rc+broker capture): an input FLOOD deadlocks the broker via the applied-ack ──\n 1583|[[requirements]]\n*1584|id = \"REQ-HAZARD-INPUT-ACK-BACKPRESSURE\"\n 1585|title = \"A FLOOD of operator input on one brain↔broker connection deadlocks the broker PERMANENTLY (entire broker — no new/existing attach; the controller stays latched because the per-conn handler can't process the detach). ROOT (doyle /diagnose, code-grounded + HITL capture, the v0.13.0 P1 ctrl+V re-open): `serve_attach` processes a whole `NetStreamData` batch of N operator `Input` records in its inner `for rec in decoder.push()` loop, calling `brain.send_effect(op_id, &bytes)` N times WITHOUT retu...\n 1586|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED v0.13.0 P1b (todlando, the real ship-blocker behind the ctrl+V wedge; doyle gates). doc = KNOWN-HAZARDS 7.19 (the deadlock + the N-1 broker-resident caveat). impl = InputReq.ack (serde default_true) + Brain::send_effect_no_ack (send_effect_inner shared body) + serve_attach operator path -> send_effect_no_ack + dispatch_input gates send_frame(applied) on req.ack. unit = InputReq.ack serde default=true on an absent field (N-1) + dispatch_input e...\n 1587|\n...\n 2347|title = \"W2 (LIFECYCLE-TRUTH, KNOWN-HAZARDS, flagship — the update wedge): PTY viewer fan-out and control mutations must not depend synchronously on a live draining brain. ROOT rig-CONFIRMED (NtSuspendProcess on the brain, no update involved): brain-subscriber session-output writes ride UNDER the per-session log lock (broker.rs:19-20); failed writes are handled (cursor freeze + detach :3486) but BLOCKED writes are not. Suspended brain => within seconds attached rc output freezes; detach does NOT releas...\n*2348|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED LIFECYCLE-TRUTH W2 (todlando 2026-07-07) per doyle ruling docs/W2-DESIGN-RULING.md @60ec0ed (minimal-plus scope). doc = KNOWN-HAZARDS entry (\"PTY fan-out + broker control plane never block on any single subscriber conn — bounded sinks everywhere incl. the brain conn; brain.ready != subscribers drained\"). impl = (a) bounded brain-conn sink (writer thread + bounded queue + BRAIN_WRITE_DEADLINE=15s deadline-evict, full-with-zero-progress last_o...\n 2349|\n 2350|[[requirements]]\n 2351|id = \"REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL\"\n...\n 2376|id = \"REQ-UPDATE-PROMOTE-DRAINED\"\n*2377|title = \"W3 (LIFECYCLE-TRUTH, mechanic-d MOVED FROM W2 per doyle gate verdict @e5ae7a9 — binding): the update-apply brain-generation promotion completes only when the OLD generation's broker subscriber connection is CLOSED or stall-EVICTED — never while blocked writes still pend on it. ROOT: `brain.ready` != subscribers drained; W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-promote window to BRAIN_WRITE_DEADLINE (15s), it does NOT close it — a new brain can signal...\n 2378|required_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED LIFECYCLE-TRUTH W3 drained-condition slice (todlando 2026-07-07, work-start); brainproc.rs run_trial DRAINED gate + broker any_local_controller_wedged truth + false-promote int rig — closes the W2 15s residual (KH 7.36)\n 2379|\n 2380|[[requirements]]\n 2381|id = \"REQ-UPDATE-TRIAL-DRAIN-DRIVE\"\n*2382|title = \"UPDATE-WEDGE (counter-54, doyle-ruled 2026-07-09 — regression of the v0.29.0 seamless brain-swap): a brain generation DRIVES the broker's controller-liveness reap (a KIND_SESSIONS poll) each heartbeat throughout its boot/trial loop, so a hard-KILLED prior generation's black-holed LOCAL controller conn (by:None) is stall-evicted within the trial window and can never permanently strand the promotion DRAINED gate. ROOT (2026-07-09 field freeze, `spt update fetch --apply` v0.30.0->v0.30.2 froze al...\n 2383|required_stages = [\"doc\", \"impl\", \"int\"]  # ACTIVATED counter-54 UPDATE-WEDGE fix (doyle ruled brain-side 2026-07-09; todlando builds). doc = a CONTEXT.md/LIFECYCLE note that the trial/boot brain drives controller-reap so a killed prior generation cannot strand old_gen_drained. impl = the brain.sessions() KIND_SESSIONS drive in run_brain's heartbeat loop (brainproc.rs, first-tick + every tick, trial + recovery brain). UNIT DELIBERATELY DROPPED (doyle FORK-2 ruling 2026-07-09): the impl is ONE uncondition...\n 2384|\n 2385|[[requirements]]\n 2386|id = \"REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE\"\n*2387|title = \"SEED (DEFERRED, doyle 2026-07-09 — post-counter-54 root-hardening for UPDATE-WEDGE; mint now, impl a FUTURE milestone): on a PLANNED brain-restart (`BRAIN_UPDATE_RESTART`, the seamless update-apply brain-cycle), the outgoing brain's LOCAL (by:None) controller conns are GRACEFULLY CLEAN-CLOSED as the brain is cycled, instead of hard-killed and left to black-hole. ROOT (field-pinned 2026-07-09, daemon.stderr.log L24277-24303): the update-restart path hard-kills the outgoing brain (`child.kill()`...\n 2388|required_stages = []  # SEED minted DEFERRED (doyle 2026-07-09); activate post-counter-54 at the milestone that delivers HITCH-FREE seamless updates. Supersedes the livehost-reattach 'Fix Y' framing (wrong site). The counter-54 fix (REQ-UPDATE-TRIAL-DRAIN-DRIVE reap-drive) makes updates SELF-HEAL in ~15s; this seed removes the residual hitch.\n 2389|\n 2390|[[requirements]]\n...\n 2396|id = \"REQ-BRAIN-RESUME-NO-CONTROL-STEAL\"\n*2397|title = \"UPDATE-WEDGE round 2 (v0.30.4, doyle-ruled 2026-07-09 — field incident on the counter-54 fetch--apply): a brain-respawn must NEVER steal, then stall-evict, the controller of a broker PTY session the daemon brain does not DRIVE. ROOT (field-pinned + SME, docs/UPDATE-WEDGE-2-RCA.md + docs/UPDATE-WEDGE-2-SME-todlando.md): `resume_sessions` (brain.rs:985) re-attaches EVERY session `KIND_SESSIONS` returns via `subscribe` = `subscribe_with(AttachIntent::Control, by:None)` (brain.rs:1450-1455). The d...\n 2398|required_stages = [\"doc\", \"impl\", \"int\"]  # ACTIVATED v0.30.4 UPDATE-WEDGE-2 fix (doyle ruled 2026-07-09; todlando builds). doc = brain.rs resume docstring corrected (kill the by:None-never-steals MYTH) + a CONTEXT/LIFECYCLE note that a brain-respawn re-attaches non-driven sessions as viewer. impl = resume path re-attaches AttachIntent::Viewer (Control only for genuinely-driven sessions, empty today). unit DROPPED if the impl is a branchless intent-flip (same FORK-2 reasoning as REQ-UPDATE-TRIAL-DRAIN-DR...\n 2399|\n 2400|[[requirements]]\n...\n 2649|[[requirements]]\n*2650|id = \"REQ-DAEMON-REFRESH\"\n 2651|title = \"THE-FORKENING W4 (operator add 2026-07-14): `spt daemon refresh` — restart the daemon BRAIN without a binary swap and WITHOUT touching the broker: exactly the apply_staged brain-cycle path (brain stop -> respawn -> readiness trial -> promote, incl. the trial-drain drive REQ-UPDATE-TRIAL-DRAIN-DRIVE and viewer-only resume REQ-BRAIN-RESUME-NO-CONTROL-STEAL) minus the swap. Recovery verb for wedged brain-held state (field motivator 2026-07-14: endpoint bringup broken on a live daemon + deployah d...\n 2652|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # FLIPPED in the W4 build commit (todlando 2026-07-15).\n 2653|\n...\n 2734|[[requirements]]\n*2735|id = \"REQ-HAZARD-REDISPATCH-CONTROL-STEAL\"\n*2736|title = \"REDISPATCH-TRUTH W1 (KNOWN-HAZARDS 7.41, hertz field RCA 2026-07-16 — 4/5 endpoints frozen per brain cycle): a fresh dispatcher must NEVER re-serve a terminal stream — a replayed historical Attach must not steal (same-identity silent become_controller, no Displaced) or clear (replayed-EOF detach_session) a LIVE controller. The legitimate same-by successor re-take after a brain restart still silently re-takes: the discriminator is stream LIFECYCLE, never origin identity. Gate: int — product...\n 2737|required_stages = [\"doc\", \"int\"]  # ACTIVATED REDISPATCH-TRUTH W1 (todlando 2026-07-16). doc = KNOWN-HAZARDS 7.41; int = redispatch.rs D1 (Served-retired historical attach) + D1b gate-round-1 (finished-UNRETIRED orderings: kill gen1 mid-serve THEN detach — pre-existing-backlog/brain-down-detach shape; gen2 skips + retires-on-sight, current controller unharmed across multi-second polls) — both production run_dispatch_loop via killable dispatch_fixture generations. HEAVY at birth, all three strings.\n 2738|\n 2739|[[requirements]]\n*2740|id = \"REQ-REDISPATCH-FINISHED-RETIRE\"\n*2741|title = \"REDISPATCH-TRUTH W1 (ADR-0038, hertz fix A): finished/terminal stream rows are RETIRED from redispatch eligibility — NetShared.streams today has NO removal path (single insert nethost.rs ~649; StreamLog::finish only marks) so every dispatcher generation re-enumerates every historical stream forever. Retire terminal rows from the enumeration the dispatcher claims from (remove, or lifecycle-exclude), preserving only the post-EOF state genuinely needed by other readers (presence/log reads); bound...\n 2742|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-TRUTH W1 (todlando 2026-07-16; int ADDED gate round 1; RULING gate round 2: retirement is ELIGIBILITY-VISIBILITY, never teardown — the \"lifecycle-exclude\" arm of the title, remove is REJECTED: the worker's reply send is fire-and-forget, so remove-on-retire races the still-queued reply inside the broker and resets the never-finished send half = requester torn-EOF/NoReply, proven 30/150 vs 0/150 A/B on kitsubito; physical teardown b...\n 2743|\n 2744|[[requirements]]\n 2745|id = \"REQ-STREAM-OPENER-DURABLE\"\n*2746|title = \"REDISPATCH-TRUTH W1 (ADR-0038, hertz fix B): stream classification identity is RESTART-DURABLE and independent of the evictable data ring — today reconstruction classifies via peek_first_line from ring seq 0, but StreamLog is a bounded 4096-transport-chunk ring (DEFAULT_STREAM_RING_CHUNKS, nethost.rs:111), so a high-traffic ACTIVE stream's opener/Request record evicts and replay classifies Unknown/Failed = active stream permanently abandoned. Pin an immutable bounded opener/classification fact...\n 2747|required_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-TRUTH W1 (todlando 2026-07-16). impl = StreamLog OpenerCapture (pinned outside the ring, bounded hunt) + net-stream-opener IPC + dispatcher opener-first classification (ring-peek fallback for the N-1 broker window) + serve_attach durable-Request seeding + floor_seq subscribe; unit = opener_pins_first_line_across_full_ring_roll + opener_capture_gives_up_bounded_on_a_newline_less_stream; int = redispatch.rs D2 (ring rolled via the SPT_STREAM...\n 2748|\n 2749|[[requirements]]\n 2750|id = \"REQ-DISPATCH-CLAIM-RETRY\"\n*2751|title = \"REDISPATCH-TRUTH W1 (ADR-0038, hertz fix C): dispatcher claims are RETRYABLE and outcomes are CLASSIFIED — today claimed.insert(stream_id) happens PRE-spawn (dispatch.rs:206/214) and is never cleared or retried on Unknown/Failed, so one transient worker-setup failure permanently abandons the stream. Distinguish active/retryable vs served vs finished: a transient worker-setup failure releases/requeues the claim (bounded/backoff-shaped); terminal classification outcomes stay terminal and do NOT ...\n 2752|required_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-TRUTH W1 (todlando 2026-07-16). impl = ClaimState lifecycle (InFlight/Terminal/Retry, should_claim + outcome_transition pure seams, MAX 3 attempts, doubling backoff); unit = transient_failure_requeues_bounded_and_terminal_never_reenters (the no-hot-loop distinction in test); int = redispatch.rs D3 (SPT_DISPATCH_INJECT_TRANSIENT_ONCE valve — one injected failure, retry recovers, injection PROVEN fired in the generation log). HEAVY at birth.\n 2753|\n 2754|# ── MESH-RECOVERY (doyle triage 2026-07-16; ADR-0039; hertz RCA .claude/reports/2026-07-10-hertz-session/02-mesh-isolation-rca.md; KNOWN-HAZARDS 7.42). Stages flip at W1 build start (traceable-per-wave-activation). ──\n...\n 2776|[[requirements]]\n*2777|id = \"REQ-HAZARD-REDISPATCH-STALL\"\n 2778|title = \"REDISPATCH-STALL W1 (KNOWN-HAZARDS 7.43, hertz v0.34 field RCA 2026-07-16 — recurrent 20-30s PTY/RC freezes, 17-62s DISPATCH tails): one wedged stream subscriber must NEVER stall stream serving, and recovery machinery must not manufacture new replay victims. Today: claim retries x the broad Err(_) opener fallback (dispatch.rs:414) install throwaway peek subscribers whose StreamLog::attach replays the entire retained ring UNDER the per-stream mutex with discarded write errors and the poisoned s...\n 2779|required_stages = [\"doc\", \"int\"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). int = production-path wedged-subscriber regression (producer+unrelated streams flat, poisoned subscriber removed, stream recovers); doc = KNOWN-HAZARDS 7.43.\n 2780|\n...\n 2787|id = \"REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE\"\n*2788|title = \"REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 2+3+4): StreamLog subscriber write discipline — (a) replay and live fan-out HALT at the first failed subscriber write and the failed subscriber is REMOVED, at ALL sites (attach replay, append, finish; PresenceLog mirrors ride the same change — today let _ = sub.write() discards errors and iteration continues, nethost.rs:322-346); (b) subscriber I/O moves OFF the StreamLog mutex: bounded per-subscriber writer queue, enqueue-under-lock / I/O-outsi...\n 2789|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = halt+remove all sites (attach/append/finish + PresenceLog) + per-subscriber writer queue off the mutex (overflow=detach+resume-from-cursor) + cancelable serve/wire pairing; unit = per-site halt/remove + overflow-detach cursor + append-never-blocks; int = T1 + T3 + T4; doc = ADR-0038 Amendment.\n 2790|\n 2791|[[requirements]]\n 2792|id = \"REQ-SERVE-OWNERSHIP-GENERATION\"\n*2793|title = \"REDISPATCH-STALL W1 (ADR-0038 Amendment, fix 6): terminal-exclusion enforced PRE-SERVE + ownership/generation validation on attach/detach — a stale worker can never detach or displace a REPLACEMENT controller (today detach_if compares Arc ptr identity only; the serve path re-checks nothing at completion). Covers the UNFINISHED-stale-row control-steal shape (raw-close no-FIN viewports, emphasys C2 leak class feeding it) that finished-row retirement (D1/D1b) definitionally cannot see — the dis...\n 2794|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = pre-serve terminal exclusion + attach/detach generation tokens; unit = stale-generation detach refused, same-generation lands; int = T6 unfinished-stale steal; doc = ADR-0038 Amendment.\n 2795|\n 2796|[[requirements]]\n\n## docs/\n### KNOWN-HAZARDS.md\n 416|- **D1 (restoration skeleton, ADR-0018 Q2/Q3):** the process boundary is restored — `spt daemon run` is the broker process and spawns a supervised `spt daemon brain` child (`brainproc.rs`); the broker survives the brain dying and respawns it (proven in production topology by `crates/spt/tests/brain_split.rs`). The logic loops still run broker-side (D2 migrates them); the `int` process-level survival E2E + the in-process re-point land at D7.\n*417|- **Closed out (2026-06-11, v0.4.0–v0.4.2):** the two-process model shipped (v0.4.0); the D7 `int` E2E (`brain_survive.rs`) + the N-1 gate prove process-level survival onto swapped bytes and re-pointed REQ-DAEMON-2 / REQ-UPD-3. The v0.4.1 fleet-verify proved this Windows-seamless (hfenduleam: brain pid rolls, broker held, `exe_hash` flips, no manual bounce) but exposed a **Linux** respawn-path gap — the resident broker respawned the brain via per-spawn `current_exe()`, which on Linux follows the `app...\n 418|\n*419|### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`\n 420|- **Failure:** the readiness-gated auto-rollback (ADR-0018 Q7) spawns the *previous* binary against durable state the *new* brain already wrote. The first release that migrates a durable-state schema in place would make the old binary unable to read it — silently bricking rollback exactly when it is needed (a logic-bricking update that can no longer fall back).\n*421|- **Invariant:** a brain must not irreversibly migrate durable state before it is ready-promoted; equivalently, every pre-ready write must remain readable by the N-1 brain. Schema migrations are gated behind ready-promotion (or written in an N-1-tolerant additive form).\n 422|- **spt-core mapping:** lands with ADR-0018's auto-rollback. Free to assert now (a 2026-06-09 source audit confirmed zero state-migration code exists); unmintable retroactively once a migration ships.\n 423|- **D5 conformance (2026-06-10):** the new durable timing state `<spt_home>/deadline-<key>.json` (restoration D5-1) is **additive** — a rolled-back pre-D5 binary does not know the file and simply ignores it (re-phasing on its own flat-sleep cadence, the pre-D5 behavior). No existing-file schema migration, no irreversible pre-ready write → the new file is rollback-N-1-safe by construction. The thing for a future D6 guard to gate is a *migration* of this file's shape, not its introduction.\n*424|- **D6 guard (2026-06-10, restoration D6-3):** the invariant is now **asserted**, not just noted. The pre-ready durable writes are **enumerated in one place** — `spt-daemon::PRE_READY_DURABLE_FILES` (`rollback_compat.rs`): `deadline-<key>.json` (D5, `DeadlineAnchor`), `applied-state.json` (D6-1, the two-phase `AppliedRecord`), and the generation-stamped `brain.ready` breadcrumb (D6-1b, `{pid, generation}`). A **tripwire unit test** pins each one's additive / N-1-readable contract (load-bearing field na...\n 425|- **Origin:** verification amendment `[V1]` (agent `doyle`) on `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.\n*426|- **Closed out (2026-06-11):** the readiness-gated auto-rollback shipped (v0.4.0) and the pre-ready durable-file registry + tripwire guard (D6-3) hold. v0.4.2 added a promotion **bytes-gate** that turns a wrong-bytes respawn into an auto-rollback rather than a false `applied` record — strengthening the rollback path the same release exercised across the fleet. No in-place schema migration has shipped, so the invariant remains **asserted, not yet exercised by a real migration** (correct — activate-don...\n 427|<!-- [doc->REQ-HAZARD-ROLLBACK-STATE-COMPAT] -->\n 428|\n 429|### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output\n...\n 442|- **Failure:** the broker respawns the brain candidate from `std::env::current_exe()` resolved **per spawn** (`brainproc.rs:817`). `spt update apply` swaps the binary by renaming the running file `spt` → `spt.old-N` and writing the new bytes at `spt`. On **Linux**, `current_exe()` = `readlink(/proc/self/exe)` is **inode-tracking** and follows the rename to `.old-N`, so the resident broker respawns the brain onto the **OLD** bytes — the brain comes up ready (readiness passes), the trial **promotes**, ...\n*443|- **Invariant:** the candidate-binary default is the canonical exe path **captured once at broker start** (before any `apply` can rename under the process), never a per-spawn `current_exe()` — giving Linux the path-at-start semantics Windows already had. AND promotion is **bytes-gated**: a trial promotes only if the candidate's stamped `brain.ready` `exe_hash` equals the staged artifact's hash for this platform; a mismatch is a failed trial → auto-rollback + loud notif (readiness alone is not proof t...\n*444|- **spt-core mapping:** v0.4.2 fix (`V042-PLAN.md`). Half 1 = `spawn_brain_supervisor` canonical-exe capture threaded into `spawn_brain_child`'s `None` default; Half 2 = the promotion bytes-gate in `supervise_brain`'s `Promoted` arm (`TrialEnv::ready_exe_hash` + `staged_artifact_hash`). The rollback path (`Some(.old-N)` selection) is unchanged. Sibling of 6.7 — the broker *process* is correct; the bytes it respawned the brain ONTO were not.\n 445|- **Origin:** ADR-0018 Q3 silently assumed `current_exe()` path-string semantics; surfaced by the v0.4.1 fleet-roll `exe_hash` bytes assert (agents `todlando` + `doyle` + `deployah`, 2026-06-11). ADR-0018 Q3 amended.\n 446|<!-- [doc->REQ-HAZARD-BRAIN-RESPAWN-PATH] -->\n 447|\n...\n 605|\n*606|<!-- [doc->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] -->\n*607|### 7.19 An operator input FLOOD must not deadlock the broker via the applied-ack on the same conn  `[REQ-HAZARD-INPUT-ACK-BACKPRESSURE]`\n 608|- **Failure (operator HITL, the ctrl+V re-open):** a flood of operator input on one brain↔broker conn wedged the WHOLE broker PERMANENTLY (no new/existing attach; the controller stayed latched — the per-conn handler couldn't process the detach). `serve_attach` processes a whole `NetStreamData` batch of N `Input` records in its inner loop, calling `send_effect` N times WITHOUT returning to `read_event()`; the broker answers each with `send_frame(applied_envelope)` on the SAME conn. Brain not reading ...\n 609|- **Invariant:** the applied-ack is OPT-IN. `InputReq` carries `ack: bool` (serde `default = true`, N-1-safe). The fire-and-forward operator/rc path (`serve_attach`) sends `ack=false` via `Brain::send_effect_no_ack`; `dispatch_input` writes NO applied frame when `ack=false`, so the per-conn handler never writes back while servicing the flood → it always drains → no deadlock (cures ANY input flood). `shellchan` (one-at-a-time spool delivery, WAITS on `BrokerEvent::Applied`) keeps `send_effect` (`ack=t...\n 610|- **spt-core mapping:** impl `msg.rs` `InputReq.ack` (`default_true`) + `brain.rs` `send_effect_no_ack`/`send_effect_inner` + `attach.rs` `serve_attach` operator path → `send_effect_no_ack` + `broker.rs` `dispatch_input` gates `send_frame(applied)` on `req.ack`; unit the serde default + the ack-emitted-iff-true + the no-ack-path exactly-once dedup; int (keystone, repro-first) a flood of N>pipe-buffer input frames through `serve_attach` on one conn — PRE-FIX deadlocks, POST-FIX drains all N + the sess...\n...\n 750|- **Failure (paid-for, rig-CONFIRMED 2026-07-06/07 — `NtSuspendProcess` on the brain, no update involved):** a controller's writer thread does a BLOCKING socket write to its brain subscriber conn. When that brain is suspended (or black-holed) the write never returns. The output-driven eviction path (`append` → `mark_controller_gone`, bounded by 7.12's `CONTROLLER_WRITE_DEADLINE`) only fires on NEW output, and `reap_dead_controller` (7.29 companion) only caught a writer that had EXITED — so a contro...\n*751|- **Invariant:** no broker client ever observes a wedge because the BRAIN stalled. (1) The controller's writer publishes its IN-FLIGHT-write window (`write_blocked_since`: `Some(Instant)` set immediately before the blocking `write_frame`, `None` after — the mutex never held ACROSS the write); a reader sees a `Some` older than `BRAIN_WRITE_DEADLINE` (15 s = 3× the 7.12 controller bound, measured full-with-**zero writer progress**, so a slow-but-DRAINING brain never trips it) as a WEDGED writer and stal...\n 752|- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (...\n 753|- **Source:** LIFECYCLE-TRUTH W2 (doyle rig `NtSuspendProcess` root + design ruling `docs/W2-DESIGN-RULING.md` @60ec0ed — minimal-plus scope, `BRAIN_WRITE_DEADLINE` 15 s, passive resubscribe + observability rider; todlando build 2026-07-07). The v0.27→v0.28 update-wedge night's flagship defect.\n 754|\n...\n 788|\n*789|### 7.41 A fresh dispatcher must NEVER re-serve a terminal stream — historical replay must not steal or clear a live controller  `[REQ-HAZARD-REDISPATCH-CONTROL-STEAL]`\n*790|<!-- [doc->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] -->\n 791|- **Failure (paid-for, hertz field RCA 2026-07-16 — 4/5 endpoints frozen, twice in one day):** every brain cycle (`spt daemon refresh`, `spt update` apply — same `applyhost.rs` path) launches a fresh `dispatch::run_dispatch_loop` whose EMPTY per-process `claimed` set enumerates every broker-held peer stream. `NetShared.streams` has NO removal path (`StreamLog::finish` only marks) and the claim condition has no finished filter → the fresh dispatcher replays FINISHED historical attach streams. A repl...\n 792|- **Invariant:** redispatch eligibility is LIFECYCLE-gated: a finished/terminal stream row is retired from the dispatcher's enumeration (a fresh dispatcher never re-serves a terminal Attach); classification identity is restart-durable and independent of the evictable data ring (immutable opener fact pinned per stream until close); claims are retryable on transient worker-setup failure and terminal on terminal outcomes (no hot-loop). The legitimate same-`by` successor re-take after a brain restart still s...\n 793|- **spt-core mapping:** `spt-daemon/src/dispatch.rs` `run_dispatch_loop` claim condition + `peek_first_line` classification + worker spawn; `spt-daemon/src/nethost.rs` `NetShared.streams` insert/finish + `DEFAULT_STREAM_RING_CHUNKS`; `spt-daemon/src/applyhost.rs` (the brain-cycle trigger).\n...\n 807|\n*808|### 7.43 One wedged stream subscriber must NEVER stall stream serving — replay halts at the first failed write, a poisoned subscriber is removed, and recovery machinery must not manufacture new replay victims  `[REQ-HAZARD-REDISPATCH-STALL]`\n*809|<!-- [doc->REQ-HAZARD-REDISPATCH-STALL] -->\n 810|- **Failure (paid-for, hertz field RCA 2026-07-16 — live v0.34 boxes, recurrent 20–30s PTY/RC freezes, DISPATCH tails 17–62s):** a COMPOSITION, not one new timer. The dispatcher's retryable claims (500ms/1s ×3) re-drove the opener fallback whose guard arm is a broad `Err(_)` (`dispatch.rs:414` — comment intends old-broker-only; catches transport timeout/EOF/poison), each pass installing a throwaway peek subscriber. `StreamLog::attach` synchronously replays the ENTIRE retained ring under the per-...\n 811|- **Invariant:** subscriber I/O never holds the `StreamLog` mutex (enqueue-under-lock, I/O outside, bounded per-subscriber queue; overflow = detach + resume-from-cursor); replay and fan-out halt at the FIRST failed subscriber write and the failed subscriber is REMOVED (attach + append + finish, all sites, `PresenceLog` mirrors included); the ring-peek fallback fires ONLY on the explicit unsupported-verb/old-broker answer; a deadline-poisoned replay is circuit-broken (global backoff, no reinstall until th...\n 812|- **spt-core mapping:** `spt-daemon/src/nethost.rs` `StreamLog::attach`/`push`/`finish` + `PresenceLog` mirrors + serve/wire worker pairing; `spt-daemon/src/dispatch.rs` `first_line` fallback arm + claim retry/backoff + worker spawn; SharedSend bounded-write deadline (round-4, KH 7.7 kin).\n...\n 1087|| 6.7 | Broker + brain are separate processes (brain restart never drops a hosted endpoint at the process level) | daemon process topology, self-update (ADR-0018) |\n*1088|| 6.8 | No irreversible durable-state migration before update ready-promotion (pre-ready writes stay N-1-readable) | auto-rollback / durable-state schema (ADR-0018) |\n 1089|| 6.10 | Phase-significant loop timing is a durable absolute-deadline grid (no per-fire write; update preserves phase, crash resets, one-shot never resets) | durable loop timing / self-update (ADR-0018 Q4) |\n*1090|| 6.11 | Brain respawn execs the applied bytes (canonical exe captured at broker start, not per-spawn current_exe) + promotion bytes-gate (exe_hash == artifact, else rollback) | daemon respawn path / self-update (ADR-0018 Q3) |\n 1091|| 6.12 | The echo-commune brief never transits the agent's `<id>-commune.md` (one writer on that path — the agent); the echo routes direct into the two-tier store | echo-commune seam / context tiering (releases#276) |\n 1092|| 6.13 | IO event tail reads cannot mint false zero; append exceeds the global max, reset repair preserves file order, and trim retains by position | `spt_store::iolog` |\n 1093|| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |\n\n### adr/\n#### 0018-broker-brain-process-isolation-restoration.md\n 23|1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.\n*24|2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** \"the executable path\" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-sp...\n 25|3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`...\n 26|4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update...\n 27|5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).\n*28|6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable ...\n 29|7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.\n 30|8. **Cross-platform uniformity (Q8).** The broker spawns the brain as a child process (`Command::spawn`) + socket IPC — no `exec`, so no Windows/Unix divergence. ConPTY/forkpty handles never leave the broker.\n 31|\n#### 0038-broker-stream-lifecycle-truth.md\n 26|## Decision\n*27|<!-- [doc->REQ-REDISPATCH-FINISHED-RETIRE] -->\n 28|\n 29|Stream rows carry explicit **lifecycle truth**, and redispatch consumes it:\n 30|\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":".worktrees/49-267-refresh-discriminator/traceable-reqs.toml, .worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md, .worktrees/49-267-refresh-discriminator/docs/adr","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":39,"fileCount":4,"files":[".worktrees/49-267-refresh-discriminator/traceable-reqs.toml",".worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md",".worktrees/49-267-refresh-discriminator/docs/adr/0018-broker-brain-process-isolation-restoration.md",".worktrees/49-267-refresh-discriminator/docs/adr/0038-broker-stream-lifecycle-truth.md"],"fileMatches":[{"path":".worktrees/49-267-refresh-discriminator/traceable-reqs.toml","count":20},{"path":".worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md","count":16},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0018-broker-brain-process-isolation-restoration.md","count":2},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0038-broker-stream-lifecycle-truth.md","count":1}],"truncated":true,"perFileLimitReached":20,"displayContent":"# .worktrees/49-267-refresh-discriminator/\n## traceable-reqs.toml\n 1174│id = \"REQ-HAZARD-ROLLBACK-STATE-COMPAT\"\n*1175│title = \"A brain must not irreversibly migrate durable state before update ready-promotion: the readiness-gated auto-rollback (ADR-0018 Q7) spawns the N-1 binary against durable state the new brain may have written, so every pre-ready write must stay N-1-readable (schema migrations gated behind ready-promotion, or written N-1-tolerant/additive). Else the first in-place schema migration silently bricks rollback (KNOWN-HAZARDS 6.8). Free now — a 2026-06-09 audit confirmed zero state-migration code exists...\n 1176│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc activated restoration D1 (KNOWN-HAZARDS 6.8 + ADR-0018 + design doc V1). impl/unit activated restoration D6-3 (the one D6 toml activation): impl = rollback_compat.rs (the pre-ready durable-file registry PRE_READY_DURABLE_FILES — the enumerated guard surface) + the two-phase AppliedRecord (relcache.rs, additive durable state); unit = the rollback_compat tripwire (every current pre-ready file shape is additive / N-1-readable; a non-additive pre-ready chang...\n 1177│# --- Added 2026-06-11 (v0.4.2): the v0.4.1 fleet-roll Linux brain-respawn blocker (ADR-0018 Q3 amendment); rule 3 register-before-satisfy, rule 5 activate per task ---\n 1178│[[requirements]]\n 1179│id = \"REQ-HAZARD-BRAIN-RESPAWN-PATH\"\n*1180│title = \"The broker respawns the brain onto the APPLIED bytes, not the renamed old binary: the candidate-binary default is the canonical exe path captured ONCE at broker start, never a per-spawn std::env::current_exe() — on Linux current_exe (readlink /proc/self/exe) is inode-tracking and follows the `apply` rename (spt -> spt.old-N), so a resident broker would respawn the brain onto OLD bytes while recording `applied` (Windows GetModuleFileName is path-at-start, so Windows was green; ADR-0018 Q3 silen...\n*1181│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # activated v0.4.2 (2026-06-11): minted from the v0.4.1 fleet-roll blocker. doc = KNOWN-HAZARDS 6.11 + ADR-0018 Q3 amendment; int = brain_respawn_rename.rs (real broker process, in-place rename P->P.old-N + new bytes at P, respawn must run B not A — fails Linux pre-fix). Stages: impl = brainproc.rs canonical-exe capture at spawn_brain_supervisor + promotion bytes-gate in supervise_brain Promoted arm (TrialEnv ready_exe_hash/staged_artifact_hash); unit =...\n 1182│# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---\n 1183│[[requirements]]\n 1184│id = \"REQ-HAZARD-PSYCHE-OUTBOUND-PROXY\"\n     │...\n 1579│title = \"In an `spt rc` session neither ctrl+V nor right-click pastes (CC explicitly supports ctrl+V). ROOT (doyle /diagnose): RawGuard does only enable_raw_mode (no bracketed paste / no mouse capture / no clipboard interception); the Windows console delivers a paste as synthetic per-char KEY EVENTs (no crossterm Event::Paste), and ctrl+V translates to bare ^V forwarded to CC — but CC runs DAEMON-SIDE with NO access to the operator's LOCAL clipboard, so remote paste is fundamentally CLIENT-ORIGINATED. ...\n*1580│required_stages = [\"doc\", \"impl\", \"unit\"]  # ACTIVATED v0.13.0 P1 (todlando, last build item; doyle gates). DEPENDS ON P0 (@0675b37, landed) — a paste chunk rides P0's single-writer thread without wedging. NO int (live clipboard + console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent; pure pieces unit-tested). impl = (cfg(windows), folds into the bug-2 event path) `wrap_bracketed_paste` (ESC[200~ + content + ESC[201~) + ctrl+V intercept (Char('v')+CONTROL, when not detach-armed) + right-mouse paste in `s...\n 1581│\n 1582│# ── P1b (operator HITL re-open 2026-06-19, doyle /diagnose on the enhanced rc+broker capture): an input FLOOD deadlocks the broker via the applied-ack ──\n 1583│[[requirements]]\n*1584│id = \"REQ-HAZARD-INPUT-ACK-BACKPRESSURE\"\n 1585│title = \"A FLOOD of operator input on one brain↔broker connection deadlocks the broker PERMANENTLY (entire broker — no new/existing attach; the controller stays latched because the per-conn handler can't process the detach). ROOT (doyle /diagnose, code-grounded + HITL capture, the v0.13.0 P1 ctrl+V re-open): `serve_attach` processes a whole `NetStreamData` batch of N operator `Input` records in its inner `for rec in decoder.push()` loop, calling `brain.send_effect(op_id, &bytes)` N times WITHOUT retu...\n 1586│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED v0.13.0 P1b (todlando, the real ship-blocker behind the ctrl+V wedge; doyle gates). doc = KNOWN-HAZARDS 7.19 (the deadlock + the N-1 broker-resident caveat). impl = InputReq.ack (serde default_true) + Brain::send_effect_no_ack (send_effect_inner shared body) + serve_attach operator path -> send_effect_no_ack + dispatch_input gates send_frame(applied) on req.ack. unit = InputReq.ack serde default=true on an absent field (N-1) + dispatch_input e...\n 1587│\n     │...\n 2347│title = \"W2 (LIFECYCLE-TRUTH, KNOWN-HAZARDS, flagship — the update wedge): PTY viewer fan-out and control mutations must not depend synchronously on a live draining brain. ROOT rig-CONFIRMED (NtSuspendProcess on the brain, no update involved): brain-subscriber session-output writes ride UNDER the per-session log lock (broker.rs:19-20); failed writes are handled (cursor freeze + detach :3486) but BLOCKED writes are not. Suspended brain => within seconds attached rc output freezes; detach does NOT releas...\n*2348│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED LIFECYCLE-TRUTH W2 (todlando 2026-07-07) per doyle ruling docs/W2-DESIGN-RULING.md @60ec0ed (minimal-plus scope). doc = KNOWN-HAZARDS entry (\"PTY fan-out + broker control plane never block on any single subscriber conn — bounded sinks everywhere incl. the brain conn; brain.ready != subscribers drained\"). impl = (a) bounded brain-conn sink (writer thread + bounded queue + BRAIN_WRITE_DEADLINE=15s deadline-evict, full-with-zero-progress last_o...\n 2349│\n 2350│[[requirements]]\n 2351│id = \"REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL\"\n     │...\n 2376│id = \"REQ-UPDATE-PROMOTE-DRAINED\"\n*2377│title = \"W3 (LIFECYCLE-TRUTH, mechanic-d MOVED FROM W2 per doyle gate verdict @e5ae7a9 — binding): the update-apply brain-generation promotion completes only when the OLD generation's broker subscriber connection is CLOSED or stall-EVICTED — never while blocked writes still pend on it. ROOT: `brain.ready` != subscribers drained; W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-promote window to BRAIN_WRITE_DEADLINE (15s), it does NOT close it — a new brain can signal...\n 2378│required_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED LIFECYCLE-TRUTH W3 drained-condition slice (todlando 2026-07-07, work-start); brainproc.rs run_trial DRAINED gate + broker any_local_controller_wedged truth + false-promote int rig — closes the W2 15s residual (KH 7.36)\n 2379│\n 2380│[[requirements]]\n 2381│id = \"REQ-UPDATE-TRIAL-DRAIN-DRIVE\"\n*2382│title = \"UPDATE-WEDGE (counter-54, doyle-ruled 2026-07-09 — regression of the v0.29.0 seamless brain-swap): a brain generation DRIVES the broker's controller-liveness reap (a KIND_SESSIONS poll) each heartbeat throughout its boot/trial loop, so a hard-KILLED prior generation's black-holed LOCAL controller conn (by:None) is stall-evicted within the trial window and can never permanently strand the promotion DRAINED gate. ROOT (2026-07-09 field freeze, `spt update fetch --apply` v0.30.0->v0.30.2 froze al...\n 2383│required_stages = [\"doc\", \"impl\", \"int\"]  # ACTIVATED counter-54 UPDATE-WEDGE fix (doyle ruled brain-side 2026-07-09; todlando builds). doc = a CONTEXT.md/LIFECYCLE note that the trial/boot brain drives controller-reap so a killed prior generation cannot strand old_gen_drained. impl = the brain.sessions() KIND_SESSIONS drive in run_brain's heartbeat loop (brainproc.rs, first-tick + every tick, trial + recovery brain). UNIT DELIBERATELY DROPPED (doyle FORK-2 ruling 2026-07-09): the impl is ONE uncondition...\n 2384│\n 2385│[[requirements]]\n 2386│id = \"REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE\"\n*2387│title = \"SEED (DEFERRED, doyle 2026-07-09 — post-counter-54 root-hardening for UPDATE-WEDGE; mint now, impl a FUTURE milestone): on a PLANNED brain-restart (`BRAIN_UPDATE_RESTART`, the seamless update-apply brain-cycle), the outgoing brain's LOCAL (by:None) controller conns are GRACEFULLY CLEAN-CLOSED as the brain is cycled, instead of hard-killed and left to black-hole. ROOT (field-pinned 2026-07-09, daemon.stderr.log L24277-24303): the update-restart path hard-kills the outgoing brain (`child.kill()`...\n 2388│required_stages = []  # SEED minted DEFERRED (doyle 2026-07-09); activate post-counter-54 at the milestone that delivers HITCH-FREE seamless updates. Supersedes the livehost-reattach 'Fix Y' framing (wrong site). The counter-54 fix (REQ-UPDATE-TRIAL-DRAIN-DRIVE reap-drive) makes updates SELF-HEAL in ~15s; this seed removes the residual hitch.\n 2389│\n 2390│[[requirements]]\n     │...\n 2396│id = \"REQ-BRAIN-RESUME-NO-CONTROL-STEAL\"\n*2397│title = \"UPDATE-WEDGE round 2 (v0.30.4, doyle-ruled 2026-07-09 — field incident on the counter-54 fetch--apply): a brain-respawn must NEVER steal, then stall-evict, the controller of a broker PTY session the daemon brain does not DRIVE. ROOT (field-pinned + SME, docs/UPDATE-WEDGE-2-RCA.md + docs/UPDATE-WEDGE-2-SME-todlando.md): `resume_sessions` (brain.rs:985) re-attaches EVERY session `KIND_SESSIONS` returns via `subscribe` = `subscribe_with(AttachIntent::Control, by:None)` (brain.rs:1450-1455). The d...\n 2398│required_stages = [\"doc\", \"impl\", \"int\"]  # ACTIVATED v0.30.4 UPDATE-WEDGE-2 fix (doyle ruled 2026-07-09; todlando builds). doc = brain.rs resume docstring corrected (kill the by:None-never-steals MYTH) + a CONTEXT/LIFECYCLE note that a brain-respawn re-attaches non-driven sessions as viewer. impl = resume path re-attaches AttachIntent::Viewer (Control only for genuinely-driven sessions, empty today). unit DROPPED if the impl is a branchless intent-flip (same FORK-2 reasoning as REQ-UPDATE-TRIAL-DRAIN-DR...\n 2399│\n 2400│[[requirements]]\n     │...\n 2649│[[requirements]]\n*2650│id = \"REQ-DAEMON-REFRESH\"\n 2651│title = \"THE-FORKENING W4 (operator add 2026-07-14): `spt daemon refresh` — restart the daemon BRAIN without a binary swap and WITHOUT touching the broker: exactly the apply_staged brain-cycle path (brain stop -> respawn -> readiness trial -> promote, incl. the trial-drain drive REQ-UPDATE-TRIAL-DRAIN-DRIVE and viewer-only resume REQ-BRAIN-RESUME-NO-CONTROL-STEAL) minus the swap. Recovery verb for wedged brain-held state (field motivator 2026-07-14: endpoint bringup broken on a live daemon + deployah d...\n 2652│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # FLIPPED in the W4 build commit (todlando 2026-07-15).\n 2653│\n     │...\n 2734│[[requirements]]\n*2735│id = \"REQ-HAZARD-REDISPATCH-CONTROL-STEAL\"\n*2736│title = \"REDISPATCH-TRUTH W1 (KNOWN-HAZARDS 7.41, hertz field RCA 2026-07-16 — 4/5 endpoints frozen per brain cycle): a fresh dispatcher must NEVER re-serve a terminal stream — a replayed historical Attach must not steal (same-identity silent become_controller, no Displaced) or clear (replayed-EOF detach_session) a LIVE controller. The legitimate same-by successor re-take after a brain restart still silently re-takes: the discriminator is stream LIFECYCLE, never origin identity. Gate: int — product...\n 2737│required_stages = [\"doc\", \"int\"]  # ACTIVATED REDISPATCH-TRUTH W1 (todlando 2026-07-16). doc = KNOWN-HAZARDS 7.41; int = redispatch.rs D1 (Served-retired historical attach) + D1b gate-round-1 (finished-UNRETIRED orderings: kill gen1 mid-serve THEN detach — pre-existing-backlog/brain-down-detach shape; gen2 skips + retires-on-sight, current controller unharmed across multi-second polls) — both production run_dispatch_loop via killable dispatch_fixture generations. HEAVY at birth, all three strings.\n 2738│\n 2739│[[requirements]]\n*2740│id = \"REQ-REDISPATCH-FINISHED-RETIRE\"\n*2741│title = \"REDISPATCH-TRUTH W1 (ADR-0038, hertz fix A): finished/terminal stream rows are RETIRED from redispatch eligibility — NetShared.streams today has NO removal path (single insert nethost.rs ~649; StreamLog::finish only marks) so every dispatcher generation re-enumerates every historical stream forever. Retire terminal rows from the enumeration the dispatcher claims from (remove, or lifecycle-exclude), preserving only the post-EOF state genuinely needed by other readers (presence/log reads); bound...\n 2742│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-TRUTH W1 (todlando 2026-07-16; int ADDED gate round 1; RULING gate round 2: retirement is ELIGIBILITY-VISIBILITY, never teardown — the \"lifecycle-exclude\" arm of the title, remove is REJECTED: the worker's reply send is fire-and-forget, so remove-on-retire races the still-queued reply inside the broker and resets the never-finished send half = requester torn-EOF/NoReply, proven 30/150 vs 0/150 A/B on kitsubito; physical teardown b...\n 2743│\n 2744│[[requirements]]\n 2745│id = \"REQ-STREAM-OPENER-DURABLE\"\n*2746│title = \"REDISPATCH-TRUTH W1 (ADR-0038, hertz fix B): stream classification identity is RESTART-DURABLE and independent of the evictable data ring — today reconstruction classifies via peek_first_line from ring seq 0, but StreamLog is a bounded 4096-transport-chunk ring (DEFAULT_STREAM_RING_CHUNKS, nethost.rs:111), so a high-traffic ACTIVE stream's opener/Request record evicts and replay classifies Unknown/Failed = active stream permanently abandoned. Pin an immutable bounded opener/classification fact...\n 2747│required_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-TRUTH W1 (todlando 2026-07-16). impl = StreamLog OpenerCapture (pinned outside the ring, bounded hunt) + net-stream-opener IPC + dispatcher opener-first classification (ring-peek fallback for the N-1 broker window) + serve_attach durable-Request seeding + floor_seq subscribe; unit = opener_pins_first_line_across_full_ring_roll + opener_capture_gives_up_bounded_on_a_newline_less_stream; int = redispatch.rs D2 (ring rolled via the SPT_STREAM...\n 2748│\n 2749│[[requirements]]\n 2750│id = \"REQ-DISPATCH-CLAIM-RETRY\"\n*2751│title = \"REDISPATCH-TRUTH W1 (ADR-0038, hertz fix C): dispatcher claims are RETRYABLE and outcomes are CLASSIFIED — today claimed.insert(stream_id) happens PRE-spawn (dispatch.rs:206/214) and is never cleared or retried on Unknown/Failed, so one transient worker-setup failure permanently abandons the stream. Distinguish active/retryable vs served vs finished: a transient worker-setup failure releases/requeues the claim (bounded/backoff-shaped); terminal classification outcomes stay terminal and do NOT ...\n 2752│required_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-TRUTH W1 (todlando 2026-07-16). impl = ClaimState lifecycle (InFlight/Terminal/Retry, should_claim + outcome_transition pure seams, MAX 3 attempts, doubling backoff); unit = transient_failure_requeues_bounded_and_terminal_never_reenters (the no-hot-loop distinction in test); int = redispatch.rs D3 (SPT_DISPATCH_INJECT_TRANSIENT_ONCE valve — one injected failure, retry recovers, injection PROVEN fired in the generation log). HEAVY at birth.\n 2753│\n 2754│# ── MESH-RECOVERY (doyle triage 2026-07-16; ADR-0039; hertz RCA .claude/reports/2026-07-10-hertz-session/02-mesh-isolation-rca.md; KNOWN-HAZARDS 7.42). Stages flip at W1 build start (traceable-per-wave-activation). ──\n     │...\n 2776│[[requirements]]\n*2777│id = \"REQ-HAZARD-REDISPATCH-STALL\"\n 2778│title = \"REDISPATCH-STALL W1 (KNOWN-HAZARDS 7.43, hertz v0.34 field RCA 2026-07-16 — recurrent 20-30s PTY/RC freezes, 17-62s DISPATCH tails): one wedged stream subscriber must NEVER stall stream serving, and recovery machinery must not manufacture new replay victims. Today: claim retries x the broad Err(_) opener fallback (dispatch.rs:414) install throwaway peek subscribers whose StreamLog::attach replays the entire retained ring UNDER the per-stream mutex with discarded write errors and the poisoned s...\n 2779│required_stages = [\"doc\", \"int\"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). int = production-path wedged-subscriber regression (producer+unrelated streams flat, poisoned subscriber removed, stream recovers); doc = KNOWN-HAZARDS 7.43.\n 2780│\n     │...\n 2787│id = \"REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE\"\n*2788│title = \"REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 2+3+4): StreamLog subscriber write discipline — (a) replay and live fan-out HALT at the first failed subscriber write and the failed subscriber is REMOVED, at ALL sites (attach replay, append, finish; PresenceLog mirrors ride the same change — today let _ = sub.write() discards errors and iteration continues, nethost.rs:322-346); (b) subscriber I/O moves OFF the StreamLog mutex: bounded per-subscriber writer queue, enqueue-under-lock / I/O-outsi...\n 2789│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = halt+remove all sites (attach/append/finish + PresenceLog) + per-subscriber writer queue off the mutex (overflow=detach+resume-from-cursor) + cancelable serve/wire pairing; unit = per-site halt/remove + overflow-detach cursor + append-never-blocks; int = T1 + T3 + T4; doc = ADR-0038 Amendment.\n 2790│\n 2791│[[requirements]]\n 2792│id = \"REQ-SERVE-OWNERSHIP-GENERATION\"\n*2793│title = \"REDISPATCH-STALL W1 (ADR-0038 Amendment, fix 6): terminal-exclusion enforced PRE-SERVE + ownership/generation validation on attach/detach — a stale worker can never detach or displace a REPLACEMENT controller (today detach_if compares Arc ptr identity only; the serve path re-checks nothing at completion). Covers the UNFINISHED-stale-row control-steal shape (raw-close no-FIN viewports, emphasys C2 leak class feeding it) that finished-row retirement (D1/D1b) definitionally cannot see — the dis...\n 2794│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = pre-serve terminal exclusion + attach/detach generation tokens; unit = stale-generation detach refused, same-generation lands; int = T6 unfinished-stale steal; doc = ADR-0038 Amendment.\n 2795│\n 2796│[[requirements]]\n\n## docs/\n### KNOWN-HAZARDS.md\n  416│- **D1 (restoration skeleton, ADR-0018 Q2/Q3):** the process boundary is restored — `spt daemon run` is the broker process and spawns a supervised `spt daemon brain` child (`brainproc.rs`); the broker survives the brain dying and respawns it (proven in production topology by `crates/spt/tests/brain_split.rs`). The logic loops still run broker-side (D2 migrates them); the `int` process-level survival E2E + the in-process re-point land at D7.\n *417│- **Closed out (2026-06-11, v0.4.0–v0.4.2):** the two-process model shipped (v0.4.0); the D7 `int` E2E (`brain_survive.rs`) + the N-1 gate prove process-level survival onto swapped bytes and re-pointed REQ-DAEMON-2 / REQ-UPD-3. The v0.4.1 fleet-verify proved this Windows-seamless (hfenduleam: brain pid rolls, broker held, `exe_hash` flips, no manual bounce) but exposed a **Linux** respawn-path gap — the resident broker respawned the brain via per-spawn `current_exe()`, which on Linux follows the `app...\n  418│\n *419│### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`\n  420│- **Failure:** the readiness-gated auto-rollback (ADR-0018 Q7) spawns the *previous* binary against durable state the *new* brain already wrote. The first release that migrates a durable-state schema in place would make the old binary unable to read it — silently bricking rollback exactly when it is needed (a logic-bricking update that can no longer fall back).\n *421│- **Invariant:** a brain must not irreversibly migrate durable state before it is ready-promoted; equivalently, every pre-ready write must remain readable by the N-1 brain. Schema migrations are gated behind ready-promotion (or written in an N-1-tolerant additive form).\n  422│- **spt-core mapping:** lands with ADR-0018's auto-rollback. Free to assert now (a 2026-06-09 source audit confirmed zero state-migration code exists); unmintable retroactively once a migration ships.\n  423│- **D5 conformance (2026-06-10):** the new durable timing state `<spt_home>/deadline-<key>.json` (restoration D5-1) is **additive** — a rolled-back pre-D5 binary does not know the file and simply ignores it (re-phasing on its own flat-sleep cadence, the pre-D5 behavior). No existing-file schema migration, no irreversible pre-ready write → the new file is rollback-N-1-safe by construction. The thing for a future D6 guard to gate is a *migration* of this file's shape, not its introduction.\n *424│- **D6 guard (2026-06-10, restoration D6-3):** the invariant is now **asserted**, not just noted. The pre-ready durable writes are **enumerated in one place** — `spt-daemon::PRE_READY_DURABLE_FILES` (`rollback_compat.rs`): `deadline-<key>.json` (D5, `DeadlineAnchor`), `applied-state.json` (D6-1, the two-phase `AppliedRecord`), and the generation-stamped `brain.ready` breadcrumb (D6-1b, `{pid, generation}`). A **tripwire unit test** pins each one's additive / N-1-readable contract (load-bearing field na...\n  425│- **Origin:** verification amendment `[V1]` (agent `doyle`) on `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.\n *426│- **Closed out (2026-06-11):** the readiness-gated auto-rollback shipped (v0.4.0) and the pre-ready durable-file registry + tripwire guard (D6-3) hold. v0.4.2 added a promotion **bytes-gate** that turns a wrong-bytes respawn into an auto-rollback rather than a false `applied` record — strengthening the rollback path the same release exercised across the fleet. No in-place schema migration has shipped, so the invariant remains **asserted, not yet exercised by a real migration** (correct — activate-don...\n  427│<!-- [doc->REQ-HAZARD-ROLLBACK-STATE-COMPAT] -->\n  428│\n  429│### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output\n     │...\n  442│- **Failure:** the broker respawns the brain candidate from `std::env::current_exe()` resolved **per spawn** (`brainproc.rs:817`). `spt update apply` swaps the binary by renaming the running file `spt` → `spt.old-N` and writing the new bytes at `spt`. On **Linux**, `current_exe()` = `readlink(/proc/self/exe)` is **inode-tracking** and follows the rename to `.old-N`, so the resident broker respawns the brain onto the **OLD** bytes — the brain comes up ready (readiness passes), the trial **promotes**, ...\n *443│- **Invariant:** the candidate-binary default is the canonical exe path **captured once at broker start** (before any `apply` can rename under the process), never a per-spawn `current_exe()` — giving Linux the path-at-start semantics Windows already had. AND promotion is **bytes-gated**: a trial promotes only if the candidate's stamped `brain.ready` `exe_hash` equals the staged artifact's hash for this platform; a mismatch is a failed trial → auto-rollback + loud notif (readiness alone is not proof t...\n *444│- **spt-core mapping:** v0.4.2 fix (`V042-PLAN.md`). Half 1 = `spawn_brain_supervisor` canonical-exe capture threaded into `spawn_brain_child`'s `None` default; Half 2 = the promotion bytes-gate in `supervise_brain`'s `Promoted` arm (`TrialEnv::ready_exe_hash` + `staged_artifact_hash`). The rollback path (`Some(.old-N)` selection) is unchanged. Sibling of 6.7 — the broker *process* is correct; the bytes it respawned the brain ONTO were not.\n  445│- **Origin:** ADR-0018 Q3 silently assumed `current_exe()` path-string semantics; surfaced by the v0.4.1 fleet-roll `exe_hash` bytes assert (agents `todlando` + `doyle` + `deployah`, 2026-06-11). ADR-0018 Q3 amended.\n  446│<!-- [doc->REQ-HAZARD-BRAIN-RESPAWN-PATH] -->\n  447│\n     │...\n  605│\n *606│<!-- [doc->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] -->\n *607│### 7.19 An operator input FLOOD must not deadlock the broker via the applied-ack on the same conn  `[REQ-HAZARD-INPUT-ACK-BACKPRESSURE]`\n  608│- **Failure (operator HITL, the ctrl+V re-open):** a flood of operator input on one brain↔broker conn wedged the WHOLE broker PERMANENTLY (no new/existing attach; the controller stayed latched — the per-conn handler couldn't process the detach). `serve_attach` processes a whole `NetStreamData` batch of N `Input` records in its inner loop, calling `send_effect` N times WITHOUT returning to `read_event()`; the broker answers each with `send_frame(applied_envelope)` on the SAME conn. Brain not reading ...\n  609│- **Invariant:** the applied-ack is OPT-IN. `InputReq` carries `ack: bool` (serde `default = true`, N-1-safe). The fire-and-forward operator/rc path (`serve_attach`) sends `ack=false` via `Brain::send_effect_no_ack`; `dispatch_input` writes NO applied frame when `ack=false`, so the per-conn handler never writes back while servicing the flood → it always drains → no deadlock (cures ANY input flood). `shellchan` (one-at-a-time spool delivery, WAITS on `BrokerEvent::Applied`) keeps `send_effect` (`ack=t...\n  610│- **spt-core mapping:** impl `msg.rs` `InputReq.ack` (`default_true`) + `brain.rs` `send_effect_no_ack`/`send_effect_inner` + `attach.rs` `serve_attach` operator path → `send_effect_no_ack` + `broker.rs` `dispatch_input` gates `send_frame(applied)` on `req.ack`; unit the serde default + the ack-emitted-iff-true + the no-ack-path exactly-once dedup; int (keystone, repro-first) a flood of N>pipe-buffer input frames through `serve_attach` on one conn — PRE-FIX deadlocks, POST-FIX drains all N + the sess...\n     │...\n  750│- **Failure (paid-for, rig-CONFIRMED 2026-07-06/07 — `NtSuspendProcess` on the brain, no update involved):** a controller's writer thread does a BLOCKING socket write to its brain subscriber conn. When that brain is suspended (or black-holed) the write never returns. The output-driven eviction path (`append` → `mark_controller_gone`, bounded by 7.12's `CONTROLLER_WRITE_DEADLINE`) only fires on NEW output, and `reap_dead_controller` (7.29 companion) only caught a writer that had EXITED — so a contro...\n *751│- **Invariant:** no broker client ever observes a wedge because the BRAIN stalled. (1) The controller's writer publishes its IN-FLIGHT-write window (`write_blocked_since`: `Some(Instant)` set immediately before the blocking `write_frame`, `None` after — the mutex never held ACROSS the write); a reader sees a `Some` older than `BRAIN_WRITE_DEADLINE` (15 s = 3× the 7.12 controller bound, measured full-with-**zero writer progress**, so a slow-but-DRAINING brain never trips it) as a WEDGED writer and stal...\n  752│- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (...\n  753│- **Source:** LIFECYCLE-TRUTH W2 (doyle rig `NtSuspendProcess` root + design ruling `docs/W2-DESIGN-RULING.md` @60ec0ed — minimal-plus scope, `BRAIN_WRITE_DEADLINE` 15 s, passive resubscribe + observability rider; todlando build 2026-07-07). The v0.27→v0.28 update-wedge night's flagship defect.\n  754│\n     │...\n  788│\n *789│### 7.41 A fresh dispatcher must NEVER re-serve a terminal stream — historical replay must not steal or clear a live controller  `[REQ-HAZARD-REDISPATCH-CONTROL-STEAL]`\n *790│<!-- [doc->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] -->\n  791│- **Failure (paid-for, hertz field RCA 2026-07-16 — 4/5 endpoints frozen, twice in one day):** every brain cycle (`spt daemon refresh`, `spt update` apply — same `applyhost.rs` path) launches a fresh `dispatch::run_dispatch_loop` whose EMPTY per-process `claimed` set enumerates every broker-held peer stream. `NetShared.streams` has NO removal path (`StreamLog::finish` only marks) and the claim condition has no finished filter → the fresh dispatcher replays FINISHED historical attach streams. A repl...\n  792│- **Invariant:** redispatch eligibility is LIFECYCLE-gated: a finished/terminal stream row is retired from the dispatcher's enumeration (a fresh dispatcher never re-serves a terminal Attach); classification identity is restart-durable and independent of the evictable data ring (immutable opener fact pinned per stream until close); claims are retryable on transient worker-setup failure and terminal on terminal outcomes (no hot-loop). The legitimate same-`by` successor re-take after a brain restart still s...\n  793│- **spt-core mapping:** `spt-daemon/src/dispatch.rs` `run_dispatch_loop` claim condition + `peek_first_line` classification + worker spawn; `spt-daemon/src/nethost.rs` `NetShared.streams` insert/finish + `DEFAULT_STREAM_RING_CHUNKS`; `spt-daemon/src/applyhost.rs` (the brain-cycle trigger).\n     │...\n  807│\n *808│### 7.43 One wedged stream subscriber must NEVER stall stream serving — replay halts at the first failed write, a poisoned subscriber is removed, and recovery machinery must not manufacture new replay victims  `[REQ-HAZARD-REDISPATCH-STALL]`\n *809│<!-- [doc->REQ-HAZARD-REDISPATCH-STALL] -->\n  810│- **Failure (paid-for, hertz field RCA 2026-07-16 — live v0.34 boxes, recurrent 20–30s PTY/RC freezes, DISPATCH tails 17–62s):** a COMPOSITION, not one new timer. The dispatcher's retryable claims (500ms/1s ×3) re-drove the opener fallback whose guard arm is a broad `Err(_)` (`dispatch.rs:414` — comment intends old-broker-only; catches transport timeout/EOF/poison), each pass installing a throwaway peek subscriber. `StreamLog::attach` synchronously replays the ENTIRE retained ring under the per-...\n  811│- **Invariant:** subscriber I/O never holds the `StreamLog` mutex (enqueue-under-lock, I/O outside, bounded per-subscriber queue; overflow = detach + resume-from-cursor); replay and fan-out halt at the FIRST failed subscriber write and the failed subscriber is REMOVED (attach + append + finish, all sites, `PresenceLog` mirrors included); the ring-peek fallback fires ONLY on the explicit unsupported-verb/old-broker answer; a deadline-poisoned replay is circuit-broken (global backoff, no reinstall until th...\n  812│- **spt-core mapping:** `spt-daemon/src/nethost.rs` `StreamLog::attach`/`push`/`finish` + `PresenceLog` mirrors + serve/wire worker pairing; `spt-daemon/src/dispatch.rs` `first_line` fallback arm + claim retry/backoff + worker spawn; SharedSend bounded-write deadline (round-4, KH 7.7 kin).\n     │...\n 1087│| 6.7 | Broker + brain are separate processes (brain restart never drops a hosted endpoint at the process level) | daemon process topology, self-update (ADR-0018) |\n*1088│| 6.8 | No irreversible durable-state migration before update ready-promotion (pre-ready writes stay N-1-readable) | auto-rollback / durable-state schema (ADR-0018) |\n 1089│| 6.10 | Phase-significant loop timing is a durable absolute-deadline grid (no per-fire write; update preserves phase, crash resets, one-shot never resets) | durable loop timing / self-update (ADR-0018 Q4) |\n*1090│| 6.11 | Brain respawn execs the applied bytes (canonical exe captured at broker start, not per-spawn current_exe) + promotion bytes-gate (exe_hash == artifact, else rollback) | daemon respawn path / self-update (ADR-0018 Q3) |\n 1091│| 6.12 | The echo-commune brief never transits the agent's `<id>-commune.md` (one writer on that path — the agent); the echo routes direct into the two-tier store | echo-commune seam / context tiering (releases#276) |\n 1092│| 6.13 | IO event tail reads cannot mint false zero; append exceeds the global max, reset repair preserves file order, and trim retains by position | `spt_store::iolog` |\n 1093│| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |\n\n### adr/\n#### 0018-broker-brain-process-isolation-restoration.md\n 23│1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.\n*24│2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** \"the executable path\" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-sp...\n 25│3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`...\n 26│4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update...\n 27│5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).\n*28│6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable ...\n 29│7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.\n 30│8. **Cross-platform uniformity (Q8).** The broker spawns the brain as a child process (`Command::spawn`) + socket IPC — no `exec`, so no Windows/Unix divergence. ConPTY/forkpty handles never leave the broker.\n 31│\n#### 0038-broker-stream-lifecycle-truth.md\n 26│## Decision\n*27│<!-- [doc->REQ-REDISPATCH-FINISHED-RETIRE] -->\n 28│\n 29│Stream rows carry explicit **lifecycle truth**, and redispatch consumes it:\n 30│","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1789528541208}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_5h0aXc92lXFViqL6kqaeMSgA|fc_0cf46cef591a107c016aaa09eccfcc87d2a251dbb188564dba","toolName":"grep","startedAt":"2026-09-16T03:15:56.896Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs:900-2144;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs:540-1200;.worktrees/49-267-refresh-disc…"},"intent":"Indexing remaining regression seams"},"id":"ea6a5da7","parentId":"bb82125b","timestamp":"2026-09-16T03:15:56.897Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_mdbQO4jmpWKrHWhnG3QIhopj|fc_0cf46cef591a107c016aaa09eccfdc87d2841318b82fd527e5","toolName":"read","startedAt":"2026-09-16T03:15:56.897Z","args":{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0038-broker-stream-lifecycle-truth.md:1-180"},"intent":"Reading lifecycle ADR amendments"},"id":"e5ad5656","parentId":"ea6a5da7","timestamp":"2026-09-16T03:15:56.897Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_S5clf1PH8hKC0DQ5jrlX3Drb|fc_0cf46cef591a107c016aaa09eccfe487d28778b88e65d5ff16","toolName":"grep","startedAt":"2026-09-16T03:15:56.897Z","args":{"path":".worktrees/49-267-refresh-discriminator/docs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs;.worktrees/49-267-refresh-discriminator/traceable-reqs.toml"},"intent":"Finding promotion exclusion evidence"},"id":"81e1e5e0","parentId":"e5ad5656","timestamp":"2026-09-16T03:15:56.897Z"}
{"type":"message","id":"bdd4b2c7","parentId":"81e1e5e0","timestamp":"2026-09-16T03:15:56.898Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_5h0aXc92lXFViqL6kqaeMSgA|fc_0cf46cef591a107c016aaa09eccfcc87d2a251dbb188564dba","name":"grep","arguments":{"i":"Indexing remaining regression seams","pattern":"^fn |pending_input|send_effect|restart|promot|by: Some","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs:900-2144;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs:540-1200;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs:535-1200;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/attach.rs","case":true,"gitignore":false,"skip":null},"intent":"Indexing remaining regression seams"},{"type":"toolCall","id":"call_mdbQO4jmpWKrHWhnG3QIhopj|fc_0cf46cef591a107c016aaa09eccfdc87d2841318b82fd527e5","name":"read","arguments":{"i":"Reading lifecycle ADR amendments","path":".worktrees/49-267-refresh-discriminator/docs/adr/0038-broker-stream-lifecycle-truth.md:1-180"},"intent":"Reading lifecycle ADR amendments"},{"type":"toolCall","id":"call_S5clf1PH8hKC0DQ5jrlX3Drb|fc_0cf46cef591a107c016aaa09eccfe487d28778b88e65d5ff16","name":"grep","arguments":{"i":"Finding promotion exclusion evidence","pattern":"by:Some|by: Some|by=Some|by.is_some|remote.*exclu|operator.*exclu","path":".worktrees/49-267-refresh-discriminator/docs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs;.worktrees/49-267-refresh-discriminator/traceable-reqs.toml","case":false,"gitignore":false,"skip":null},"intent":"Finding promotion exclusion evidence"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":18871,"output":332,"cacheRead":30080,"cacheWrite":0,"totalTokens":49283,"cost":{"input":0.18871000000000002,"output":0.0166,"cacheRead":0.03008,"cacheWrite":0,"total":0.23539000000000002}},"stopReason":"toolUse","timestamp":1789528541240,"responseId":"resp_0cf46cef591a107c016aaa09dd72a087d28bfaa68eb7886654","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0cf46cef591a107c016aaa09eccfcc87d2a251dbb188564dba","type":"function_call","status":"completed","arguments":"{\"i\":\"Indexing remaining regression seams\",\"pattern\":\"^fn |pending_input|send_effect|restart|promot|by: Some\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs:900-2144;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs:540-1200;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs:535-1200;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/attach.rs\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_5h0aXc92lXFViqL6kqaeMSgA","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"grep","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa09eccfdc87d2841318b82fd527e5","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading lifecycle ADR amendments\",\"path\":\".worktrees/49-267-refresh-discriminator/docs/adr/0038-broker-stream-lifecycle-truth.md:1-180\"}","call_id":"call_mdbQO4jmpWKrHWhnG3QIhopj","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"read","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa09eccfe487d28778b88e65d5ff16","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding promotion exclusion evidence\",\"pattern\":\"by:Some|by: Some|by=Some|by.is_some|remote.*exclu|operator.*exclu\",\"path\":\".worktrees/49-267-refresh-discriminator/docs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs;.worktrees/49-267-refresh-discriminator/traceable-reqs.toml\",\"case\":false,\"gitignore\":false,\"skip\":null}","call_id":"call_S5clf1PH8hKC0DQ5jrlX3Drb","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"grep","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}}]},"duration":15650.123399999924,"ttft":15601.542200000025,"completedAt":1789528556892,"contextSnapshot":{"promptTokens":48951,"nonMessageTokens":10304,"compactionEpoch":0}}}
{"type":"message","id":"edac1f8a","parentId":"bdd4b2c7","timestamp":"2026-09-16T03:15:56.916Z","message":{"role":"toolResult","toolCallId":"call_mdbQO4jmpWKrHWhnG3QIhopj|fc_0cf46cef591a107c016aaa09eccfdc87d2841318b82fd527e5","toolName":"read","content":[{"type":"text","text":"# ADR-0038: Broker stream-table lifecycle truth (redispatch eligibility)\n\n- **Status:** accepted (hertz RCA + doyle seam-verify 2026-07-16, dispatched same day); **amended 2026-07-16 (REDISPATCH-STALL — see Amendment below)**\n- **Context:** REDISPATCH-TRUTH milestone; RCA `.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`\n\n## Context\n\nThe broker's `NetShared.streams` table is append-only in practice: rows are\ninserted once (nethost.rs ~649) and `StreamLog::finish` only marks EOF —\nno removal or retirement path exists. Stream *classification* is derived at\nredispatch time by peeking ring seq 0, but the `StreamLog` data ring is\nbounded (4096 transport chunks) and evicts the protocol opener under\ntraffic. Dispatcher claims are a per-process one-shot `HashSet` insert\nperformed before worker spawn, never cleared or retried.\n\nAfter any brain cycle (`spt daemon refresh`, `spt update` apply — same\npath), the fresh `dispatch::run_dispatch_loop` therefore replays EVERY\nbroker-held peer stream, including finished historical attach streams. A\nreplayed historical Attach silently steals the live controller\n(same-identity `become_controller`, no `Displaced`) and its replayed EOF\nclears it — the operator sees a frozen remote PTY (field: 4/5 endpoints,\ntwice on 2026-07-16). A high-traffic active stream whose opener evicted\nclassifies Unknown/Failed and is permanently abandoned (claim burned\npre-spawn).\n\n## Decision\n<!-- [doc->REQ-REDISPATCH-FINISHED-RETIRE] -->\n\nStream rows carry explicit **lifecycle truth**, and redispatch consumes it:\n\n1. **Finished is terminal for redispatch.** A finished/terminal stream row\n   is retired from redispatch eligibility (removed from the enumeration the\n   dispatcher claims from, or excluded by lifecycle state). Only the\n   post-EOF state genuinely needed by other readers (presence, log reads)\n   survives. A fresh dispatcher never re-serves a terminal Attach.\n   *Gate round 2 ruling: retirement is the EXCLUDED-BY-LIFECYCLE arm —\n   eligibility-visibility only, never transport teardown. Removing the row\n   races the worker's fire-and-forget reply send and resets the\n   never-finished send half (requester sees torn-EOF instead of its reply);\n   physical teardown belongs to the connection-close sweep alone.*\n2. **Classification identity is restart-durable and ring-independent.**\n   Each broker-held inbound stream pins an immutable bounded opener/\n   classification fact (the complete first NDJSON record or its derived\n   family+cursor facts) OUTSIDE the evictable data ring, held until stream\n   close. Recovery classifies from that metadata, never from ring seq 0.\n3. **Claims are retryable, outcomes are classified.** The dispatcher\n   distinguishes active/retryable vs served vs finished. A transient\n   worker-setup failure releases/requeues the claim; terminal outcomes do\n   not hot-loop.\n4. **Live reconstruction semantics are unchanged.** The legitimate\n   same-operator successor re-take after a brain restart (controller\n   identity keyed on `by`) still silently re-takes; the discriminator for\n   replay suppression is stream lifecycle, never origin identity.\n\n## Consequences\n\n- Brain cycles (refresh/update) stop being a controller-integrity hazard:\n  historical streams can neither take nor clear a live controller, and\n  active streams survive opener eviction — verified by production-path\n  regressions (the prior e2e manually re-served a stream and bypassed\n  rediscovery entirely).\n- The stream table gains a retirement duty (bounded growth instead of\n  forever-discoverable rows) and a small per-stream metadata cost (one\n  pinned opener record per stream, ring untouched).\n- Rejected alternatives: clearing the whole stream table on brain restart\n  (destroys live streams' reconstruction facts along with the stale ones);\n  enlarging the data ring to preserve openers (unbounded memory for a\n  correctness fact); filtering replay by origin identity (breaks the\n  legitimate successor re-take, the exact silent-steal seam); a\n  per-generation claimed-set handoff (patches the symptom, keeps terminal\n  rows eligible forever).\n\n## Amendment — REDISPATCH-STALL (2026-07-16)\n<!-- [doc->REQ-DISPATCH-FALLBACK-CIRCUIT] -->\n<!-- [doc->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE] -->\n<!-- [doc->REQ-SERVE-OWNERSHIP-GENERATION] -->\n<!-- [doc->REQ-DISPATCH-HYGIENE-TELEMETRY] -->\n\n- **Status:** accepted (hertz v0.34 field-stall RCA + doyle seam-verify, same\n  day, both hours apart); RCA summary\n  `.claude/reports/2026-07-16-redispatch-stall/rca-summary.md`; hazard\n  KNOWN-HAZARDS 7.43; build shape `docs/REDISPATCH-STALL-TRIAGE.md`.\n\nThe W1 machinery this ADR shipped is field-correct on its own terms (D1/D1b\nhold: finished rows never serve) but two of its legs COMPOSE into a stall\nengine on live v0.34: Decision 3's retryable claims re-drive Decision 2's N-1\nring-peek fallback, whose guard arm is a broad `Err(_)`\n(`dispatch.rs:414` — intended old-broker-only, catches transport\ntimeout/EOF/poison too), each pass installing a throwaway peek subscriber\nwhose `StreamLog::attach` replays the whole retained ring UNDER the per-stream\nmutex with discarded write errors — one wedged subscriber conn = serial 15s\nbounded-write poison windows (33 observed, all 15,000–15,154 ms) composing\ninto 17–62s PTY/RC stalls.\n\nAmended decisions:\n\n1. **(amends Decision 2)** The ring-peek fallback fires ONLY on the explicit\n   unsupported-verb/old-broker answer. Every other opener-query error\n   (transport timeout, EOF, poison) is `Failed` → the bounded claim requeue —\n   never a second replay subscriber.\n2. **(new — subscriber write discipline)** Replay and live fan-out stop at\n   the FIRST failed subscriber write, and the failed subscriber is REMOVED\n   (attach replay, append, finish — all three sites). Subscriber I/O moves\n   OFF the `StreamLog` mutex: a bounded per-subscriber writer queue,\n   enqueue-under-lock / I/O-outside; queue overflow = detach that subscriber\n   + resume-from-cursor (the existing gapless-resume contract), never a\n   producer stall.\n3. **(new — bounded forwarding)** Attach worker completion and forwarding are\n   BOUNDED and cancelable: a poisoned brain subscriber cancels its paired\n   serve/wire worker (no orphan forwarding legs). Coordinate at build with\n   the DAEMON-LIFECYCLE C2 stream-lease work — same neighborhood, build once.\n4. **(amends Decision 3)** Retries are CLASSIFIED: a pre-setup transient may\n   retry; a deadline-poisoned replay is CIRCUIT-BROKEN (global backoff), and\n   a replacement subscriber is never installed until the prior subscriber is\n   fully gone. This is an upgrade of the bounded backoff, NOT a revert to the\n   v0.33 burn-the-claim abandonment (explicitly rejected).\n5. **(new — serve-side validation)** Terminal-exclusion is enforced pre-serve,\n   and attach/detach validate ownership/generation — a stale worker can never\n   detach a replacement controller. This also covers the UNFINISHED-stale-row\n   steal shape (raw-close no-FIN viewports) that Decision 1's finished-row\n   retirement cannot see.\n6. **(new — dispatcher hygiene + telemetry)** The redispatch worker pool is\n   bounded with batched cold enumeration and NO claim/sessions locks held\n   across I/O (clone-then-drop before `detach_if`/`info.json` writes, KH 7.12\n   kin); dispatch telemetry is keyed per\n   gen/stream/family/endpoint/attempt/conn with\n   replay/poison/cancel/worker-outcome events and gauges.\n\nRejected here: reverting retryable claims (restores permanent abandonment);\nwidening any write deadline (hides the composition instead of removing it);\ntearing down all subscribers on any poison (punishes healthy streams for a\nwedged conn)."}],"details":{"totalLines":132,"displayContent":{"text":"# ADR-0038: Broker stream-table lifecycle truth (redispatch eligibility)\r\n\r\n- **Status:** accepted (hertz RCA + doyle seam-verify 2026-07-16, dispatched same day); **amended 2026-07-16 (REDISPATCH-STALL — see Amendment below)**\r\n- **Context:** REDISPATCH-TRUTH milestone; RCA `.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`\r\n\r\n## Context\r\n\r\nThe broker's `NetShared.streams` table is append-only in practice: rows are\r\ninserted once (nethost.rs ~649) and `StreamLog::finish` only marks EOF —\r\nno removal or retirement path exists. Stream *classification* is derived at\r\nredispatch time by peeking ring seq 0, but the `StreamLog` data ring is\r\nbounded (4096 transport chunks) and evicts the protocol opener under\r\ntraffic. Dispatcher claims are a per-process one-shot `HashSet` insert\r\nperformed before worker spawn, never cleared or retried.\r\n\r\nAfter any brain cycle (`spt daemon refresh`, `spt update` apply — same\r\npath), the fresh `dispatch::run_dispatch_loop` therefore replays EVERY\r\nbroker-held peer stream, including finished historical attach streams. A\r\nreplayed historical Attach silently steals the live controller\r\n(same-identity `become_controller`, no `Displaced`) and its replayed EOF\r\nclears it — the operator sees a frozen remote PTY (field: 4/5 endpoints,\r\ntwice on 2026-07-16). A high-traffic active stream whose opener evicted\r\nclassifies Unknown/Failed and is permanently abandoned (claim burned\r\npre-spawn).\r\n\r\n## Decision\r\n<!-- [doc->REQ-REDISPATCH-FINISHED-RETIRE] -->\r\n\r\nStream rows carry explicit **lifecycle truth**, and redispatch consumes it:\r\n\r\n1. **Finished is terminal for redispatch.** A finished/terminal stream row\r\n   is retired from redispatch eligibility (removed from the enumeration the\r\n   dispatcher claims from, or excluded by lifecycle state). Only the\r\n   post-EOF state genuinely needed by other readers (presence, log reads)\r\n   survives. A fresh dispatcher never re-serves a terminal Attach.\r\n   *Gate round 2 ruling: retirement is the EXCLUDED-BY-LIFECYCLE arm —\r\n   eligibility-visibility only, never transport teardown. Removing the row\r\n   races the worker's fire-and-forget reply send and resets the\r\n   never-finished send half (requester sees torn-EOF instead of its reply);\r\n   physical teardown belongs to the connection-close sweep alone.*\r\n2. **Classification identity is restart-durable and ring-independent.**\r\n   Each broker-held inbound stream pins an immutable bounded opener/\r\n   classification fact (the complete first NDJSON record or its derived\r\n   family+cursor facts) OUTSIDE the evictable data ring, held until stream\r\n   close. Recovery classifies from that metadata, never from ring seq 0.\r\n3. **Claims are retryable, outcomes are classified.** The dispatcher\r\n   distinguishes active/retryable vs served vs finished. A transient\r\n   worker-setup failure releases/requeues the claim; terminal outcomes do\r\n   not hot-loop.\r\n4. **Live reconstruction semantics are unchanged.** The legitimate\r\n   same-operator successor re-take after a brain restart (controller\r\n   identity keyed on `by`) still silently re-takes; the discriminator for\r\n   replay suppression is stream lifecycle, never origin identity.\r\n\r\n## Consequences\r\n\r\n- Brain cycles (refresh/update) stop being a controller-integrity hazard:\r\n  historical streams can neither take nor clear a live controller, and\r\n  active streams survive opener eviction — verified by production-path\r\n  regressions (the prior e2e manually re-served a stream and bypassed\r\n  rediscovery entirely).\r\n- The stream table gains a retirement duty (bounded growth instead of\r\n  forever-discoverable rows) and a small per-stream metadata cost (one\r\n  pinned opener record per stream, ring untouched).\r\n- Rejected alternatives: clearing the whole stream table on brain restart\r\n  (destroys live streams' reconstruction facts along with the stale ones);\r\n  enlarging the data ring to preserve openers (unbounded memory for a\r\n  correctness fact); filtering replay by origin identity (breaks the\r\n  legitimate successor re-take, the exact silent-steal seam); a\r\n  per-generation claimed-set handoff (patches the symptom, keeps terminal\r\n  rows eligible forever).\r\n\r\n## Amendment — REDISPATCH-STALL (2026-07-16)\r\n<!-- [doc->REQ-DISPATCH-FALLBACK-CIRCUIT] -->\r\n<!-- [doc->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE] -->\r\n<!-- [doc->REQ-SERVE-OWNERSHIP-GENERATION] -->\r\n<!-- [doc->REQ-DISPATCH-HYGIENE-TELEMETRY] -->\r\n\r\n- **Status:** accepted (hertz v0.34 field-stall RCA + doyle seam-verify, same\r\n  day, both hours apart); RCA summary\r\n  `.claude/reports/2026-07-16-redispatch-stall/rca-summary.md`; hazard\r\n  KNOWN-HAZARDS 7.43; build shape `docs/REDISPATCH-STALL-TRIAGE.md`.\r\n\r\nThe W1 machinery this ADR shipped is field-correct on its own terms (D1/D1b\r\nhold: finished rows never serve) but two of its legs COMPOSE into a stall\r\nengine on live v0.34: Decision 3's retryable claims re-drive Decision 2's N-1\r\nring-peek fallback, whose guard arm is a broad `Err(_)`\r\n(`dispatch.rs:414` — intended old-broker-only, catches transport\r\ntimeout/EOF/poison too), each pass installing a throwaway peek subscriber\r\nwhose `StreamLog::attach` replays the whole retained ring UNDER the per-stream\r\nmutex with discarded write errors — one wedged subscriber conn = serial 15s\r\nbounded-write poison windows (33 observed, all 15,000–15,154 ms) composing\r\ninto 17–62s PTY/RC stalls.\r\n\r\nAmended decisions:\r\n\r\n1. **(amends Decision 2)** The ring-peek fallback fires ONLY on the explicit\r\n   unsupported-verb/old-broker answer. Every other opener-query error\r\n   (transport timeout, EOF, poison) is `Failed` → the bounded claim requeue —\r\n   never a second replay subscriber.\r\n2. **(new — subscriber write discipline)** Replay and live fan-out stop at\r\n   the FIRST failed subscriber write, and the failed subscriber is REMOVED\r\n   (attach replay, append, finish — all three sites). Subscriber I/O moves\r\n   OFF the `StreamLog` mutex: a bounded per-subscriber writer queue,\r\n   enqueue-under-lock / I/O-outside; queue overflow = detach that subscriber\r\n   + resume-from-cursor (the existing gapless-resume contract), never a\r\n   producer stall.\r\n3. **(new — bounded forwarding)** Attach worker completion and forwarding are\r\n   BOUNDED and cancelable: a poisoned brain subscriber cancels its paired\r\n   serve/wire worker (no orphan forwarding legs). Coordinate at build with\r\n   the DAEMON-LIFECYCLE C2 stream-lease work — same neighborhood, build once.\r\n4. **(amends Decision 3)** Retries are CLASSIFIED: a pre-setup transient may\r\n   retry; a deadline-poisoned replay is CIRCUIT-BROKEN (global backoff), and\r\n   a replacement subscriber is never installed until the prior subscriber is\r\n   fully gone. This is an upgrade of the bounded backoff, NOT a revert to the\r\n   v0.33 burn-the-claim abandonment (explicitly rejected).\r\n5. **(new — serve-side validation)** Terminal-exclusion is enforced pre-serve,\r\n   and attach/detach validate ownership/generation — a stale worker can never\r\n   detach a replacement controller. This also covers the UNFINISHED-stale-row\r\n   steal shape (raw-close no-FIN viewports) that Decision 1's finished-row\r\n   retirement cannot see.\r\n6. **(new — dispatcher hygiene + telemetry)** The redispatch worker pool is\r\n   bounded with batched cold enumeration and NO claim/sessions locks held\r\n   across I/O (clone-then-drop before `detach_if`/`info.json` writes, KH 7.12\r\n   kin); dispatch telemetry is keyed per\r\n   gen/stream/family/endpoint/attempt/conn with\r\n   replay/poison/cancel/worker-outcome events and gauges.\r\n\r\nRejected here: reverting retryable claims (restores permanent abandonment);\r\nwidening any write deadline (hides the composition instead of removing it);\r\ntearing down all subscribers on any poison (punishes healthy streams for a\r\nwedged conn).\r","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132]},"fileSize":7837,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\docs\\adr\\0038-broker-stream-lifecycle-truth.md"}}},"isError":false,"timestamp":1789528556915}}
{"type":"message","id":"76afebfa","parentId":"edac1f8a","timestamp":"2026-09-16T03:15:56.943Z","message":{"role":"toolResult","toolCallId":"call_5h0aXc92lXFViqL6kqaeMSgA|fc_0cf46cef591a107c016aaa09eccfcc87d2a251dbb188564dba","toolName":"grep","content":[{"type":"text","text":"# .worktrees/49-267-refresh-discriminator/crates/spt-daemon/\n\n## tests/\n### attach.rs\n 925|                            life1\n*926|                                .send_effect(MintedOp::new(Minter::Rc, op_id), &b)\n 927|                                .expect(\"journaled input\");\n 928|                        }\n 929|                        _ => {}\n...\n 1003|    // the resulting authoritative screen still shows BOTH markers. The screen is the\n*1004|    // idempotent truth: a GAP from the restart/replay (a lost record) would leave a\n 1005|    // marker off the final screen. MARKER_ONE proves the pre-crash output survived\n 1006|    // the handoff + re-serve; MARKER_TWO proves the dead-window buffered input was\n 1007|    // delivered after life 2 took over. Rendering (vs a raw substring scan) is what\n...\n 1014|        \"the operator's rendered screen still shows the pre-crash marker across the \\\n*1015|         brain restart (no gap in the re-served output): {painted:?}\"\n 1016|    );\n 1017|    assert!(\n 1018|        painted.contains(\"MARKER_TWO\"),\n 1019|        \"the operator's rendered screen shows the dead-window marker delivered after \\\n*1020|         the handoff (buffered input survived the restart): {painted:?}\"\n 1021|    );\n 1022|\n 1023|    // Ground truth: the session's OWN authoritative screen (a fresh local attach →\n...\n 1040|#[test]\n*1041|fn attach_registers_remote_drive_detection() {\n 1042|    let _home = init_home();\n 1043|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 1044|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 1173|/// sid). The spawner is the broker's by=None local controller placeholder.\n*1174|fn labeled_session(home: &TestHome, label: &str) -> (Arc<Broker>, String, Brain, u64) {\n 1175|    let name = unique_name();\n 1176|    let broker = net_broker(&name, &home.path().join(label));\n 1177|    let mut spawner = connect_retry(&name);\n...\n 1188|/// replayed output / size frames), returning the resolved role.\n*1189|fn read_outcome(brain: &mut Brain) -> SubscribeOutcome {\n 1190|    let deadline = brain.call_deadline();\n 1191|    for _ in 0..2000 {\n 1192|        match brain.read_event_until(deadline).expect(\"event\") {\n...\n 1205|#[test]\n*1206|fn controller_viewer_matrix_and_loud_take() {\n 1207|    let _home = init_home();\n 1208|    let (_broker, name, _spawner, sid) = labeled_session(&_home, \"cv-matrix\");\n 1209|\n...\n 1264|// [int->REQ-RCVIEW-1] GATE #7 (the self-kick trap): a SAME-origin re-subscribe —\n*1265|// a successor re-taking the controller slot after a brain restart — silently\n 1266|// re-takes (Controller), and NEVER fires Displaced to the prior holder. Only a\n 1267|// DIFFERENT origin's Take is loud. Here the same `by` re-subscribes and we assert\n 1268|// no Displaced reaches the first connection.\n 1269|#[test]\n*1270|fn same_origin_re_subscribe_does_not_displace() {\n 1271|    let _home = init_home();\n 1272|    let (_broker, name, _spawner, sid) = labeled_session(&_home, \"cv-gate7\");\n 1273|\n...\n 1289|    // The \"successor\" re-serves the SAME operator's stream (same by) on a new\n*1290|    // connection — the brain-restart re-take.\n 1291|    let mut successor = connect_retry(&name);\n 1292|    successor\n 1293|        .attach_as(sid, 0, AttachIntent::Control, 0, Some(\"node-A\"))\n...\n 1304|    successor\n*1305|        .send_effect(MintedOp::new(Minter::Rc, 7), b\"GATE7\\r\")\n 1306|        .expect(\"drive\");\n 1307|    let mut saw_displaced = false;\n 1308|    for _ in 0..50 {\n...\n 1329|#[test]\n*1330|fn resize_is_controller_exclusive() {\n 1331|    let _home = init_home();\n 1332|    let (_broker, name, _spawner, sid) = labeled_session(&_home, \"cv-resize\");\n 1333|\n...\n 1417|#[test]\n*1418|fn wedged_viewer_does_not_stall_controller() {\n 1419|    let _home = init_home();\n 1420|    let (_broker, name, _spawner, sid) = labeled_session(&_home, \"cv-isolation\");\n 1421|\n...\n 1442|    let big = vec![b'x'; 200_000];\n*1443|    ctrl.send_effect(MintedOp::new(Minter::Rc, 1), &big)\n 1444|        .expect(\"burst\");\n*1445|    ctrl.send_effect(MintedOp::new(Minter::Rc, 2), b\"\\rISOLATED\\r\")\n 1446|        .expect(\"marker\");\n 1447|\n 1448|    let mut got = Vec::new();\n...\n 1506|\n*1507|fn fresh_viewer_sees_ring_burst(name: &str, sid: u64) -> bool {\n 1508|    let mut probe = Brain::cold_start_pump(\n 1509|        name,\n 1510|        1,\n...\n 1534|/// deadline (the viewer rides a pump conn so the per-slice timeout is honored).\n*1535|fn viewer_reads_marker_no_displace(brain: &mut Brain, needle: &[u8]) -> bool {\n 1536|    let mut got = Vec::new();\n 1537|    let deadline = std::time::Instant::now() + Duration::from_secs(15);\n 1538|    while std::time::Instant::now() < deadline {\n...\n 1560|#[test]\n*1561|fn controller_restart_with_viewer_no_displace_and_viewer_survives() {\n 1562|    let _home = init_home();\n 1563|    let (_broker, name, _spawner, sid) = labeled_session(&_home, \"cv-gate7-combined\");\n 1564|\n...\n 1589|    // The viewer renders pre-crash output (it is genuinely live).\n*1590|    ctrl.send_effect(MintedOp::new(Minter::Rc, 1), b\"BEFORE\\r\")\n 1591|        .expect(\"before marker\");\n 1592|    assert!(\n 1593|        viewer_reads_marker_no_displace(&mut view, b\"BEFORE\"),\n### redispatch.rs\n 573|// via SPT_STREAM_RING_CHUNKS — production sizing untouched), the dispatcher\n*574|// generation restarts, and DURABLE classification (the broker-pinned opener,\n*575|// never ring seq 0) resumes the SAME operator stream: post-restart input\n 576|// renders. Pre-fix this classified Unknown/Failed with the claim burned —\n 577|// the stream was permanently abandoned.\n 578|#[test]\n*579|fn opener_evicted_active_stream_resumes_after_dispatcher_restart() {\n 580|    let _home = init_home();\n 581|    // Shrink the ring BEFORE the broker starts (read once at NetShared::new).\n 582|    // nextest = one process per test: no cross-test env bleed.\n...\n 688|#[test]\n*689|fn transient_worker_start_failure_recovers_via_claim_retry() {\n 690|    let _home = init_home();\n 691|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 692|    let (name_a, name_b) = (unique_name(), unique_name());\n### redispatch_stall.rs\n 545|#[test]\n*546|fn opener_transient_is_bounded_requeue_never_a_peek_subscriber() {\n 547|    let _home = init_home();\n 548|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 549|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 606|#[test]\n*607|fn seat_blocked_failure_trips_the_breaker_and_the_stream_recovers() {\n 608|    let _home = init_home();\n 609|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 610|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 681|// row's replayed Request carries an OLDER attach generation than the\n*682|// replacement and must neither TAKE nor CLEAR it: post-restart input keeps\n 683|// rendering across several dispatcher polls with no EOF (pre-fix: the\n 684|// replayed Request silently re-took the same-identity controller and the\n 685|// stale worker's terminal detach cleared the replacement — the frozen-PTY /\n...\n 687|#[test]\n*688|fn stale_unfinished_attach_replay_neither_takes_nor_clears_the_replacement_controller() {\n 689|    let _home = init_home();\n 690|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 691|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 808|#[test]\n*809|fn old_broker_unknown_verb_answer_falls_back_to_the_peek_and_serves_real_traffic() {\n 810|    let _home = init_home();\n 811|    // The N-1 broker image: the opener verb answers like a broker that\n 812|    // predates it. Set BEFORE bind; nextest = process-per-test.\n\n## src/\n### attach.rs\n 9|//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\n*10|//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\n 11|//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\n 12|//!   journal (a replayed input record cannot double-type);\n 13|//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\n...\n 23|//! `Input.op_id`, and the operator's viewport dedups `Output` records by their\n*24|//! session-log `seq` (the render cursor). A post-restart re-serve may\n 25|//! re-transmit output records on the wire — duplicates are dropped at the\n 26|//! render cursor, the same discipline the local `OutputLog` replay uses\n 27|//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\n...\n 38|//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\n*39|//! A restarted target brain re-attaches the session from its output cursor,\n 40|//! resubscribes the stream from its net cursor, and re-drives journaled sends;\n*41|//! a restarted operator brain resubscribes and dedups rendered chunks by\n 42|//! `Output.seq`.\n 43|\n 44|use std::io;\n...\n 80|// [impl->REQ-ACL-RC-VIEW-SPLIT]\n*81|fn attach_surface(intent: AttachIntent) -> &'static str {\n 82|    match intent {\n 83|        AttachIntent::Viewer => surface::RC_VIEW,\n 84|        AttachIntent::Control | AttachIntent::Take => surface::RC_ATTACH,\n...\n 148|// [impl->REQ-INST-3]\n*149|fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {\n 150|    let _ = crate::resting::daemon_rest_event(endpoint, event, None);\n 151|}\n 152|\n...\n 218|/// error propagates unchanged.\n*219|fn is_output_gap(e: &io::Error) -> bool {\n 220|    e.kind() == io::ErrorKind::InvalidData && e.to_string().contains(\"output gap\")\n 221|}\n 222|\n...\n 366|#[allow(clippy::too_many_arguments)]\n*367|fn handle_attach_request(\n 368|    brain: &mut Brain,\n 369|    wire: &mut Brain,\n 370|    stream_id: u64,\n...\n 446|/// is being attached, for the gate. `net_from_seq` is this brain's resume\n*447|/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n 448|/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n 449|/// journal, and re-pumped output dedups at the operator's render cursor).\n 450|///\n...\n 511|    // controller input that races ahead of the reply is BUFFERED, not dropped\n*512|    // (the replayed-input-after-restart case) and not leaked to the PTY before\n 513|    // we know we control the session (the busy-refuse case).\n 514|    let mut role: Option<ServeRole> = None;\n*515|    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\n 516|    // Temporary W2b discriminator: paired boundaries, never payload contents.\n 517|    let w2b_trace = std::env::var(\"SPT_PUMP_TRACE\").as_deref() == Ok(\"1\");\n 518|    macro_rules! w2b_probe {\n...\n 522|                    now_ms(), std::process::id(), std::thread::current().id(), stream_id,\n*523|                    pending_input.len(), $label);\n 524|            }\n 525|            let result = $call;\n 526|            if w2b_trace {\n...\n 528|                    now_ms(), std::process::id(), std::thread::current().id(), stream_id,\n*529|                    pending_input.len(), $label);\n 530|            }\n 531|            result\n 532|        }};\n...\n 638|                now_ms(), std::process::id(), std::thread::current().id(), stream_id,\n*639|                pending_input.len(), kind, role);\n 640|        }\n 641|        match event {\n 642|            // ── Wire → session: requests, input, resize. ───────────────────\n...\n 703|                                    // so it carries the `rc` journal tag (ADR-0034).\n*704|                                    w2b_probe!(\"live_input\", brain.send_effect_no_ack(\n 705|                                        MintedOp::new(Minter::Rc, op_id),\n 706|                                        &bytes,\n 707|                                    ))?\n...\n 710|                                // controller confirmation, discarded on busy/viewer.\n*711|                                None => pending_input.push((op_id, bytes)),\n 712|                                // A viewer never reaches here (intent gate).\n 713|                                Some(ServeRole::Viewer) => {}\n 714|                            }\n...\n 783|                        // minted ops carry the `rc` tag).\n*784|                        let pending_count = pending_input.len();\n 785|                        if w2b_trace {\n 786|                            eprintln!(\"[DEBUG-w2b] ms={} pid={} thread={:?} stream={} pending={} enter=pending_flush\",\n 787|                                now_ms(), std::process::id(), std::thread::current().id(), stream_id, pending_count);\n 788|                        }\n*789|                        for (index, (op, b)) in pending_input.drain(..).enumerate() {\n 790|                            if w2b_trace {\n*791|                                eprintln!(\"[DEBUG-w2b] ms={} pid={} thread={:?} stream={} remaining={} op={} bytes={} enter=pending_send_effect\",\n 792|                                    now_ms(), std::process::id(), std::thread::current().id(), stream_id, pending_count - index, op, b.len());\n 793|                            }\n*794|                            let sent = brain.send_effect(MintedOp::new(Minter::Rc, op), &b);\n 795|                            if w2b_trace {"}],"details":{"scopePath":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs, .worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs, .worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs, .worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/attach.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":49,"fileCount":4,"files":[".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/attach.rs"],"fileMatches":[{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs","count":20},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs","count":4},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs","count":5},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/attach.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# .worktrees/49-267-refresh-discriminator/crates/spt-daemon/\n\n## tests/\n### attach.rs\n  925│                            life1\n *926│                                .send_effect(MintedOp::new(Minter::Rc, op_id), &b)\n  927│                                .expect(\"journaled input\");\n  928│                        }\n  929│                        _ => {}\n     │...\n 1003│    // the resulting authoritative screen still shows BOTH markers. The screen is the\n*1004│    // idempotent truth: a GAP from the restart/replay (a lost record) would leave a\n 1005│    // marker off the final screen. MARKER_ONE proves the pre-crash output survived\n 1006│    // the handoff + re-serve; MARKER_TWO proves the dead-window buffered input was\n 1007│    // delivered after life 2 took over. Rendering (vs a raw substring scan) is what\n     │...\n 1014│        \"the operator's rendered screen still shows the pre-crash marker across the \\\n*1015│         brain restart (no gap in the re-served output): {painted:?}\"\n 1016│    );\n 1017│    assert!(\n 1018│        painted.contains(\"MARKER_TWO\"),\n 1019│        \"the operator's rendered screen shows the dead-window marker delivered after \\\n*1020│         the handoff (buffered input survived the restart): {painted:?}\"\n 1021│    );\n 1022│\n 1023│    // Ground truth: the session's OWN authoritative screen (a fresh local attach →\n     │...\n 1040│#[test]\n*1041│fn attach_registers_remote_drive_detection() {\n 1042│    let _home = init_home();\n 1043│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 1044│    let (name_a, name_b) = (unique_name(), unique_name());\n     │...\n 1173│/// sid). The spawner is the broker's by=None local controller placeholder.\n*1174│fn labeled_session(home: &TestHome, label: &str) -> (Arc<Broker>, String, Brain, u64) {\n 1175│    let name = unique_name();\n 1176│    let broker = net_broker(&name, &home.path().join(label));\n 1177│    let mut spawner = connect_retry(&name);\n     │...\n 1188│/// replayed output / size frames), returning the resolved role.\n*1189│fn read_outcome(brain: &mut Brain) -> SubscribeOutcome {\n 1190│    let deadline = brain.call_deadline();\n 1191│    for _ in 0..2000 {\n 1192│        match brain.read_event_until(deadline).expect(\"event\") {\n     │...\n 1205│#[test]\n*1206│fn controller_viewer_matrix_and_loud_take() {\n 1207│    let _home = init_home();\n 1208│    let (_broker, name, _spawner, sid) = labeled_session(&_home, \"cv-matrix\");\n 1209│\n     │...\n 1264│// [int->REQ-RCVIEW-1] GATE #7 (the self-kick trap): a SAME-origin re-subscribe —\n*1265│// a successor re-taking the controller slot after a brain restart — silently\n 1266│// re-takes (Controller), and NEVER fires Displaced to the prior holder. Only a\n 1267│// DIFFERENT origin's Take is loud. Here the same `by` re-subscribes and we assert\n 1268│// no Displaced reaches the first connection.\n 1269│#[test]\n*1270│fn same_origin_re_subscribe_does_not_displace() {\n 1271│    let _home = init_home();\n 1272│    let (_broker, name, _spawner, sid) = labeled_session(&_home, \"cv-gate7\");\n 1273│\n     │...\n 1289│    // The \"successor\" re-serves the SAME operator's stream (same by) on a new\n*1290│    // connection — the brain-restart re-take.\n 1291│    let mut successor = connect_retry(&name);\n 1292│    successor\n 1293│        .attach_as(sid, 0, AttachIntent::Control, 0, Some(\"node-A\"))\n     │...\n 1304│    successor\n*1305│        .send_effect(MintedOp::new(Minter::Rc, 7), b\"GATE7\\r\")\n 1306│        .expect(\"drive\");\n 1307│    let mut saw_displaced = false;\n 1308│    for _ in 0..50 {\n     │...\n 1329│#[test]\n*1330│fn resize_is_controller_exclusive() {\n 1331│    let _home = init_home();\n 1332│    let (_broker, name, _spawner, sid) = labeled_session(&_home, \"cv-resize\");\n 1333│\n     │...\n 1417│#[test]\n*1418│fn wedged_viewer_does_not_stall_controller() {\n 1419│    let _home = init_home();\n 1420│    let (_broker, name, _spawner, sid) = labeled_session(&_home, \"cv-isolation\");\n 1421│\n     │...\n 1442│    let big = vec![b'x'; 200_000];\n*1443│    ctrl.send_effect(MintedOp::new(Minter::Rc, 1), &big)\n 1444│        .expect(\"burst\");\n*1445│    ctrl.send_effect(MintedOp::new(Minter::Rc, 2), b\"\\rISOLATED\\r\")\n 1446│        .expect(\"marker\");\n 1447│\n 1448│    let mut got = Vec::new();\n     │...\n 1506│\n*1507│fn fresh_viewer_sees_ring_burst(name: &str, sid: u64) -> bool {\n 1508│    let mut probe = Brain::cold_start_pump(\n 1509│        name,\n 1510│        1,\n     │...\n 1534│/// deadline (the viewer rides a pump conn so the per-slice timeout is honored).\n*1535│fn viewer_reads_marker_no_displace(brain: &mut Brain, needle: &[u8]) -> bool {\n 1536│    let mut got = Vec::new();\n 1537│    let deadline = std::time::Instant::now() + Duration::from_secs(15);\n 1538│    while std::time::Instant::now() < deadline {\n     │...\n 1560│#[test]\n*1561│fn controller_restart_with_viewer_no_displace_and_viewer_survives() {\n 1562│    let _home = init_home();\n 1563│    let (_broker, name, _spawner, sid) = labeled_session(&_home, \"cv-gate7-combined\");\n 1564│\n     │...\n 1589│    // The viewer renders pre-crash output (it is genuinely live).\n*1590│    ctrl.send_effect(MintedOp::new(Minter::Rc, 1), b\"BEFORE\\r\")\n 1591│        .expect(\"before marker\");\n 1592│    assert!(\n 1593│        viewer_reads_marker_no_displace(&mut view, b\"BEFORE\"),\n### redispatch.rs\n 573│// via SPT_STREAM_RING_CHUNKS — production sizing untouched), the dispatcher\n*574│// generation restarts, and DURABLE classification (the broker-pinned opener,\n*575│// never ring seq 0) resumes the SAME operator stream: post-restart input\n 576│// renders. Pre-fix this classified Unknown/Failed with the claim burned —\n 577│// the stream was permanently abandoned.\n 578│#[test]\n*579│fn opener_evicted_active_stream_resumes_after_dispatcher_restart() {\n 580│    let _home = init_home();\n 581│    // Shrink the ring BEFORE the broker starts (read once at NetShared::new).\n 582│    // nextest = one process per test: no cross-test env bleed.\n    │...\n 688│#[test]\n*689│fn transient_worker_start_failure_recovers_via_claim_retry() {\n 690│    let _home = init_home();\n 691│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 692│    let (name_a, name_b) = (unique_name(), unique_name());\n### redispatch_stall.rs\n 545│#[test]\n*546│fn opener_transient_is_bounded_requeue_never_a_peek_subscriber() {\n 547│    let _home = init_home();\n 548│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 549│    let (name_a, name_b) = (unique_name(), unique_name());\n    │...\n 606│#[test]\n*607│fn seat_blocked_failure_trips_the_breaker_and_the_stream_recovers() {\n 608│    let _home = init_home();\n 609│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 610│    let (name_a, name_b) = (unique_name(), unique_name());\n    │...\n 681│// row's replayed Request carries an OLDER attach generation than the\n*682│// replacement and must neither TAKE nor CLEAR it: post-restart input keeps\n 683│// rendering across several dispatcher polls with no EOF (pre-fix: the\n 684│// replayed Request silently re-took the same-identity controller and the\n 685│// stale worker's terminal detach cleared the replacement — the frozen-PTY /\n    │...\n 687│#[test]\n*688│fn stale_unfinished_attach_replay_neither_takes_nor_clears_the_replacement_controller() {\n 689│    let _home = init_home();\n 690│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 691│    let (name_a, name_b) = (unique_name(), unique_name());\n    │...\n 808│#[test]\n*809│fn old_broker_unknown_verb_answer_falls_back_to_the_peek_and_serves_real_traffic() {\n 810│    let _home = init_home();\n 811│    // The N-1 broker image: the opener verb answers like a broker that\n 812│    // predates it. Set BEFORE bind; nextest = process-per-test.\n\n## src/\n### attach.rs\n   9│//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\n *10│//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\n  11│//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\n  12│//!   journal (a replayed input record cannot double-type);\n  13│//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\n    │...\n  23│//! `Input.op_id`, and the operator's viewport dedups `Output` records by their\n *24│//! session-log `seq` (the render cursor). A post-restart re-serve may\n  25│//! re-transmit output records on the wire — duplicates are dropped at the\n  26│//! render cursor, the same discipline the local `OutputLog` replay uses\n  27│//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\n    │...\n  38│//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\n *39│//! A restarted target brain re-attaches the session from its output cursor,\n  40│//! resubscribes the stream from its net cursor, and re-drives journaled sends;\n *41│//! a restarted operator brain resubscribes and dedups rendered chunks by\n  42│//! `Output.seq`.\n  43│\n  44│use std::io;\n    │...\n  80│// [impl->REQ-ACL-RC-VIEW-SPLIT]\n *81│fn attach_surface(intent: AttachIntent) -> &'static str {\n  82│    match intent {\n  83│        AttachIntent::Viewer => surface::RC_VIEW,\n  84│        AttachIntent::Control | AttachIntent::Take => surface::RC_ATTACH,\n    │...\n 148│// [impl->REQ-INST-3]\n*149│fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {\n 150│    let _ = crate::resting::daemon_rest_event(endpoint, event, None);\n 151│}\n 152│\n    │...\n 218│/// error propagates unchanged.\n*219│fn is_output_gap(e: &io::Error) -> bool {\n 220│    e.kind() == io::ErrorKind::InvalidData && e.to_string().contains(\"output gap\")\n 221│}\n 222│\n    │...\n 366│#[allow(clippy::too_many_arguments)]\n*367│fn handle_attach_request(\n 368│    brain: &mut Brain,\n 369│    wire: &mut Brain,\n 370│    stream_id: u64,\n    │...\n 446│/// is being attached, for the gate. `net_from_seq` is this brain's resume\n*447│/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n 448│/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n 449│/// journal, and re-pumped output dedups at the operator's render cursor).\n 450│///\n    │...\n 511│    // controller input that races ahead of the reply is BUFFERED, not dropped\n*512│    // (the replayed-input-after-restart case) and not leaked to the PTY before\n 513│    // we know we control the session (the busy-refuse case).\n 514│    let mut role: Option<ServeRole> = None;\n*515│    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\n 516│    // Temporary W2b discriminator: paired boundaries, never payload contents.\n 517│    let w2b_trace = std::env::var(\"SPT_PUMP_TRACE\").as_deref() == Ok(\"1\");\n 518│    macro_rules! w2b_probe {\n    │...\n 522│                    now_ms(), std::process::id(), std::thread::current().id(), stream_id,\n*523│                    pending_input.len(), $label);\n 524│            }\n 525│            let result = $call;\n 526│            if w2b_trace {\n    │...\n 528│                    now_ms(), std::process::id(), std::thread::current().id(), stream_id,\n*529│                    pending_input.len(), $label);\n 530│            }\n 531│            result\n 532│        }};\n    │...\n 638│                now_ms(), std::process::id(), std::thread::current().id(), stream_id,\n*639│                pending_input.len(), kind, role);\n 640│        }\n 641│        match event {\n 642│            // ── Wire → session: requests, input, resize. ───────────────────\n    │...\n 703│                                    // so it carries the `rc` journal tag (ADR-0034).\n*704│                                    w2b_probe!(\"live_input\", brain.send_effect_no_ack(\n 705│                                        MintedOp::new(Minter::Rc, op_id),\n 706│                                        &bytes,\n 707│                                    ))?\n    │...\n 710│                                // controller confirmation, discarded on busy/viewer.\n*711│                                None => pending_input.push((op_id, bytes)),\n 712│                                // A viewer never reaches here (intent gate).\n 713│                                Some(ServeRole::Viewer) => {}\n 714│                            }\n    │...\n 783│                        // minted ops carry the `rc` tag).\n*784│                        let pending_count = pending_input.len();\n 785│                        if w2b_trace {\n 786│                            eprintln!(\"[DEBUG-w2b] ms={} pid={} thread={:?} stream={} pending={} enter=pending_flush\",\n 787│                                now_ms(), std::process::id(), std::thread::current().id(), stream_id, pending_count);\n 788│                        }\n*789│                        for (index, (op, b)) in pending_input.drain(..).enumerate() {\n 790│                            if w2b_trace {\n*791│                                eprintln!(\"[DEBUG-w2b] ms={} pid={} thread={:?} stream={} remaining={} op={} bytes={} enter=pending_send_effect\",\n 792│                                    now_ms(), std::process::id(), std::thread::current().id(), stream_id, pending_count - index, op, b.len());\n 793│                            }\n*794│                            let sent = brain.send_effect(MintedOp::new(Minter::Rc, op), &b);\n 795│                            if w2b_trace {"},"isError":false,"timestamp":1789528556943}}
{"type":"message","id":"92b62a80","parentId":"76afebfa","timestamp":"2026-09-16T03:15:56.988Z","message":{"role":"toolResult","toolCallId":"call_S5clf1PH8hKC0DQ5jrlX3Drb|fc_0cf46cef591a107c016aaa09eccfe487d28778b88e65d5ff16","toolName":"grep","content":[{"type":"text","text":"# .worktrees/49-267-refresh-discriminator/\n## traceable-reqs.toml\n 547|id = \"REQ-RUN-SHORTCUT\"\n*548|title = \"`<basename>-<id>` launcher shortcut generation (picker `s` keybind, M12-W2-T2.4): from any pre-start options set the picker writes/updates a `<basename>-<id>` launcher at the project root whose body is `spt go <id>` (since U3 it bakes no selection; pre-U3 it baked the non-interactive flags: adapter[:profile] + id + (create|resume) + (start|attach|view); Kick/Instantiate/Change-adapter/Fork are interactive-only, not bakeable). BASENAME IS A PARAMETER (operator rev. 2026-06-14): harness-agnostic s...\n 549|required_stages = [\"doc\", \"impl\", \"unit\"]  # activated M12-W2 (no int: shortcut content + sentinel create-vs-update as unit; flag→action mapping unit)\n 550|[[requirements]]\n 551|id = \"REQ-RUN-PICKER-HOME\"\n...\n 619|id = \"REQ-RCVIEW-1\"\n*620|title = \"Remote-attach controller/viewer model (CONTEXT.md:317): a session's broker OutputLog serves ONE interactive controller (input + EXCLUSIVE PTY resize; its viewport sets the size, sent on attach + every window change via crossterm Event::Resize) plus ANY NUMBER of read-only `--view` attachers (output-only, no input, no resize; client-side letterbox — center+pad when larger, clip+1-line indicator when smaller; only the local ctrl-b d detach chord). Attach intent is three-valued (`Viewer | Control...\n 621|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # activated M12-W2.5 (int: multi-subscriber is cross-process/cross-node — extend the attach.rs int suite with a 2nd viewer + controller-exclusive resize)\n 622|[[requirements]]\n 623|id = \"REQ-KICK-1\"\n...\n 628|id = \"REQ-PICKER-1\"\n*629|title = \"The picker renders a FOUR-state endpoint status (extending the W2 online/offline duality): the list-item square AND a color-coded STATUS line at the top of the pick-existing right-side details both show — gray OFFLINE; green ONLINE (online + PTY-controllable spt-hosted, not controlled); amber 'ONLINE - HARNESS ONLY' (online but NOT broker-PTY-controllable = harness-hosted, no broker PTY seat — today mis-shows green); blue 'ONLINE + CONTROLLED' (online + driven_by.is_some()). Derived on Endpo...\n 630|required_stages = [\"impl\", \"unit\"]  # activated v0.10.0 PICKER-1: impl = InfoJson.controllable + listen/bind stamp + EpStatus 4-state + square_span/render_pick color + data.rs derive; unit = the 4-state derivation + view snapshot render)\n 631|[[requirements]]\n 632|id = \"REQ-PICKER-2\"\n...\n 674|id = \"REQ-MSG-IDLE-EDGE-DRAIN\"\n*675|title = \"On an endpoint's ACTIVE→IDLE transition the daemon DRAINS its pending spool (NON-DEFERRED ONLY — AMENDED 2026-07-26) through the same shared spt-hosted inject leg — closing the SECOND F-023 gap for the classes an inject may carry: an spt-hosted endpoint (no api-listen relay to wake it) strands its 'IDLE+no-binary → non-deferred for a relay that does not exist' class without this drain. AMENDMENT 2026-07-26 (FIELD-TRUTH W1 roll-in, operator ruling; doyle spool-audit RCA — spt-shells she...\n 676|required_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED F-023 leg 2; AMENDED 2026-07-26, evidence RE-PINNED 2026-07-27 in the FIELD-TRUTH W1 roll-in build PR (todlando). impl = spt-store/spool.rs claim_idle_edge_inner narrowed to a single `delivered = 0 AND deferred = 0` SQL with NO branch — the include_deferred parameter is GONE from claim_idle_edge_at/claim_idle_edge_audited_at, so no caller can widen it; spt-daemon/src/inject.rs drain_idle_spool drops its resting::deferred_held computation (dead once...\n 677|[[requirements]]\n 678|id = \"REQ-HAZARD-DELIVERY-STARVATION\"\n...\n 1405|title = \"The `spt endpoint run` picker shows project history for FRESH endpoints (operator-raised v0.12.0 real-harness finding). Symptom: a fresh endpoint shows no project history in the picker. ROOT TBD — investigate the project-history loader (v0.10.0 PICKER-2, picker/data.rs) before fixing: distinguish a real loader bug from 'fresh = no history yet' semantics. (v0.12.1)\"\n*1406|required_stages = [\"impl\", \"unit\"]  # ACTIVATED v0.12.1 Wave 3 P1 (todlando; doyle ruling Option A, operator looped in). FINDING: NOT a loader bug — the branch-enumeration loader (project_history_for) is correct. SEMANTICS gap: \"project history\" = projects with COMMITTED context, so a FRESH endpoint (no context branch yet) is excluded from the Project category (model.rs:533) even when created/running IN the current project. RULING: an endpoint belongs to a project if CREATED there (cwd origin) OR has c...\n 1407|[[requirements]]\n 1408|id = \"REQ-PICKER-ONLINE-ACTION\"\n 1409|title = \"The `spt endpoint run` picker shows the correct action for an ALREADY-ONLINE endpoint — Attach, NOT 'Start now' (operator-raised v0.12.0 real-harness finding). Symptom: the picker offers 'Start now' for endpoints that are already online. ROOT TBD — investigate the status→action mapping (v0.10.0 PICKER-1 four-state status, picker/model.rs): is it reading live/online state correctly, or rendering stale/wedged broker state (i.e. partly a symptom of the broker wedge / status=online latch)? Fix...\n...\n 1530|id = \"REQ-RC-DRIVER-READ-LIVENESS\"\n*1531|title = \"The rc client's busy-refuse reads a LIVE driver, never a stale stamp (kin to REQ-HAZARD-DRIVEN-BY-SELFHEAL / KH 7.15, observed from the CLIENT side; instrumented x20 2026-08-19, doyle-ruled same day). MECHANISM, measured not inferred: `driven_by` is broker-owned and EVERY clear of it lives in the daemon (broker stamp_reaped, converge_perch_stamps, the livehost reconcile belt), while the write that makes a stopped perch read `offline` is `terminal_normalize` — run SYNCHRONOUSLY in the `spt endp...\n 1532|required_stages = [\"impl\", \"unit\"]  # ACTIVATED with the ER bring-up residual fix (todlando 2026-08-19, doyle BUILD-GO). No int by ruling: the fix must make the EXISTING bring-up cell's offline arm green unchanged.\n 1533|\n 1534|# ── W5 follow-up: the idle wedged-REMOTE-controller latch (A2, repro-proven, deferred) ──\n...\n 1814|id = \"REQ-ENDPOINT-LIST-REST-FILTER\"\n*1815|title = \"spt endpoint list hides SUSPENDED instances by default; a new --show-all flag reveals them. Status-first row ordering with fixed precedence ONLINE > CONTROLLED > UNBOUND > SUSPENDED (when shown) > corrupt last, alphabetical by id within each band. Two invariants: (1) CORRUPT rows ALWAYS render regardless of filters — corrupt is a record condition demanding operator action (purge/re-mint), not resting clutter; hiding it would re-create counter-39 bug #3 (cross-ref REQ-HAZARD-CORRUPT-PERCH-COHER...\n*1816|required_stages = [\"impl\", \"unit\"]  # ACTIVATED F-025 item 3 Phase B (todlando). impl = `--show-all` flag on EndpointCmd::List (clap, reference.md regen) threaded to cmd_endpoint_list → render_node_grouped; filter_and_order (SUSPENDED hidden unless show_all, CORRUPT always kept, sorts status-first via instance_rank then id, returns hidden-suspended count) + render_total (discloses \"+M suspended hidden\") + instance_rank (corrupt→last) + format_instance_rows renders a corrupt row as the SUSPENDED squar...\n 1817|[[requirements]]\n 1818|id = \"REQ-ADAPTER-UPDATE-INPLACE\"\n 1819|title = \"Bug #18: spt adapter update fails at re-register with os error 2 because it derives the install dir from the update repo NAME (_github/<safe>) instead of updating in place at the adapter record source_dir; when the adapter repo is intentionally renamed across releases (spt-claude-code to claude-spt, supported), the derived dir is fresh/empty and re-register reads a missing manifest. Fix: adapter update installs and re-registers in place at the registered source_dir and tolerates a changed update...\n...\n 1849|id = \"REQ-PICKER-CONTROLLED-LOCAL\"\n*1850|title = \"#3 local half: a LOCALLY-controlled endpoint renders CONTROLLED in its own node's picker. display_status() (crates/spt/src/picker/model.rs:415) derives Controlled ONLY from driven_by.is_some(), but the legacy LOCAL attach path left driven_by=None (the remote-only reading stated here was falsified by doyle ruling v3 2026-07-19 — driven_by names the controlling node, own included; KH 7.15 governs CLEARING it, not who may latch it) — so a locally-controlled endpoint of that era has driven_by=No...\n*1851|required_stages = [\"impl\", \"unit\"]  # ACTIVATED F026 W1 #3 (todlando 2026-07-03). impl = EndpointRow.controlled:bool (model.rs) threaded from info.json.controlled at every builder (data.rs local_rows, cli.rs this_node_cell — so `endpoint list` shares the fix, model.rs from_resource_row remote = driven_by.is_some() interim until #4) + display_status → Controlled on driven_by.is_some()||controlled + view.rs render_confirm control_line \"controlled locally\" arm for an unnamed local driver. unit = model.r...\n 1852|\n 1853|# ── W2 history + details-truth ──\n 1854|[[requirements]]\n...\n 1866|title = \"#4: remote endpoint details (harness + project history) are gossiped, not faked. Today from_resource_row (crates/spt/src/picker/model.rs:340) hardcodes project_history=Vec::new() for every remote row and passes adapter_profile=row.resources (the blurb masquerading as the harness), and Instance/ResourceRow (crates/spt-net/src/net/registry.rs:457) carry no adapter field and no project list. Fix: additive gossip fields N-1-safe exactly like endpoint_type — Instance.adapter (composite <adapter>[:p...\n*1867|required_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED F026 W2 #4 (todlando 2026-07-02). THREE gossip fields on Instance+ResourceRow (adapter:Option<String>, recent_projects:Vec<String>, controlled:bool — the doyle-corrected explicit flip, NOT controller_node.is_some() which undercounts the local-controller case). impl = registry.rs Instance+ResourceRow fields (serde-default N-1) + resource_projection mapping; registryhost.rs advertise_local populates (adapter=info.adapter, controlled=info.controlled, ...\n 1868|\n 1869|# ── W3 surfaces ──\n 1870|[[requirements]]\n...\n 1938|id = \"REQ-RESUME-ROW-PER-PROJECT\"\n*1939|title = \"A5 (F028, operator #6): resume-from-history labels EVERY session with the endpoint's newest project. data.rs:480-496 resume_rows_for clones project_history.first() onto every ResumeRow (line 481/488), so all sessions read as the head project (the ghost). The per-row e.cwd is already carried for launch-into-dir. FIX: derive per-row project_id_for_dir(e.cwd) (owlery-excluded -> fall back to trigger token), rendered through A1's display-name path. See triage A5.\"\n 1940|required_stages = [\"impl\", \"unit\"]  # ACTIVATED F028 W1 A5 (todlando 2026-07-03). impl = data.rs resume_rows_for delegates to pure resume_rows_from(entries, owlery, id_of): per-row project = project_id_for_dir(e.cwd), MEMOIZED per unique cwd (MAX_LEDGER-bounded git cost, doyle note), owlery-internal → empty (trigger-title fallback); the project_history-head param is DROPPED. caller (gather) drops the arg; project_history still fills the row field. unit = resume_rows_derive_per_row_project (folder_id st...\n 1941|\n 1942|[[requirements]]\n...\n 2157|title = \"A-4b (REMOTE-TRUTH triage §A + ADR-0034 Decision 2): the tracing-only op families auto-retry ONCE with a fresh op on the typed no-longer-held error, so a broker-restart-dropped conn/stream self-heals instead of surfacing core lingo to the user. ROOT: net/rest.rs declares rest op-ids 'tracing/correlation only … redelivery needs reporting, not dedup', yet the journal enforced exactly-once on them — a rest/attach op whose conn the broker no longer holds (post-restart) returned the typed 'alrea...\n*2158|required_stages = [\"doc\", \"impl\", \"unit\"]  # ACTIVATED REMOTE-TRUTH A-4b (todlando). impl = a shared FROZEN const OP_NO_LONGER_HELD_MARKER (effect.rs; extracted VERBATIM from the broker's current no-longer-held message which crosses the brain IPC boundary — new cli must match un-upgraded daemon text; broker.rs:2796/3047 reference it keeping the emitted text byte-identical) + a with_tracing_retry(mint, run) helper (effect.rs; substring-matches the const, re-mints a FRESH same-minter op once, on 2nd no-l...\n 2159|[[requirements]]\n 2160|id = \"REQ-REST-VERB-ROUTING\"\n 2161|title = \"A-3 (REMOTE-TRUTH triage §A + Q3 operator-law): a BARE-id rest verb (spt wake/suspend <id>) routes across the subnet like send's fallback instead of failing local-only. ROOT (certain): cmd_rest (cli.rs:3296) gates the remote arm on id.contains('@'|':'); a bare id falls to the local-only arm (cli.rs:3340) → daemon_rest_event → info::read_info miss (resting.rs:248) → 'WOKE_FAIL:{id}: info.json absent or unreadable — not a hosted perch'. cmd_send (cli.rs:5142) DOES fall back on a local mis...\n...\n 2184|id = \"REQ-HAZARD-BROKER-FLOOR-LOCK-POISON\"\n*2185|title = \"B-1 (REMOTE-TRUTH triage §B-1, PIVOTED @e5eb99a): NO bare `.lock().unwrap()` on a broker-resident lock reachable from serve/dispatch — a brain-only self-update keeps the broker + all its Mutexes ALIVE (REQ-UPD-3), so a single panic under one poisons it PERMANENTLY: the next `.lock().unwrap()` panics, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines ('brain IPC read deadline elapsed') while non-locked ops keep working. TRIAGE-DRIFT (sweep-dispatch-site-counts dis...\n 2186|required_stages = [\"doc\", \"impl\", \"unit\"]  # ACTIVATED REMOTE-TRUTH B-1 PIVOTED (todlando). doc = KNOWN-HAZARDS.md 7.33 (class-level: NO bare .lock().unwrap() on a serve/dispatch-reachable broker-resident lock — recover/recover_log or documented fail-fast). impl = broker::recover<T> (into_inner shared helper) applied at self.sessions ×18 + sessions_exit + pair_holds ×4 (23), broker::recover_log (recover + coherence clamp on the poison branch) at the 11 OutputLog ring lock sites, OutputLog::clamp_or_r...\n 2187|[[requirements]]\n 2188|id = \"REQ-CONTROLLER-LIVENESS-REAP\"\n*2189|title = \"B-2 (REMOTE-TRUTH triage §B-2, REDUCED @bdc1242): a stale ONLINE+CONTROLLED stamp on a live-session perch self-heals — the info.json driven_by/controlled RECORD is made to match the broker's SINK-TABLE TRUTH. ROOT (persisted-stale-stamp class, doyle Q1): the livehost control reap gates on !has_session (reconcile_hosted_liveness), and a brain-only update KEEPS the session (REQ-UPD-3), so a controller stamp that went stale WHILE the session lived was never re-derived from broker truth. NOT a tr...\n 2190|required_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED REMOTE-TRUTH B-2 REDUCED (todlando); int ACTIVATED on the green [twohost] rig run 28772170556 @279df99 (2026-07-06). impl = OutputLog::reap_dead_controller (broker.rs: drop a controller whose _writer.is_finished() in-memory ONLY, no stamp — lock-safe under the KIND_SESSIONS snapshot log lock) + the snapshot-closure call site (recover_log(&log) made mut; reap BEFORE reading controller_by/has_controller so the reply + the existing off-lock converge_p...\n 2191|[[requirements]]\n 2192|id = \"REQ-ADAPTER-FLOOR-ENFORCE\"\n...\n 2213|title = \"C-4 (REMOTE-TRUTH triage §C-4 #9): Backspace is a back() ALIAS across the picker — one keypress backs out one screen along the SAME reverse map Esc walks (model back(), complete for all screens incl. the C-3 ConfirmPurge), and from the kind layer it cancels the picker (Esc parity) — EXCEPT the two text-edit contexts, where Backspace stays CHAR-DELETE: CreateId entry (id_backspace) and the pick-list filter mode (filter_backspace). DELIBERATE: no empty-buffer fallthrough to back() in the text...\n*2214|required_stages = [\"impl\", \"unit\"]  # ACTIVATED REMOTE-TRUTH C-4 (todlando). impl = the handle_key top-level Backspace arm (back() alias, CreateId + filter-mode exclusions) next to the Esc arm (picker/mod.rs). unit = a new picker/mod.rs tests module driving handle_key directly: backspace_backs_out_screens_like_esc (Confirm→PickExisting→Kind→Cancelled, Esc parity) + backspace_stays_char_delete_in_text_contexts (CreateId buffer shortens screen unchanged; empty-buffer no-nav control; filter-mode query...\n 2215|\n 2216|[[requirements]]\n 2217|id = \"REQ-RC-RECONNECT\"\n...\n 2496|id = \"REQ-LIVE-AGENT-NO-INJECT-DELIVERY\"\n*2497|title = \"F-033 RE-SCOPED (doyle re-ruling 2026-07-10 after the #82 gate falsified the original premise — the hosting-mode class split is BINDING context): 'state:live_agent has a self-delivery reader' CONFLATED two hosting modes. (A) HARNESS-hosted live agents (api listen path) DO deliver via adapter channels only — and are ALREADY structurally excluded from inject: bind_from_seed stamps controllable=Some(false), no broker PTY exists. (B) SPT-HOSTED/CONTROLLED live agents' inject leg IS their deliver...\n 2498|required_stages = [\"impl\", \"unit\"]  # FLIPPED per doyle residual-class ruling (2026-07-10, after the W4-gate RE-QUEUE): impl = the daemon-startup echo-scrub (INJECT_ECHO_ENV_VARS fold, dual-tagged at the scrub site alongside REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE — the F-033 typed-garbage root was ambient SPT_INJECT_VERIFY_ECHO force-arming the Layer-2 re-drive; raw payload+CR path proven dead). unit = leg (a): the shared_inject_leg_detects_spt_hosted non-controllable cases (None + bind_from_seed's So...\n 2499|\n 2500|# --- Added 2026-07-09 (F-034, perri field finding — hertz /sptc:live normal-session bringup cascade, spt 0.29.0; adapter leg already fixed by perri; scope F-034-sptc-live-core-legs.md + repro hertz-sptc-live-repro.md). 3 core legs, rule 3 register-before-satisfy, rule 5 activate at fix-milestone. ---\n...\n 2538|\n*2539|# ── MSG-IDENTITY milestone (operator GO 2026-07-10: \"two triages + prominent backlog seeds, especially messaging + endpoint identity\"; F-037 EXCLUDED by operator, covered later) ──\n 2540|# Scope: F-036 legs a+c (+ schema-doc fold), F-033/carrier double-deliver cluster, F-038 autostart, F-039 poison log-churn; also ACTIVATES F-032 + F-034 legs (registered earlier, stages flipped this commit). doyle triage; todlando builds; doyle gates; deployah publishes. rule 3 register-before-satisfy; rule 5 activate-at-work-start (this dispatch IS work-start).\n 2541|[[requirements]]\n 2542|id = \"REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE\"\n...\n 2557|id = \"REQ-RELAY-NO-BUSY-DELIVER\"\n*2558|title = \"MSG-IDENTITY W4 (operator self-send probe 2026-07-09, companion leg): the relay/idle-inject carrier MUST NOT fire for an ACTIVE endpoint — the daemon already guards send-time (daemon.stderr.log: 'ENDPOINT_INJECT: endpoint ACTIVE -> spool (deferred hint), not injected') yet the field row ended taken_leg='idle-inject' for a message sent while the endpoint was ACTIVE, so the guard is bypassed somewhere on the busy-to-idle EDGE (the parked-drain idle-injects rows that arrived during busy without r...\n 2559|required_stages = [\"unit\"]  # REDUCED per doyle RCA-lock ruling 2026-07-10 (.claude/doyle-to-todlando.md): the exclusive claim (REQ-CARRIER-CLAIM-EXCLUSIVE claim_rows kernel) closes the busy-to-idle edge itself — the idle-edge claim re-reads delivered under the write lock, so a poll-taken row can never idle-inject. This leg keeps ONLY the ordering assertion: unit = a row taken by a (now-atomic) poll is never idle-injected; already-idle delivery (F-023/W5 class) stays green.\n 2560|\n 2561|[[requirements]]\n...\n 3141|id = \"REQ-STREAM-LIFETIME-CLASS\"\n*3142|title = \"RESCOPED at W2 activation 2026-07-22 (doyle verify-first, C3-retirement precedent): the CLEAN-CASE chain this REQ was minted for is ALREADY SHIPPED under REQ-STREAM-LEASE-CLASSES / ADR-0040 decision 6, verified against @7c0f12d — StreamLifetime is opener-declared and on the wire, serde-default Durable so N-1 openers keep exact today-semantics (msg.rs:810-836 + the additive-wire unit @msg.rs:1408); rc attach/view is the SOLE ConnectionBound opener (attach.rs:667-677); the broker binds the class...\n 3143|required_stages = [\"int\"]  # W2 ACTIVATED on build/daemon-lifecycle-w2 (todlando 2026-07-22), then RESCOPED AGAIN the same day onto the MEASUREMENT: the Leg A rig falsified the pre-registered premise (non-repro = SUCCESS, doyle's framing), so impl/unit are deactivated and only the regression guard is owed. See RESCOPE 2 in the title.\n 3144|\n 3145|[[requirements]]\n\n## docs/\n### 49-267-REFRESH-OBSERVATION-PLAN.md\n 218|\n*219|Its exclusion of `by: Some` controllers from the promotion predicate **does not exclude indirect\n 220|effects on those viewports**. Connection identity is **mapped** from the run's own attribution\n 221|(`conn=`, session id) and never inferred.\n 222|\n### DEFERRED.md\n 45|| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:\"owl_message\"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceab...\n*46|| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ \"attached\" tri-state** + the **\"Kick `<node>` and attach\"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — \"who is attached to endpoint E's PTY, by which node/surface\" — derive from the `presence.rs` PresenceLog conn c...\n 47|| **Cross-node shell TUNNEL relay (REQ-SHELL-4 cross-node-on-LAN carry)**: W3 built + proved the opaque byte tunnel **same-node only** (broker-homed `TunnelHub` loopback stream pair + the local control socket; `tunnel_e2e` round-trips `<EVENT`-looking bytes byte-exact). The **cross-node** path does NOT exist: `StreamFamily` (dispatch.rs) has no `Tunnel` variant, `classify_first_line` no tunnel arm, `SHELL_LINK` carries only relink/cmd/drive (no tunnel action), and every tunnel surface (`tunnel_ensure/sen...\n 48|| **Per-connection contiguous-sent cursor — structural guard against a decreasing-floor controller re-take (`REQ-HAZARD-CONTROLLER-WRITER-REORDER` follow-up)**: v0.13.0 P1c fixes the controller-writer reorder while KEEPING handoff's eager subscribe (fix #1 \"drop the subscribe\" was reverted — it's the standalone-resume mechanism), so the decreasing-floor double-take (handoff subscribe@K + serve_attach subscribe@0) is still PRESENT and made safe by: `controller_writer` epoch-gated under `send.lock()` (...\n 49|| **Adapter-manifest-enabled rc VT/mouse mode (`spt rc` scroll vs right-click-paste xor resolution)**: v0.13.0 P1/P1b's Windows `EnableMouseCapture` (for client-originated right-click bracketed paste, 7.18) makes Windows Terminal hand ALL wheel events to rc instead of scrolling its own scrollback; rc forwards the wheel to the harness as SGR reports (7.20) but ONLY when the harness has mouse-reporting on (`enabled && sgr`) — and **Claude Code's REPL never enables mouse tracking** (it has no internal scr...\n### FLAKE-LEDGER.md\n 31|| CI build step `Build notify-shell` (kitsubito Linux) | 1× kitsubito (2026-06-16 run 27652755792 attempt 1, v0.8.1 PR #17) | crates.io dep-download blip during the notify-shell build — `download of config.json failed, curl failed` fetching `serde_json`: a transient registry/network fetch failure on the runner, not a code or test fault (a real dep break fails both runners deterministically; this hit one runner once) | observed; cleared by `gh run rerun --failed` (attempt 2 green, builds reused). Harde...\n*32|| `spt dummy_harness_e2e` (BOTH tests: `endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches` + `endpoint_run_attach_awaits_online_before_attaching`) | 1× kitsubito (2026-07-05, REMOTE-TRUTH Batch-2 re-gate @f2a799e; roles swap run-to-run) | **process-global `set_var` race in a multi-test e2e binary.** Both tests `std::env::set_var(\"SPT_HOME\", own_tempdir)` then do IN-PROC reads (`perch::spt_home`/`adapters_dir`, `registry::register`, psychebin `fs::copy` staging). One file = one test binar...\n 33|| `spt-store registry::tests::concurrent_registration_never_locks` | 1× hfenduleam local gate (2026-07-05, REMOTE-TRUTH Batch-2 bless matrix @3f6f327, fresh worktree, `nextest --workspace --no-fail-fast`) | 16-concurrent-writer SQLite registration gate FAILED at 5.63s — the SQLITE_BUSY load-contention class ALREADY documented at `db.rs:60`: box saturated by the 1588-test full-parallel suite meant a writer couldn't schedule its retry inside the default 5s `registry_busy_timeout`. Isolated ×5: green, 0...\n 34|| `spt-daemon::resume resume_mode_brain_spawning_new_sessions_delivers_each` | 1× kitsubito (2026-07-05 run 28768663321 attempt 1, REMOTE-TRUTH [twohost] PR #52) | Phase-B heavy-class timing EOF — \"session exited before the expected output arrived\" (UnexpectedEof) at 62s under the serialized heavy pool. SAME FAMILY as this file's prior Linux-CI flake (7940318, D4-2b: sequential spawn/read hardening) — a spawned session's exit raced the expected output read on the slow shared box. File untouched by t...\n 35|| `spt-daemon::input_ack_deadlock input_flood_through_serve_attach_does_not_deadlock_broker` | 1× hfenduleam local gate (2026-07-06, REMOTE-TRUTH F-2 matrix @a21bc6b, `nextest --workspace --no-fail-fast`) | deadline-margin under full-suite load: the flood exchange's `recv_timeout(30s)` (input_ack_deadlock.rs:512) blew under the 1608-test parallel pool + 7 leaky daemons — FAIL at 34.5s vs ~32s natural runtime isolated (the margin is ~3s by design). Subsystem disjoint from the triggering commit (F-2 = a...\n...\n 49|| `spt-daemon::twohost_web two_host_web_role_b` — the A_CELLS completion witness (:257-284) | 1x, the FIRST-EVER real pair run (one box, 2026-09-07 02:56Z, `.worktrees/gate-w1-26a96d58/.spt/twohost-web/none2/`). No pair had ever run role B before — kitsubito's leg was the env-gated no-op, so this cell had never once executed for real. | **NOT A FLAKE — a DETERMINISTIC completion-witness defect, and it would have failed every pair run forever.** Filed here because doyle routed it here; the distincti...\n*50|| `spt::resident_service_e2e a_declared_service_rises_with_the_daemon_and_reaches_the_cli` — the **:453 PRECONDITION** assert (a THIRD distinct signature for this test: distinct from its :664 teardown-LEAK row above, and the direct successor of that row's noted \":382 PRECONDITION\" signature, which is this assert at its old line) | 1× kitsubito Linux (2026-09-07, the W1 #249 builder battery AT the gated sha `8d980fdf`, gate dir `~/spt-w1/.worktrees/ws272-w1/.spt/ws272-w1-gate/`; nextest exit 100, ONE S...\n 51|| `spt::resume_no_control_steal_e2e brain_respawn_keeps_every_session_controller_and_still_promotes` — keyed on the MESSAGE `the trial candidate never stamped brain.ready`, NOT on the rendered line. The panic renders at `:358`, which is `teardown_panic`'s funnel `panic!` shared by every failing arm in the file (:397 and :490 today); a row keyed on :358 would silently absorb an unrelated future red | 1× kitsubito Linux (2026-09-07, same battery, same Summary and same preserved raw as the `resident_serv...\n 52|| `spt-daemon::mesh_recovery roster_route_survives_a_transient_dial_failure_with_discovery_disabled` — \"never converged: roster route converged after the transient\" (`mesh_recovery.rs:97` at `25e60015`; `converge()` = 600 × 25 ms = 15.0 s wall-clock budget) | 1× hfenduleam Windows golden, #272 WEBSERVE r2 run 34262154550 attempt 3 (2026-09-09 00:43:52Z, job 102283894969, Phase B serialized, cell 15.715 s, 233/234). Same cell PASSED at the same sha on attempts 1 and 2 (9.801 s, 7.172 s). | **BOX CONTE...\n 53|| `spt-daemon::registry_lifecycle oneway_rounds_plateau_rows_seats_and_a_refresh_replays_nothing` — \"the refresh must subscribe NO historical rows (zero replay churn): held 0 -> 1, seats 1\" (`registry_lifecycle.rs:514` at `25e60015`; the directional held/seats assertion after the gen-2 refresh) | 1× hfenduleam Windows golden, #272 WEBSERVE r2 run 34262154550 attempt 4 (2026-09-09 02:36Z, job 102306494097, Phase B serialized, cell 11.594 s, 233/234). Same cell PASSED at the same sha on attempts 1-3 (10...\n### INFRA-REGISTER.md\n 5017|     instead of confounded.\n*5018|  3. **OPERATOR ASK — Defender path exclusions** for `C:\\actions-runner\\_work` and the gate pools,\n 5019|     read from an ADMIN shell first (see the unreadable-row warning above), then re-measure the\n 5020|     first-touch tax on a file **under that path** — the scratchpad measurement is outside the\n 5021|     runner dir, so it proves the mechanism's size, not the runner's exposure. Unsettable\n### KNOWN-HAZARDS.md\n 583|### 7.15 An OFFLINE spt-hosted endpoint must NOT render phantom `ONLINE+CONTROLLED` — clear `driven_by` when its session is gone  `[REQ-HAZARD-DRIVEN-BY-SELFHEAL]`\n*584|- **Failure:** `driven_by` (the `info.json` `ONLINE+CONTROLLED` latch) is single-written by the broker via `clear_controller`/`stamp_driven_by`, which only fire on a controller change. When an spt-hosted endpoint's broker session is GONE (harness dead — the B2 case, 7-series sibling `REQ-HAZARD-HOSTED-LIVENESS-RECONCILE`), no controller event ever fires, so a stale `driven_by=Some(node)` persists: the picker renders a phantom \"controlled by X\" on an endpoint that is actually OFFLINE. The B2 reconcile a...\n 585|- **Invariant:** `reconcile_hosted_liveness`, when it offlines a sessionless controllable perch (the B2 keystone — no live broker session ⇒ dead harness), ALSO clears `driven_by` (`set_driven_by(perch, None)`). RACE-FREE and single-writer-safe: with NO live broker session there is no controller to re-stamp `driven_by` concurrently, so the brain may write it here without contending the broker. (The LIVE-session leg — a controller gone while its session survives — is NOT this hazard: a clean discon...\n 586|- **MEASURED 2026-07-22 (DAEMON-LIFECYCLE W2 Leg B, `transport_death_eof.rs`) — the residual SPLITS in two, and only one half is still open.** On REAL QUIC (two brokers, `BindScope::Loopback` + a real `net_dial` — NOT the in-process duplex, which has no idle timeout at all), a remote controller whose QUIC STACK DIES WITHOUT A FIN (peer frozen: endpoint alive, UDP port still bound, keepalives unanswered — the sleeping-laptop shape) DOES self-heal: the transport's own idle timeout tears the conn, the...\n 587|- **Watch-out (repro-proven, real broker):** `SessionInfo.controller_by==None` is AMBIGUOUS — `dispatch_spawn` pre-attaches the spawner as the LOCAL controller with `by=None`, so a live LOCALLY-driven session also reads `None`. It is therefore NOT a usable standalone `driven_by` clear trigger (would false-clear a live local session). The shipped Gap-B self-heal needs no controller signal at all (it keys on session ABSENCE).\n...\n 640|- **spt-core mapping:** impl `spt-daemon/src/inject.rs` `try_spt_hosted_inject`/`is_spt_hosted_no_relay` (the shared leg) called by `wan.rs` `receive_wan` (leg 1) AND `spt/src/api/delivery.rs` `drain_idle_window` (leg 2, via `spool::claim_idle_edge_at`/`release_at`); int (real broker+PTY+binary) `tests/inject_control_wedge.rs` `wan_arrival_to_idle_spt_hosted_injects_with_no_hook_poll` (leg 1) + `spt/tests/idle_edge_drain_e2e.rs` `spool_while_active_then_idle_fires_injection` (leg 2) — both prove delive...\n*641|- **SCOPE (amended 2026-07-27, FIELD-TRUTH W1 roll-in, operator ruling):** this no-starvation guarantee binds the **`default` and `idle_only` classes ONLY**. The **`active_only` (deferred) class is explicitly OUTSIDE it** — per ADR-0028 it is hook-carried background context whose contract is \"active hook window only; never wakes an idle agent\", so with no hook cadence it WAITS. That is starvation *by design*, not a defect, and it is the one case where \"reached the spool\" does not imply \"will be deliver...\n 642|- **Source:** F-023 (BUILD-F023-WANIDLE; doyle RCA + gate, todlando build 2026-07-02). Delivered in two legs: WAN-ingress inject (leg 1) + idle-edge drain (leg 2). Scope narrowed 2026-07-27 (FIELD-TRUTH W1 roll-in) — see SCOPE above.\n 643|<!-- [doc->REQ-HAZARD-DELIVERY-STARVATION] -->\n 644|\n...\n 683|- **Invariant:** on session reap the broker clears the perch's control/viewer stamps — `OutputLog::stamp_reaped()` = `set_driven_by(None)` + `set_controlled(false)` + `set_viewer_count(0)` via the known endpoint id — so no teardown path leaves a stamp behind. The broker stays the SINGLE writer; the clear is unconditional (idempotent when already clear) and RACE-FREE — the session is dead, so there is no live controller to re-stamp concurrently (the same argument as 7.15's no-session leg). Best-effo...\n*684|- **spt-core mapping:** impl `broker.rs` `OutputLog::stamp_reaped` + the exit-waiter reap call (before `sessions.remove`); int (real broker+PTY, no mocks) `crates/spt-daemon/tests/control_stamp_lifetime.rs` `reap_clears_control_and_viewer_stamps` (spawn hosted session → the live controller stamps `controlled=true` → latch `driven_by`+`viewer_count` on disk → KILL the child → the reap clears all three; RED-first verified: fix disabled leaves `driven_by=Some`, `controlled=true`, `viewer_count=Some(...\n 685|- **Source:** F-026 W1 #2 (BUILD-F026-PICKERTRUTH; doyle triage + gate, todlando build 2026-07-03).\n 686|\n 687|---\n...\n 750|- **Failure (paid-for, rig-CONFIRMED 2026-07-06/07 — `NtSuspendProcess` on the brain, no update involved):** a controller's writer thread does a BLOCKING socket write to its brain subscriber conn. When that brain is suspended (or black-holed) the write never returns. The output-driven eviction path (`append` → `mark_controller_gone`, bounded by 7.12's `CONTROLLER_WRITE_DEADLINE`) only fires on NEW output, and `reap_dead_controller` (7.29 companion) only caught a writer that had EXITED — so a contro...\n*751|- **Invariant:** no broker client ever observes a wedge because the BRAIN stalled. (1) The controller's writer publishes its IN-FLIGHT-write window (`write_blocked_since`: `Some(Instant)` set immediately before the blocking `write_frame`, `None` after — the mutex never held ACROSS the write); a reader sees a `Some` older than `BRAIN_WRITE_DEADLINE` (15 s = 3× the 7.12 controller bound, measured full-with-**zero writer progress**, so a slow-but-DRAINING brain never trips it) as a WEDGED writer and stal...\n 752|- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (...\n 753|- **Source:** LIFECYCLE-TRUTH W2 (doyle rig `NtSuspendProcess` root + design ruling `docs/W2-DESIGN-RULING.md` @60ec0ed — minimal-plus scope, `BRAIN_WRITE_DEADLINE` 15 s, passive resubscribe + observability rider; todlando build 2026-07-07). The v0.27→v0.28 update-wedge night's flagship defect.\n 754|\n...\n 844|<!-- [doc->REQ-HAZARD-CONTROLLER-LEASE] -->\n*845|- **Failure (paid-for, hertz same-machine `--take` RCA, field repro 2026-07-16):** terminal A controlled an endpoint; terminal B on the SAME machine ran `spt rc --take`. Local loopback attaches carry only NODE identity, so `resolve_subscribe` computed `same_identity=true` and took the silent successor path for a distinct `--take` — intent never consulted; sink replaced with no `Displaced`, no stream close (the only loud branch is different-remote+Take). `dispatch_input` is session-addressed with no con...\n 846|- **Invariant:** each rc invocation/attach mints a unique controller LEASE id carried through subscribe, the controller slot, and Input/Resize; the only silent successor is same-lease + equal-or-newer generation (dispatcher recovery, ADR-0038 fix-6 preserved); explicit Take on a DISTINCT lease always revokes ATOMICALLY and authoritatively (old lease fenced + old attach stream FORCE-closed; the Displaced notice via the old writer is best-effort — a Full-queue notice drop must never leave the incumbent i...\n 847|- **spt-core mapping:** `spt-daemon/src/broker.rs` `OutputLog::resolve_subscribe` (same-identity branch) + `become_controller` slot + `dispatch_input` fence; `spt-daemon/src/brain.rs` documented `None==None` note; `spt/src/rc.rs` loopback attach origin + `PumpEnd::Displaced` (client side already correct).\n 848|- **Source:** RC-RENDER-TRUTH W2 (hertz RCA filed 2026-07-18, doyle seam-verified same day; ADR-0044).\n### NEXT-MILESTONE-PICKER-TRIAGE.md\n 108|(`crates/spt/src/picker/model.rs:415`) derives Controlled **only** from\n*109|`driven_by.is_some()` — and the legacy local attach path left `driven_by` None.\n 110|(**MODEL CORRECTION**, doyle ruling v3 2026-07-19, applied here 2026-08-19: this\n 111|line read \"`driven_by` is REMOTE-only by design (KH 7.15)\". Falsified —\n 112|`driven_by` names the controlling node, own node included, and the own-hex latch\n...\n 121|`controller_node.is_some()`); `display_status` → Controlled when\n*122|`driven_by.is_some() || controlled`; desc pane says \"controlled locally\" when the driver\n 123|is unnamed. **Red-first seam:** unit — locally-controlled row (driven_by None,\n 124|controlled true) must render `EpDisplay::Controlled`. Confidence HIGH / effort S.\n 125|\n### NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md\n 210|  entirely) queries the broker for every perch stamped\n*211|  controlled/`driven_by=Some(remote)`: no live controller or identity\n 212|  mismatch → clear the stamps. Boot-tick coverage falls out (reconcile runs\n 213|  at boot), which also cleans stamps left by a PRE-restart wedge.\n*214|- KH 7.15 binding: reap ONLY, `by=Some(remote)` only — a local `by=None`\n 215|  controller is never latched or cleared by this path.\n 216|- Red-first: the triage's sever-across-brain-restart int (stamps clear\n 217|  within a tick) + a persisted-stale-stamp unit (stamp says controlled,\n### W3-GATE-VERDICT.md\n 40|  → kill + rollback (conservative — never false-promote onto a wedge). `old_gen_drained` reads\n*41|  broker truth in-process (`any_local_controller_wedged`, `by:None` only — remote conns excluded,\n 42|  the W2 concern), sessions→log lock order, no I/O under lock, poison → false (fail toward\n 43|  promotion, never strands). Fail-safe→drained where nothing to read (net-less / NoTrialEnv).\n 44|  **No normal-apply regression:** a clean-closed old conn drains at once → fast promote; only the\n### W3-WAVE-GATE-REPORT.md\n 34|  whose conn is black-holed reads wedged until the W2 stall-evict releases it; a cleanly-closed\n*35|  conn is absent. **REMOTE conns (`by:Some`) are EXCLUDED** — that is the W2 concern, not an\n 36|  old-gen brain conn (a stuck remote viewer must never gate a brain promotion).\n 37|  `NoTrialEnv` + a net-less/bind-failed broker degrade to `drained=true` (no control plane to\n 38|  gate — the normal fast promote on `brain.ready` is preserved). `ProductionTrialEnv` holds\n...\n 55|  (latch releases). Unit (broker): `local_controller_wedged_scopes_to_the_brains_own_conn`\n*56|  (by:None only; remote + idle excluded).\n 57|\n 58|## Local gate (this box, HFENDULEAM — Windows)\n 59|- clippy `-p spt-daemon --all-targets`: clean.\n\n### adr/\n#### 0028-activity-gated-delivery-and-send-modifier-axes.md\n 36|\n*37|The operator needs per-message control over that delivery, beyond the single `--deferred` (spool-only, hook-channel) flag that exists today. The risk is modelling these as one flat list of mutually-exclusive \"message types\" — which conflates independent concerns (*when* vs *through what* vs *how long*) and forbids coherent combinations.\n 38|\n 39|## Decision\n 40|\n\n## crates/spt-daemon/src/\n### broker.rs\n 2854|    /// releases it (or the conn EOFs); a cleanly-closed conn is simply absent. A\n*2855|    /// REMOTE operator's wedged conn (`by: Some`) is the W2 stall-evict concern, NOT\n 2856|    /// an old-gen brain conn, so it is EXCLUDED — a stuck remote viewer must never\n 2857|    /// gate a brain promotion. Pure over the injected `now`/`deadline` (no thread).\n 2858|    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n...\n 13138|            send,\n*13139|            by: Some(\"remote-node\".to_string()),\n 13140|            last_ok: Instant::now(),\n 13141|            write_blocked_since: Arc::new(Mutex::new(None)),\n 13142|            _writer: writer,\n...\n 13211|    // lazy-reap must NOT touch it — reaping a live-but-idle controller would false-clear\n*13212|    // a genuinely CONTROLLED session's stamp. KH 7.15 (reap-only, by=Some) holds by\n 13213|    // construction: reap only ever CLEARS, never latches driven_by.\n 13214|    #[test]\n 13215|    fn reap_dead_controller_leaves_a_live_writer_untouched() {\n...\n 13226|            send,\n*13227|            by: Some(\"remote-node\".to_string()),\n 13228|            last_ok: Instant::now(),\n 13229|            write_blocked_since: Arc::new(Mutex::new(None)),\n 13230|            _writer: writer,\n...\n 13307|    // read is LOCAL-scoped: only the brain's OWN wedged conn (by:None) reads wedged;\n*13308|    // a wedged REMOTE operator conn (by:Some) is the W2 concern and must NOT gate a\n 13309|    // brain promotion; an idle local controller (nothing in flight) is not wedged.\n 13310|    #[test]\n 13311|    fn local_controller_wedged_scopes_to_the_brains_own_conn() {\n...\n 13321|\n*13322|        // A REMOTE (by:Some) controller wedged the SAME way → NOT counted (W2 concern,\n 13323|        // not an old-gen brain conn — a stuck remote viewer must never gate a promote).\n 13324|        let mut remote = OutputLog::new(2, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 13325|        seed_controller_with_block(&mut remote, Some(\"operator\"), Some(wedged_since()));\n...\n 13327|            !remote.local_controller_wedged(now, BRAIN_WRITE_DEADLINE),\n*13328|            \"a wedged REMOTE controller is excluded from the drained gate\"\n 13329|        );\n 13330|        remote.controller = None;\n 13331|\n...\n 16105|                            intent: AttachIntent::Control,\n*16106|                            by: Some(\"old-op\".to_string()),\n 16107|                            gen: 100,\n 16108|                            code: None,\n 16109|                            seal_ceremony: false,\n...\n 16140|                            intent: AttachIntent::Take,\n*16141|                            by: Some(\"new-op\".to_string()),\n 16142|                            gen: 200,\n 16143|                            code: None,\n 16144|                            seal_ceremony: true,\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":".worktrees/49-267-refresh-discriminator/docs, .worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs, .worktrees/49-267-refresh-discriminator/traceable-reqs.toml","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":47,"fileCount":12,"files":[".worktrees/49-267-refresh-discriminator/docs/49-267-REFRESH-OBSERVATION-PLAN.md",".worktrees/49-267-refresh-discriminator/docs/DEFERRED.md",".worktrees/49-267-refresh-discriminator/docs/FLAKE-LEDGER.md",".worktrees/49-267-refresh-discriminator/docs/INFRA-REGISTER.md",".worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md",".worktrees/49-267-refresh-discriminator/docs/NEXT-MILESTONE-PICKER-TRIAGE.md",".worktrees/49-267-refresh-discriminator/docs/NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md",".worktrees/49-267-refresh-discriminator/docs/W3-GATE-VERDICT.md",".worktrees/49-267-refresh-discriminator/docs/W3-WAVE-GATE-REPORT.md",".worktrees/49-267-refresh-discriminator/docs/adr/0028-activity-gated-delivery-and-send-modifier-axes.md",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs",".worktrees/49-267-refresh-discriminator/traceable-reqs.toml"],"fileMatches":[{"path":".worktrees/49-267-refresh-discriminator/docs/49-267-REFRESH-OBSERVATION-PLAN.md","count":1},{"path":".worktrees/49-267-refresh-discriminator/docs/DEFERRED.md","count":1},{"path":".worktrees/49-267-refresh-discriminator/docs/FLAKE-LEDGER.md","count":2},{"path":".worktrees/49-267-refresh-discriminator/docs/INFRA-REGISTER.md","count":1},{"path":".worktrees/49-267-refresh-discriminator/docs/KNOWN-HAZARDS.md","count":5},{"path":".worktrees/49-267-refresh-discriminator/docs/NEXT-MILESTONE-PICKER-TRIAGE.md","count":2},{"path":".worktrees/49-267-refresh-discriminator/docs/NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md","count":2},{"path":".worktrees/49-267-refresh-discriminator/docs/W3-GATE-VERDICT.md","count":1},{"path":".worktrees/49-267-refresh-discriminator/docs/W3-WAVE-GATE-REPORT.md","count":2},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0028-activity-gated-delivery-and-send-modifier-axes.md","count":1},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs","count":9},{"path":".worktrees/49-267-refresh-discriminator/traceable-reqs.toml","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# .worktrees/49-267-refresh-discriminator/\n## traceable-reqs.toml\n  547│id = \"REQ-RUN-SHORTCUT\"\n *548│title = \"`<basename>-<id>` launcher shortcut generation (picker `s` keybind, M12-W2-T2.4): from any pre-start options set the picker writes/updates a `<basename>-<id>` launcher at the project root whose body is `spt go <id>` (since U3 it bakes no selection; pre-U3 it baked the non-interactive flags: adapter[:profile] + id + (create|resume) + (start|attach|view); Kick/Instantiate/Change-adapter/Fork are interactive-only, not bakeable). BASENAME IS A PARAMETER (operator rev. 2026-06-14): harness-agnostic s...\n  549│required_stages = [\"doc\", \"impl\", \"unit\"]  # activated M12-W2 (no int: shortcut content + sentinel create-vs-update as unit; flag→action mapping unit)\n  550│[[requirements]]\n  551│id = \"REQ-RUN-PICKER-HOME\"\n     │...\n  619│id = \"REQ-RCVIEW-1\"\n *620│title = \"Remote-attach controller/viewer model (CONTEXT.md:317): a session's broker OutputLog serves ONE interactive controller (input + EXCLUSIVE PTY resize; its viewport sets the size, sent on attach + every window change via crossterm Event::Resize) plus ANY NUMBER of read-only `--view` attachers (output-only, no input, no resize; client-side letterbox — center+pad when larger, clip+1-line indicator when smaller; only the local ctrl-b d detach chord). Attach intent is three-valued (`Viewer | Control...\n  621│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # activated M12-W2.5 (int: multi-subscriber is cross-process/cross-node — extend the attach.rs int suite with a 2nd viewer + controller-exclusive resize)\n  622│[[requirements]]\n  623│id = \"REQ-KICK-1\"\n     │...\n  628│id = \"REQ-PICKER-1\"\n *629│title = \"The picker renders a FOUR-state endpoint status (extending the W2 online/offline duality): the list-item square AND a color-coded STATUS line at the top of the pick-existing right-side details both show — gray OFFLINE; green ONLINE (online + PTY-controllable spt-hosted, not controlled); amber 'ONLINE - HARNESS ONLY' (online but NOT broker-PTY-controllable = harness-hosted, no broker PTY seat — today mis-shows green); blue 'ONLINE + CONTROLLED' (online + driven_by.is_some()). Derived on Endpo...\n  630│required_stages = [\"impl\", \"unit\"]  # activated v0.10.0 PICKER-1: impl = InfoJson.controllable + listen/bind stamp + EpStatus 4-state + square_span/render_pick color + data.rs derive; unit = the 4-state derivation + view snapshot render)\n  631│[[requirements]]\n  632│id = \"REQ-PICKER-2\"\n     │...\n  674│id = \"REQ-MSG-IDLE-EDGE-DRAIN\"\n *675│title = \"On an endpoint's ACTIVE→IDLE transition the daemon DRAINS its pending spool (NON-DEFERRED ONLY — AMENDED 2026-07-26) through the same shared spt-hosted inject leg — closing the SECOND F-023 gap for the classes an inject may carry: an spt-hosted endpoint (no api-listen relay to wake it) strands its 'IDLE+no-binary → non-deferred for a relay that does not exist' class without this drain. AMENDMENT 2026-07-26 (FIELD-TRUTH W1 roll-in, operator ruling; doyle spool-audit RCA — spt-shells she...\n  676│required_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED F-023 leg 2; AMENDED 2026-07-26, evidence RE-PINNED 2026-07-27 in the FIELD-TRUTH W1 roll-in build PR (todlando). impl = spt-store/spool.rs claim_idle_edge_inner narrowed to a single `delivered = 0 AND deferred = 0` SQL with NO branch — the include_deferred parameter is GONE from claim_idle_edge_at/claim_idle_edge_audited_at, so no caller can widen it; spt-daemon/src/inject.rs drain_idle_spool drops its resting::deferred_held computation (dead once...\n  677│[[requirements]]\n  678│id = \"REQ-HAZARD-DELIVERY-STARVATION\"\n     │...\n 1405│title = \"The `spt endpoint run` picker shows project history for FRESH endpoints (operator-raised v0.12.0 real-harness finding). Symptom: a fresh endpoint shows no project history in the picker. ROOT TBD — investigate the project-history loader (v0.10.0 PICKER-2, picker/data.rs) before fixing: distinguish a real loader bug from 'fresh = no history yet' semantics. (v0.12.1)\"\n*1406│required_stages = [\"impl\", \"unit\"]  # ACTIVATED v0.12.1 Wave 3 P1 (todlando; doyle ruling Option A, operator looped in). FINDING: NOT a loader bug — the branch-enumeration loader (project_history_for) is correct. SEMANTICS gap: \"project history\" = projects with COMMITTED context, so a FRESH endpoint (no context branch yet) is excluded from the Project category (model.rs:533) even when created/running IN the current project. RULING: an endpoint belongs to a project if CREATED there (cwd origin) OR has c...\n 1407│[[requirements]]\n 1408│id = \"REQ-PICKER-ONLINE-ACTION\"\n 1409│title = \"The `spt endpoint run` picker shows the correct action for an ALREADY-ONLINE endpoint — Attach, NOT 'Start now' (operator-raised v0.12.0 real-harness finding). Symptom: the picker offers 'Start now' for endpoints that are already online. ROOT TBD — investigate the status→action mapping (v0.10.0 PICKER-1 four-state status, picker/model.rs): is it reading live/online state correctly, or rendering stale/wedged broker state (i.e. partly a symptom of the broker wedge / status=online latch)? Fix...\n     │...\n 1530│id = \"REQ-RC-DRIVER-READ-LIVENESS\"\n*1531│title = \"The rc client's busy-refuse reads a LIVE driver, never a stale stamp (kin to REQ-HAZARD-DRIVEN-BY-SELFHEAL / KH 7.15, observed from the CLIENT side; instrumented x20 2026-08-19, doyle-ruled same day). MECHANISM, measured not inferred: `driven_by` is broker-owned and EVERY clear of it lives in the daemon (broker stamp_reaped, converge_perch_stamps, the livehost reconcile belt), while the write that makes a stopped perch read `offline` is `terminal_normalize` — run SYNCHRONOUSLY in the `spt endp...\n 1532│required_stages = [\"impl\", \"unit\"]  # ACTIVATED with the ER bring-up residual fix (todlando 2026-08-19, doyle BUILD-GO). No int by ruling: the fix must make the EXISTING bring-up cell's offline arm green unchanged.\n 1533│\n 1534│# ── W5 follow-up: the idle wedged-REMOTE-controller latch (A2, repro-proven, deferred) ──\n     │...\n 1814│id = \"REQ-ENDPOINT-LIST-REST-FILTER\"\n*1815│title = \"spt endpoint list hides SUSPENDED instances by default; a new --show-all flag reveals them. Status-first row ordering with fixed precedence ONLINE > CONTROLLED > UNBOUND > SUSPENDED (when shown) > corrupt last, alphabetical by id within each band. Two invariants: (1) CORRUPT rows ALWAYS render regardless of filters — corrupt is a record condition demanding operator action (purge/re-mint), not resting clutter; hiding it would re-create counter-39 bug #3 (cross-ref REQ-HAZARD-CORRUPT-PERCH-COHER...\n*1816│required_stages = [\"impl\", \"unit\"]  # ACTIVATED F-025 item 3 Phase B (todlando). impl = `--show-all` flag on EndpointCmd::List (clap, reference.md regen) threaded to cmd_endpoint_list → render_node_grouped; filter_and_order (SUSPENDED hidden unless show_all, CORRUPT always kept, sorts status-first via instance_rank then id, returns hidden-suspended count) + render_total (discloses \"+M suspended hidden\") + instance_rank (corrupt→last) + format_instance_rows renders a corrupt row as the SUSPENDED squar...\n 1817│[[requirements]]\n 1818│id = \"REQ-ADAPTER-UPDATE-INPLACE\"\n 1819│title = \"Bug #18: spt adapter update fails at re-register with os error 2 because it derives the install dir from the update repo NAME (_github/<safe>) instead of updating in place at the adapter record source_dir; when the adapter repo is intentionally renamed across releases (spt-claude-code to claude-spt, supported), the derived dir is fresh/empty and re-register reads a missing manifest. Fix: adapter update installs and re-registers in place at the registered source_dir and tolerates a changed update...\n     │...\n 1849│id = \"REQ-PICKER-CONTROLLED-LOCAL\"\n*1850│title = \"#3 local half: a LOCALLY-controlled endpoint renders CONTROLLED in its own node's picker. display_status() (crates/spt/src/picker/model.rs:415) derives Controlled ONLY from driven_by.is_some(), but the legacy LOCAL attach path left driven_by=None (the remote-only reading stated here was falsified by doyle ruling v3 2026-07-19 — driven_by names the controlling node, own included; KH 7.15 governs CLEARING it, not who may latch it) — so a locally-controlled endpoint of that era has driven_by=No...\n*1851│required_stages = [\"impl\", \"unit\"]  # ACTIVATED F026 W1 #3 (todlando 2026-07-03). impl = EndpointRow.controlled:bool (model.rs) threaded from info.json.controlled at every builder (data.rs local_rows, cli.rs this_node_cell — so `endpoint list` shares the fix, model.rs from_resource_row remote = driven_by.is_some() interim until #4) + display_status → Controlled on driven_by.is_some()||controlled + view.rs render_confirm control_line \"controlled locally\" arm for an unnamed local driver. unit = model.r...\n 1852│\n 1853│# ── W2 history + details-truth ──\n 1854│[[requirements]]\n     │...\n 1866│title = \"#4: remote endpoint details (harness + project history) are gossiped, not faked. Today from_resource_row (crates/spt/src/picker/model.rs:340) hardcodes project_history=Vec::new() for every remote row and passes adapter_profile=row.resources (the blurb masquerading as the harness), and Instance/ResourceRow (crates/spt-net/src/net/registry.rs:457) carry no adapter field and no project list. Fix: additive gossip fields N-1-safe exactly like endpoint_type — Instance.adapter (composite <adapter>[:p...\n*1867│required_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED F026 W2 #4 (todlando 2026-07-02). THREE gossip fields on Instance+ResourceRow (adapter:Option<String>, recent_projects:Vec<String>, controlled:bool — the doyle-corrected explicit flip, NOT controller_node.is_some() which undercounts the local-controller case). impl = registry.rs Instance+ResourceRow fields (serde-default N-1) + resource_projection mapping; registryhost.rs advertise_local populates (adapter=info.adapter, controlled=info.controlled, ...\n 1868│\n 1869│# ── W3 surfaces ──\n 1870│[[requirements]]\n     │...\n 1938│id = \"REQ-RESUME-ROW-PER-PROJECT\"\n*1939│title = \"A5 (F028, operator #6): resume-from-history labels EVERY session with the endpoint's newest project. data.rs:480-496 resume_rows_for clones project_history.first() onto every ResumeRow (line 481/488), so all sessions read as the head project (the ghost). The per-row e.cwd is already carried for launch-into-dir. FIX: derive per-row project_id_for_dir(e.cwd) (owlery-excluded -> fall back to trigger token), rendered through A1's display-name path. See triage A5.\"\n 1940│required_stages = [\"impl\", \"unit\"]  # ACTIVATED F028 W1 A5 (todlando 2026-07-03). impl = data.rs resume_rows_for delegates to pure resume_rows_from(entries, owlery, id_of): per-row project = project_id_for_dir(e.cwd), MEMOIZED per unique cwd (MAX_LEDGER-bounded git cost, doyle note), owlery-internal → empty (trigger-title fallback); the project_history-head param is DROPPED. caller (gather) drops the arg; project_history still fills the row field. unit = resume_rows_derive_per_row_project (folder_id st...\n 1941│\n 1942│[[requirements]]\n     │...\n 2157│title = \"A-4b (REMOTE-TRUTH triage §A + ADR-0034 Decision 2): the tracing-only op families auto-retry ONCE with a fresh op on the typed no-longer-held error, so a broker-restart-dropped conn/stream self-heals instead of surfacing core lingo to the user. ROOT: net/rest.rs declares rest op-ids 'tracing/correlation only … redelivery needs reporting, not dedup', yet the journal enforced exactly-once on them — a rest/attach op whose conn the broker no longer holds (post-restart) returned the typed 'alrea...\n*2158│required_stages = [\"doc\", \"impl\", \"unit\"]  # ACTIVATED REMOTE-TRUTH A-4b (todlando). impl = a shared FROZEN const OP_NO_LONGER_HELD_MARKER (effect.rs; extracted VERBATIM from the broker's current no-longer-held message which crosses the brain IPC boundary — new cli must match un-upgraded daemon text; broker.rs:2796/3047 reference it keeping the emitted text byte-identical) + a with_tracing_retry(mint, run) helper (effect.rs; substring-matches the const, re-mints a FRESH same-minter op once, on 2nd no-l...\n 2159│[[requirements]]\n 2160│id = \"REQ-REST-VERB-ROUTING\"\n 2161│title = \"A-3 (REMOTE-TRUTH triage §A + Q3 operator-law): a BARE-id rest verb (spt wake/suspend <id>) routes across the subnet like send's fallback instead of failing local-only. ROOT (certain): cmd_rest (cli.rs:3296) gates the remote arm on id.contains('@'|':'); a bare id falls to the local-only arm (cli.rs:3340) → daemon_rest_event → info::read_info miss (resting.rs:248) → 'WOKE_FAIL:{id}: info.json absent or unreadable — not a hosted perch'. cmd_send (cli.rs:5142) DOES fall back on a local mis...\n     │...\n 2184│id = \"REQ-HAZARD-BROKER-FLOOR-LOCK-POISON\"\n*2185│title = \"B-1 (REMOTE-TRUTH triage §B-1, PIVOTED @e5eb99a): NO bare `.lock().unwrap()` on a broker-resident lock reachable from serve/dispatch — a brain-only self-update keeps the broker + all its Mutexes ALIVE (REQ-UPD-3), so a single panic under one poisons it PERMANENTLY: the next `.lock().unwrap()` panics, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines ('brain IPC read deadline elapsed') while non-locked ops keep working. TRIAGE-DRIFT (sweep-dispatch-site-counts dis...\n 2186│required_stages = [\"doc\", \"impl\", \"unit\"]  # ACTIVATED REMOTE-TRUTH B-1 PIVOTED (todlando). doc = KNOWN-HAZARDS.md 7.33 (class-level: NO bare .lock().unwrap() on a serve/dispatch-reachable broker-resident lock — recover/recover_log or documented fail-fast). impl = broker::recover<T> (into_inner shared helper) applied at self.sessions ×18 + sessions_exit + pair_holds ×4 (23), broker::recover_log (recover + coherence clamp on the poison branch) at the 11 OutputLog ring lock sites, OutputLog::clamp_or_r...\n 2187│[[requirements]]\n 2188│id = \"REQ-CONTROLLER-LIVENESS-REAP\"\n*2189│title = \"B-2 (REMOTE-TRUTH triage §B-2, REDUCED @bdc1242): a stale ONLINE+CONTROLLED stamp on a live-session perch self-heals — the info.json driven_by/controlled RECORD is made to match the broker's SINK-TABLE TRUTH. ROOT (persisted-stale-stamp class, doyle Q1): the livehost control reap gates on !has_session (reconcile_hosted_liveness), and a brain-only update KEEPS the session (REQ-UPD-3), so a controller stamp that went stale WHILE the session lived was never re-derived from broker truth. NOT a tr...\n 2190│required_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED REMOTE-TRUTH B-2 REDUCED (todlando); int ACTIVATED on the green [twohost] rig run 28772170556 @279df99 (2026-07-06). impl = OutputLog::reap_dead_controller (broker.rs: drop a controller whose _writer.is_finished() in-memory ONLY, no stamp — lock-safe under the KIND_SESSIONS snapshot log lock) + the snapshot-closure call site (recover_log(&log) made mut; reap BEFORE reading controller_by/has_controller so the reply + the existing off-lock converge_p...\n 2191│[[requirements]]\n 2192│id = \"REQ-ADAPTER-FLOOR-ENFORCE\"\n     │...\n 2213│title = \"C-4 (REMOTE-TRUTH triage §C-4 #9): Backspace is a back() ALIAS across the picker — one keypress backs out one screen along the SAME reverse map Esc walks (model back(), complete for all screens incl. the C-3 ConfirmPurge), and from the kind layer it cancels the picker (Esc parity) — EXCEPT the two text-edit contexts, where Backspace stays CHAR-DELETE: CreateId entry (id_backspace) and the pick-list filter mode (filter_backspace). DELIBERATE: no empty-buffer fallthrough to back() in the text...\n*2214│required_stages = [\"impl\", \"unit\"]  # ACTIVATED REMOTE-TRUTH C-4 (todlando). impl = the handle_key top-level Backspace arm (back() alias, CreateId + filter-mode exclusions) next to the Esc arm (picker/mod.rs). unit = a new picker/mod.rs tests module driving handle_key directly: backspace_backs_out_screens_like_esc (Confirm→PickExisting→Kind→Cancelled, Esc parity) + backspace_stays_char_delete_in_text_contexts (CreateId buffer shortens screen unchanged; empty-buffer no-nav control; filter-mode query...\n 2215│\n 2216│[[requirements]]\n 2217│id = \"REQ-RC-RECONNECT\"\n     │...\n 2496│id = \"REQ-LIVE-AGENT-NO-INJECT-DELIVERY\"\n*2497│title = \"F-033 RE-SCOPED (doyle re-ruling 2026-07-10 after the #82 gate falsified the original premise — the hosting-mode class split is BINDING context): 'state:live_agent has a self-delivery reader' CONFLATED two hosting modes. (A) HARNESS-hosted live agents (api listen path) DO deliver via adapter channels only — and are ALREADY structurally excluded from inject: bind_from_seed stamps controllable=Some(false), no broker PTY exists. (B) SPT-HOSTED/CONTROLLED live agents' inject leg IS their deliver...\n 2498│required_stages = [\"impl\", \"unit\"]  # FLIPPED per doyle residual-class ruling (2026-07-10, after the W4-gate RE-QUEUE): impl = the daemon-startup echo-scrub (INJECT_ECHO_ENV_VARS fold, dual-tagged at the scrub site alongside REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE — the F-033 typed-garbage root was ambient SPT_INJECT_VERIFY_ECHO force-arming the Layer-2 re-drive; raw payload+CR path proven dead). unit = leg (a): the shared_inject_leg_detects_spt_hosted non-controllable cases (None + bind_from_seed's So...\n 2499│\n 2500│# --- Added 2026-07-09 (F-034, perri field finding — hertz /sptc:live normal-session bringup cascade, spt 0.29.0; adapter leg already fixed by perri; scope F-034-sptc-live-core-legs.md + repro hertz-sptc-live-repro.md). 3 core legs, rule 3 register-before-satisfy, rule 5 activate at fix-milestone. ---\n     │...\n 2538│\n*2539│# ── MSG-IDENTITY milestone (operator GO 2026-07-10: \"two triages + prominent backlog seeds, especially messaging + endpoint identity\"; F-037 EXCLUDED by operator, covered later) ──\n 2540│# Scope: F-036 legs a+c (+ schema-doc fold), F-033/carrier double-deliver cluster, F-038 autostart, F-039 poison log-churn; also ACTIVATES F-032 + F-034 legs (registered earlier, stages flipped this commit). doyle triage; todlando builds; doyle gates; deployah publishes. rule 3 register-before-satisfy; rule 5 activate-at-work-start (this dispatch IS work-start).\n 2541│[[requirements]]\n 2542│id = \"REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE\"\n     │...\n 2557│id = \"REQ-RELAY-NO-BUSY-DELIVER\"\n*2558│title = \"MSG-IDENTITY W4 (operator self-send probe 2026-07-09, companion leg): the relay/idle-inject carrier MUST NOT fire for an ACTIVE endpoint — the daemon already guards send-time (daemon.stderr.log: 'ENDPOINT_INJECT: endpoint ACTIVE -> spool (deferred hint), not injected') yet the field row ended taken_leg='idle-inject' for a message sent while the endpoint was ACTIVE, so the guard is bypassed somewhere on the busy-to-idle EDGE (the parked-drain idle-injects rows that arrived during busy without r...\n 2559│required_stages = [\"unit\"]  # REDUCED per doyle RCA-lock ruling 2026-07-10 (.claude/doyle-to-todlando.md): the exclusive claim (REQ-CARRIER-CLAIM-EXCLUSIVE claim_rows kernel) closes the busy-to-idle edge itself — the idle-edge claim re-reads delivered under the write lock, so a poll-taken row can never idle-inject. This leg keeps ONLY the ordering assertion: unit = a row taken by a (now-atomic) poll is never idle-injected; already-idle delivery (F-023/W5 class) stays green.\n 2560│\n 2561│[[requirements]]\n     │...\n 3141│id = \"REQ-STREAM-LIFETIME-CLASS\"\n*3142│title = \"RESCOPED at W2 activation 2026-07-22 (doyle verify-first, C3-retirement precedent): the CLEAN-CASE chain this REQ was minted for is ALREADY SHIPPED under REQ-STREAM-LEASE-CLASSES / ADR-0040 decision 6, verified against @7c0f12d — StreamLifetime is opener-declared and on the wire, serde-default Durable so N-1 openers keep exact today-semantics (msg.rs:810-836 + the additive-wire unit @msg.rs:1408); rc attach/view is the SOLE ConnectionBound opener (attach.rs:667-677); the broker binds the class...\n 3143│required_stages = [\"int\"]  # W2 ACTIVATED on build/daemon-lifecycle-w2 (todlando 2026-07-22), then RESCOPED AGAIN the same day onto the MEASUREMENT: the Leg A rig falsified the pre-registered premise (non-repro = SUCCESS, doyle's framing), so impl/unit are deactivated and only the regression guard is owed. See RESCOPE 2 in the title.\n 3144│\n 3145│[[requirements]]\n\n## docs/\n### 49-267-REFRESH-OBSERVATION-PLAN.md\n 218│\n*219│Its exclusion of `by: Some` controllers from the promotion predicate **does not exclude indirect\n 220│effects on those viewports**. Connection identity is **mapped** from the run's own attribution\n 221│(`conn=`, session id) and never inferred.\n 222│\n### DEFERRED.md\n 45│| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:\"owl_message\"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceab...\n*46│| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ \"attached\" tri-state** + the **\"Kick `<node>` and attach\"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — \"who is attached to endpoint E's PTY, by which node/surface\" — derive from the `presence.rs` PresenceLog conn c...\n 47│| **Cross-node shell TUNNEL relay (REQ-SHELL-4 cross-node-on-LAN carry)**: W3 built + proved the opaque byte tunnel **same-node only** (broker-homed `TunnelHub` loopback stream pair + the local control socket; `tunnel_e2e` round-trips `<EVENT`-looking bytes byte-exact). The **cross-node** path does NOT exist: `StreamFamily` (dispatch.rs) has no `Tunnel` variant, `classify_first_line` no tunnel arm, `SHELL_LINK` carries only relink/cmd/drive (no tunnel action), and every tunnel surface (`tunnel_ensure/sen...\n 48│| **Per-connection contiguous-sent cursor — structural guard against a decreasing-floor controller re-take (`REQ-HAZARD-CONTROLLER-WRITER-REORDER` follow-up)**: v0.13.0 P1c fixes the controller-writer reorder while KEEPING handoff's eager subscribe (fix #1 \"drop the subscribe\" was reverted — it's the standalone-resume mechanism), so the decreasing-floor double-take (handoff subscribe@K + serve_attach subscribe@0) is still PRESENT and made safe by: `controller_writer` epoch-gated under `send.lock()` (...\n 49│| **Adapter-manifest-enabled rc VT/mouse mode (`spt rc` scroll vs right-click-paste xor resolution)**: v0.13.0 P1/P1b's Windows `EnableMouseCapture` (for client-originated right-click bracketed paste, 7.18) makes Windows Terminal hand ALL wheel events to rc instead of scrolling its own scrollback; rc forwards the wheel to the harness as SGR reports (7.20) but ONLY when the harness has mouse-reporting on (`enabled && sgr`) — and **Claude Code's REPL never enables mouse tracking** (it has no internal scr...\n### FLAKE-LEDGER.md\n 31│| CI build step `Build notify-shell` (kitsubito Linux) | 1× kitsubito (2026-06-16 run 27652755792 attempt 1, v0.8.1 PR #17) | crates.io dep-download blip during the notify-shell build — `download of config.json failed, curl failed` fetching `serde_json`: a transient registry/network fetch failure on the runner, not a code or test fault (a real dep break fails both runners deterministically; this hit one runner once) | observed; cleared by `gh run rerun --failed` (attempt 2 green, builds reused). Harde...\n*32│| `spt dummy_harness_e2e` (BOTH tests: `endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches` + `endpoint_run_attach_awaits_online_before_attaching`) | 1× kitsubito (2026-07-05, REMOTE-TRUTH Batch-2 re-gate @f2a799e; roles swap run-to-run) | **process-global `set_var` race in a multi-test e2e binary.** Both tests `std::env::set_var(\"SPT_HOME\", own_tempdir)` then do IN-PROC reads (`perch::spt_home`/`adapters_dir`, `registry::register`, psychebin `fs::copy` staging). One file = one test binar...\n 33│| `spt-store registry::tests::concurrent_registration_never_locks` | 1× hfenduleam local gate (2026-07-05, REMOTE-TRUTH Batch-2 bless matrix @3f6f327, fresh worktree, `nextest --workspace --no-fail-fast`) | 16-concurrent-writer SQLite registration gate FAILED at 5.63s — the SQLITE_BUSY load-contention class ALREADY documented at `db.rs:60`: box saturated by the 1588-test full-parallel suite meant a writer couldn't schedule its retry inside the default 5s `registry_busy_timeout`. Isolated ×5: green, 0...\n 34│| `spt-daemon::resume resume_mode_brain_spawning_new_sessions_delivers_each` | 1× kitsubito (2026-07-05 run 28768663321 attempt 1, REMOTE-TRUTH [twohost] PR #52) | Phase-B heavy-class timing EOF — \"session exited before the expected output arrived\" (UnexpectedEof) at 62s under the serialized heavy pool. SAME FAMILY as this file's prior Linux-CI flake (7940318, D4-2b: sequential spawn/read hardening) — a spawned session's exit raced the expected output read on the slow shared box. File untouched by t...\n 35│| `spt-daemon::input_ack_deadlock input_flood_through_serve_attach_does_not_deadlock_broker` | 1× hfenduleam local gate (2026-07-06, REMOTE-TRUTH F-2 matrix @a21bc6b, `nextest --workspace --no-fail-fast`) | deadline-margin under full-suite load: the flood exchange's `recv_timeout(30s)` (input_ack_deadlock.rs:512) blew under the 1608-test parallel pool + 7 leaky daemons — FAIL at 34.5s vs ~32s natural runtime isolated (the margin is ~3s by design). Subsystem disjoint from the triggering commit (F-2 = a...\n   │...\n 49│| `spt-daemon::twohost_web two_host_web_role_b` — the A_CELLS completion witness (:257-284) | 1x, the FIRST-EVER real pair run (one box, 2026-09-07 02:56Z, `.worktrees/gate-w1-26a96d58/.spt/twohost-web/none2/`). No pair had ever run role B before — kitsubito's leg was the env-gated no-op, so this cell had never once executed for real. | **NOT A FLAKE — a DETERMINISTIC completion-witness defect, and it would have failed every pair run forever.** Filed here because doyle routed it here; the distincti...\n*50│| `spt::resident_service_e2e a_declared_service_rises_with_the_daemon_and_reaches_the_cli` — the **:453 PRECONDITION** assert (a THIRD distinct signature for this test: distinct from its :664 teardown-LEAK row above, and the direct successor of that row's noted \":382 PRECONDITION\" signature, which is this assert at its old line) | 1× kitsubito Linux (2026-09-07, the W1 #249 builder battery AT the gated sha `8d980fdf`, gate dir `~/spt-w1/.worktrees/ws272-w1/.spt/ws272-w1-gate/`; nextest exit 100, ONE S...\n 51│| `spt::resume_no_control_steal_e2e brain_respawn_keeps_every_session_controller_and_still_promotes` — keyed on the MESSAGE `the trial candidate never stamped brain.ready`, NOT on the rendered line. The panic renders at `:358`, which is `teardown_panic`'s funnel `panic!` shared by every failing arm in the file (:397 and :490 today); a row keyed on :358 would silently absorb an unrelated future red | 1× kitsubito Linux (2026-09-07, same battery, same Summary and same preserved raw as the `resident_serv...\n 52│| `spt-daemon::mesh_recovery roster_route_survives_a_transient_dial_failure_with_discovery_disabled` — \"never converged: roster route converged after the transient\" (`mesh_recovery.rs:97` at `25e60015`; `converge()` = 600 × 25 ms = 15.0 s wall-clock budget) | 1× hfenduleam Windows golden, #272 WEBSERVE r2 run 34262154550 attempt 3 (2026-09-09 00:43:52Z, job 102283894969, Phase B serialized, cell 15.715 s, 233/234). Same cell PASSED at the same sha on attempts 1 and 2 (9.801 s, 7.172 s). | **BOX CONTE...\n 53│| `spt-daemon::registry_lifecycle oneway_rounds_plateau_rows_seats_and_a_refresh_replays_nothing` — \"the refresh must subscribe NO historical rows (zero replay churn): held 0 -> 1, seats 1\" (`registry_lifecycle.rs:514` at `25e60015`; the directional held/seats assertion after the gen-2 refresh) | 1× hfenduleam Windows golden, #272 WEBSERVE r2 run 34262154550 attempt 4 (2026-09-09 02:36Z, job 102306494097, Phase B serialized, cell 11.594 s, 233/234). Same cell PASSED at the same sha on attempts 1-3 (10...\n### INFRA-REGISTER.md\n 5017│     instead of confounded.\n*5018│  3. **OPERATOR ASK — Defender path exclusions** for `C:\\actions-runner\\_work` and the gate pools,\n 5019│     read from an ADMIN shell first (see the unreadable-row warning above), then re-measure the\n 5020│     first-touch tax on a file **under that path** — the scratchpad measurement is outside the\n 5021│     runner dir, so it proves the mechanism's size, not the runner's exposure. Unsettable\n### KNOWN-HAZARDS.md\n 583│### 7.15 An OFFLINE spt-hosted endpoint must NOT render phantom `ONLINE+CONTROLLED` — clear `driven_by` when its session is gone  `[REQ-HAZARD-DRIVEN-BY-SELFHEAL]`\n*584│- **Failure:** `driven_by` (the `info.json` `ONLINE+CONTROLLED` latch) is single-written by the broker via `clear_controller`/`stamp_driven_by`, which only fire on a controller change. When an spt-hosted endpoint's broker session is GONE (harness dead — the B2 case, 7-series sibling `REQ-HAZARD-HOSTED-LIVENESS-RECONCILE`), no controller event ever fires, so a stale `driven_by=Some(node)` persists: the picker renders a phantom \"controlled by X\" on an endpoint that is actually OFFLINE. The B2 reconcile a...\n 585│- **Invariant:** `reconcile_hosted_liveness`, when it offlines a sessionless controllable perch (the B2 keystone — no live broker session ⇒ dead harness), ALSO clears `driven_by` (`set_driven_by(perch, None)`). RACE-FREE and single-writer-safe: with NO live broker session there is no controller to re-stamp `driven_by` concurrently, so the brain may write it here without contending the broker. (The LIVE-session leg — a controller gone while its session survives — is NOT this hazard: a clean discon...\n 586│- **MEASURED 2026-07-22 (DAEMON-LIFECYCLE W2 Leg B, `transport_death_eof.rs`) — the residual SPLITS in two, and only one half is still open.** On REAL QUIC (two brokers, `BindScope::Loopback` + a real `net_dial` — NOT the in-process duplex, which has no idle timeout at all), a remote controller whose QUIC STACK DIES WITHOUT A FIN (peer frozen: endpoint alive, UDP port still bound, keepalives unanswered — the sleeping-laptop shape) DOES self-heal: the transport's own idle timeout tears the conn, the...\n 587│- **Watch-out (repro-proven, real broker):** `SessionInfo.controller_by==None` is AMBIGUOUS — `dispatch_spawn` pre-attaches the spawner as the LOCAL controller with `by=None`, so a live LOCALLY-driven session also reads `None`. It is therefore NOT a usable standalone `driven_by` clear trigger (would false-clear a live local session). The shipped Gap-B self-heal needs no controller signal at all (it keys on session ABSENCE).\n    │...\n 640│- **spt-core mapping:** impl `spt-daemon/src/inject.rs` `try_spt_hosted_inject`/`is_spt_hosted_no_relay` (the shared leg) called by `wan.rs` `receive_wan` (leg 1) AND `spt/src/api/delivery.rs` `drain_idle_window` (leg 2, via `spool::claim_idle_edge_at`/`release_at`); int (real broker+PTY+binary) `tests/inject_control_wedge.rs` `wan_arrival_to_idle_spt_hosted_injects_with_no_hook_poll` (leg 1) + `spt/tests/idle_edge_drain_e2e.rs` `spool_while_active_then_idle_fires_injection` (leg 2) — both prove delive...\n*641│- **SCOPE (amended 2026-07-27, FIELD-TRUTH W1 roll-in, operator ruling):** this no-starvation guarantee binds the **`default` and `idle_only` classes ONLY**. The **`active_only` (deferred) class is explicitly OUTSIDE it** — per ADR-0028 it is hook-carried background context whose contract is \"active hook window only; never wakes an idle agent\", so with no hook cadence it WAITS. That is starvation *by design*, not a defect, and it is the one case where \"reached the spool\" does not imply \"will be deliver...\n 642│- **Source:** F-023 (BUILD-F023-WANIDLE; doyle RCA + gate, todlando build 2026-07-02). Delivered in two legs: WAN-ingress inject (leg 1) + idle-edge drain (leg 2). Scope narrowed 2026-07-27 (FIELD-TRUTH W1 roll-in) — see SCOPE above.\n 643│<!-- [doc->REQ-HAZARD-DELIVERY-STARVATION] -->\n 644│\n    │...\n 683│- **Invariant:** on session reap the broker clears the perch's control/viewer stamps — `OutputLog::stamp_reaped()` = `set_driven_by(None)` + `set_controlled(false)` + `set_viewer_count(0)` via the known endpoint id — so no teardown path leaves a stamp behind. The broker stays the SINGLE writer; the clear is unconditional (idempotent when already clear) and RACE-FREE — the session is dead, so there is no live controller to re-stamp concurrently (the same argument as 7.15's no-session leg). Best-effo...\n*684│- **spt-core mapping:** impl `broker.rs` `OutputLog::stamp_reaped` + the exit-waiter reap call (before `sessions.remove`); int (real broker+PTY, no mocks) `crates/spt-daemon/tests/control_stamp_lifetime.rs` `reap_clears_control_and_viewer_stamps` (spawn hosted session → the live controller stamps `controlled=true` → latch `driven_by`+`viewer_count` on disk → KILL the child → the reap clears all three; RED-first verified: fix disabled leaves `driven_by=Some`, `controlled=true`, `viewer_count=Some(...\n 685│- **Source:** F-026 W1 #2 (BUILD-F026-PICKERTRUTH; doyle triage + gate, todlando build 2026-07-03).\n 686│\n 687│---\n    │...\n 750│- **Failure (paid-for, rig-CONFIRMED 2026-07-06/07 — `NtSuspendProcess` on the brain, no update involved):** a controller's writer thread does a BLOCKING socket write to its brain subscriber conn. When that brain is suspended (or black-holed) the write never returns. The output-driven eviction path (`append` → `mark_controller_gone`, bounded by 7.12's `CONTROLLER_WRITE_DEADLINE`) only fires on NEW output, and `reap_dead_controller` (7.29 companion) only caught a writer that had EXITED — so a contro...\n*751│- **Invariant:** no broker client ever observes a wedge because the BRAIN stalled. (1) The controller's writer publishes its IN-FLIGHT-write window (`write_blocked_since`: `Some(Instant)` set immediately before the blocking `write_frame`, `None` after — the mutex never held ACROSS the write); a reader sees a `Some` older than `BRAIN_WRITE_DEADLINE` (15 s = 3× the 7.12 controller bound, measured full-with-**zero writer progress**, so a slow-but-DRAINING brain never trips it) as a WEDGED writer and stal...\n 752│- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (...\n 753│- **Source:** LIFECYCLE-TRUTH W2 (doyle rig `NtSuspendProcess` root + design ruling `docs/W2-DESIGN-RULING.md` @60ec0ed — minimal-plus scope, `BRAIN_WRITE_DEADLINE` 15 s, passive resubscribe + observability rider; todlando build 2026-07-07). The v0.27→v0.28 update-wedge night's flagship defect.\n 754│\n    │...\n 844│<!-- [doc->REQ-HAZARD-CONTROLLER-LEASE] -->\n*845│- **Failure (paid-for, hertz same-machine `--take` RCA, field repro 2026-07-16):** terminal A controlled an endpoint; terminal B on the SAME machine ran `spt rc --take`. Local loopback attaches carry only NODE identity, so `resolve_subscribe` computed `same_identity=true` and took the silent successor path for a distinct `--take` — intent never consulted; sink replaced with no `Displaced`, no stream close (the only loud branch is different-remote+Take). `dispatch_input` is session-addressed with no con...\n 846│- **Invariant:** each rc invocation/attach mints a unique controller LEASE id carried through subscribe, the controller slot, and Input/Resize; the only silent successor is same-lease + equal-or-newer generation (dispatcher recovery, ADR-0038 fix-6 preserved); explicit Take on a DISTINCT lease always revokes ATOMICALLY and authoritatively (old lease fenced + old attach stream FORCE-closed; the Displaced notice via the old writer is best-effort — a Full-queue notice drop must never leave the incumbent i...\n 847│- **spt-core mapping:** `spt-daemon/src/broker.rs` `OutputLog::resolve_subscribe` (same-identity branch) + `become_controller` slot + `dispatch_input` fence; `spt-daemon/src/brain.rs` documented `None==None` note; `spt/src/rc.rs` loopback attach origin + `PumpEnd::Displaced` (client side already correct).\n 848│- **Source:** RC-RENDER-TRUTH W2 (hertz RCA filed 2026-07-18, doyle seam-verified same day; ADR-0044).\n### NEXT-MILESTONE-PICKER-TRIAGE.md\n 108│(`crates/spt/src/picker/model.rs:415`) derives Controlled **only** from\n*109│`driven_by.is_some()` — and the legacy local attach path left `driven_by` None.\n 110│(**MODEL CORRECTION**, doyle ruling v3 2026-07-19, applied here 2026-08-19: this\n 111│line read \"`driven_by` is REMOTE-only by design (KH 7.15)\". Falsified —\n 112│`driven_by` names the controlling node, own node included, and the own-hex latch\n    │...\n 121│`controller_node.is_some()`); `display_status` → Controlled when\n*122│`driven_by.is_some() || controlled`; desc pane says \"controlled locally\" when the driver\n 123│is unnamed. **Red-first seam:** unit — locally-controlled row (driven_by None,\n 124│controlled true) must render `EpDisplay::Controlled`. Confidence HIGH / effort S.\n 125│\n### NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md\n 210│  entirely) queries the broker for every perch stamped\n*211│  controlled/`driven_by=Some(remote)`: no live controller or identity\n 212│  mismatch → clear the stamps. Boot-tick coverage falls out (reconcile runs\n 213│  at boot), which also cleans stamps left by a PRE-restart wedge.\n*214│- KH 7.15 binding: reap ONLY, `by=Some(remote)` only — a local `by=None`\n 215│  controller is never latched or cleared by this path.\n 216│- Red-first: the triage's sever-across-brain-restart int (stamps clear\n 217│  within a tick) + a persisted-stale-stamp unit (stamp says controlled,\n### W3-GATE-VERDICT.md\n 40│  → kill + rollback (conservative — never false-promote onto a wedge). `old_gen_drained` reads\n*41│  broker truth in-process (`any_local_controller_wedged`, `by:None` only — remote conns excluded,\n 42│  the W2 concern), sessions→log lock order, no I/O under lock, poison → false (fail toward\n 43│  promotion, never strands). Fail-safe→drained where nothing to read (net-less / NoTrialEnv).\n 44│  **No normal-apply regression:** a clean-closed old conn drains at once → fast promote; only the\n### W3-WAVE-GATE-REPORT.md\n 34│  whose conn is black-holed reads wedged until the W2 stall-evict releases it; a cleanly-closed\n*35│  conn is absent. **REMOTE conns (`by:Some`) are EXCLUDED** — that is the W2 concern, not an\n 36│  old-gen brain conn (a stuck remote viewer must never gate a brain promotion).\n 37│  `NoTrialEnv` + a net-less/bind-failed broker degrade to `drained=true` (no control plane to\n 38│  gate — the normal fast promote on `brain.ready` is preserved). `ProductionTrialEnv` holds\n   │...\n 55│  (latch releases). Unit (broker): `local_controller_wedged_scopes_to_the_brains_own_conn`\n*56│  (by:None only; remote + idle excluded).\n 57│\n 58│## Local gate (this box, HFENDULEAM — Windows)\n 59│- clippy `-p spt-daemon --all-targets`: clean.\n\n### adr/\n#### 0028-activity-gated-delivery-and-send-modifier-axes.md\n 36│\n*37│The operator needs per-message control over that delivery, beyond the single `--deferred` (spool-only, hook-channel) flag that exists today. The risk is modelling these as one flat list of mutually-exclusive \"message types\" — which conflates independent concerns (*when* vs *through what* vs *how long*) and forbids coherent combinations.\n 38│\n 39│## Decision\n 40│\n\n## crates/spt-daemon/src/\n### broker.rs\n  2854│    /// releases it (or the conn EOFs); a cleanly-closed conn is simply absent. A\n *2855│    /// REMOTE operator's wedged conn (`by: Some`) is the W2 stall-evict concern, NOT\n  2856│    /// an old-gen brain conn, so it is EXCLUDED — a stuck remote viewer must never\n  2857│    /// gate a brain promotion. Pure over the injected `now`/`deadline` (no thread).\n  2858│    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n      │...\n 13138│            send,\n*13139│            by: Some(\"remote-node\".to_string()),\n 13140│            last_ok: Instant::now(),\n 13141│            write_blocked_since: Arc::new(Mutex::new(None)),\n 13142│            _writer: writer,\n      │...\n 13211│    // lazy-reap must NOT touch it — reaping a live-but-idle controller would false-clear\n*13212│    // a genuinely CONTROLLED session's stamp. KH 7.15 (reap-only, by=Some) holds by\n 13213│    // construction: reap only ever CLEARS, never latches driven_by.\n 13214│    #[test]\n 13215│    fn reap_dead_controller_leaves_a_live_writer_untouched() {\n      │...\n 13226│            send,\n*13227│            by: Some(\"remote-node\".to_string()),\n 13228│            last_ok: Instant::now(),\n 13229│            write_blocked_since: Arc::new(Mutex::new(None)),\n 13230│            _writer: writer,\n      │...\n 13307│    // read is LOCAL-scoped: only the brain's OWN wedged conn (by:None) reads wedged;\n*13308│    // a wedged REMOTE operator conn (by:Some) is the W2 concern and must NOT gate a\n 13309│    // brain promotion; an idle local controller (nothing in flight) is not wedged.\n 13310│    #[test]\n 13311│    fn local_controller_wedged_scopes_to_the_brains_own_conn() {\n      │...\n 13321│\n*13322│        // A REMOTE (by:Some) controller wedged the SAME way → NOT counted (W2 concern,\n 13323│        // not an old-gen brain conn — a stuck remote viewer must never gate a promote).\n 13324│        let mut remote = OutputLog::new(2, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 13325│        seed_controller_with_block(&mut remote, Some(\"operator\"), Some(wedged_since()));\n      │...\n 13327│            !remote.local_controller_wedged(now, BRAIN_WRITE_DEADLINE),\n*13328│            \"a wedged REMOTE controller is excluded from the drained gate\"\n 13329│        );\n 13330│        remote.controller = None;\n 13331│\n      │...\n 16105│                            intent: AttachIntent::Control,\n*16106│                            by: Some(\"old-op\".to_string()),\n 16107│                            gen: 100,\n 16108│                            code: None,\n 16109│                            seal_ceremony: false,\n      │...\n 16140│                            intent: AttachIntent::Take,\n*16141│                            by: Some(\"new-op\".to_string()),\n 16142│                            gen: 200,\n 16143│                            code: None,\n 16144│                            seal_ceremony: true,","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1789528556986}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_giaDL1fZp5XJTqiyYh4hlqwn|fc_0cf46cef591a107c016aaa09f7571c87d29bedefc7657879dc","toolName":"read","startedAt":"2026-09-16T03:16:07.415Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs:100-137,579-687"},"intent":"Reading actual redispatch workload"},"id":"d895156a","parentId":"92b62a80","timestamp":"2026-09-16T03:16:07.415Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_NG6HNcYPdlWO2cJ4LtpKqlxc|fc_0cf46cef591a107c016aaa09f7573087d2a3615559577c081a","toolName":"read","startedAt":"2026-09-16T03:16:07.415Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs:850-1000"},"intent":"Reading small restart replay case"},"id":"6d19b86a","parentId":"d895156a","timestamp":"2026-09-16T03:16:07.415Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_OvWBgopjcKp3bsG2ssLcO4th|fc_0cf46cef591a107c016aaa09f7573c87d2bafcd149649ad571","toolName":"read","startedAt":"2026-09-16T03:16:07.415Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/attach.rs:480-515,534-615,686-716,756-824"},"intent":"Reading replay and pending branches"},"id":"03fcfcce","parentId":"6d19b86a","timestamp":"2026-09-16T03:16:07.415Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_MmeTVxZLsnWz9Td4nf9BOICi|fc_0cf46cef591a107c016aaa09f7574487d2a158b6d4e8f18ffe","toolName":"read","startedAt":"2026-09-16T03:16:07.415Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs:415-455,478-535,688-788"},"intent":"Reading forwarding and stale containment"},"id":"8f71414e","parentId":"03fcfcce","timestamp":"2026-09-16T03:16:07.415Z"}
{"type":"message","id":"e3ad979d","parentId":"8f71414e","timestamp":"2026-09-16T03:16:07.415Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_giaDL1fZp5XJTqiyYh4hlqwn|fc_0cf46cef591a107c016aaa09f7571c87d29bedefc7657879dc","name":"read","arguments":{"i":"Reading actual redispatch workload","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs:100-137,579-687"},"intent":"Reading actual redispatch workload"},{"type":"toolCall","id":"call_NG6HNcYPdlWO2cJ4LtpKqlxc|fc_0cf46cef591a107c016aaa09f7573087d2a3615559577c081a","name":"read","arguments":{"i":"Reading small restart replay case","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs:850-1000"},"intent":"Reading small restart replay case"},{"type":"toolCall","id":"call_OvWBgopjcKp3bsG2ssLcO4th|fc_0cf46cef591a107c016aaa09f7573c87d2bafcd149649ad571","name":"read","arguments":{"i":"Reading replay and pending branches","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/attach.rs:480-515,534-615,686-716,756-824"},"intent":"Reading replay and pending branches"},{"type":"toolCall","id":"call_MmeTVxZLsnWz9Td4nf9BOICi|fc_0cf46cef591a107c016aaa09f7574487d2a158b6d4e8f18ffe","name":"read","arguments":{"i":"Reading forwarding and stale containment","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs:415-455,478-535,688-788"},"intent":"Reading forwarding and stale containment"},{"type":"toolCall","id":"call_BPgyqW28rEdANgqMfx45RD4m|fc_0cf46cef591a107c016aaa09f7574c87d296b0ff9b592bf956","name":"read","arguments":{"i":"Reading exact requirement contracts","path":".worktrees/49-267-refresh-discriminator/traceable-reqs.toml:2375-2398,2649-2652,2780-2794"},"intent":"Reading exact requirement contracts"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":17862,"output":280,"cacheRead":48768,"cacheWrite":0,"totalTokens":66910,"cost":{"input":0.17862,"output":0.014,"cacheRead":0.048768,"cacheWrite":0,"total":0.24138800000000002}},"stopReason":"toolUse","timestamp":1789528557009,"responseId":"resp_0cf46cef591a107c016aaa09ed527887d2842677868b267b05","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0cf46cef591a107c016aaa09f7571c87d29bedefc7657879dc","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading actual redispatch workload\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs:100-137,579-687\"}","call_id":"call_giaDL1fZp5XJTqiyYh4hlqwn","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"read","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa09f7573087d2a3615559577c081a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading small restart replay case\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs:850-1000\"}","call_id":"call_NG6HNcYPdlWO2cJ4LtpKqlxc","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"read","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa09f7573c87d2bafcd149649ad571","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading replay and pending branches\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/attach.rs:480-515,534-615,686-716,756-824\"}","call_id":"call_OvWBgopjcKp3bsG2ssLcO4th","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"read","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa09f7574487d2a158b6d4e8f18ffe","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading forwarding and stale containment\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs:415-455,478-535,688-788\"}","call_id":"call_MmeTVxZLsnWz9Td4nf9BOICi","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"read","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa09f7574c87d296b0ff9b592bf956","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading exact requirement contracts\",\"path\":\".worktrees/49-267-refresh-discriminator/traceable-reqs.toml:2375-2398,2649-2652,2780-2794\"}","call_id":"call_BPgyqW28rEdANgqMfx45RD4m","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"read","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}}]},"duration":10403.100899999961,"ttft":10359.988199999556,"completedAt":1789528567413,"contextSnapshot":{"promptTokens":66630,"nonMessageTokens":10304,"compactionEpoch":0}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BPgyqW28rEdANgqMfx45RD4m|fc_0cf46cef591a107c016aaa09f7574c87d296b0ff9b592bf956","toolName":"read","startedAt":"2026-09-16T03:16:07.415Z","args":{"path":".worktrees/49-267-refresh-discriminator/traceable-reqs.toml:2375-2398,2649-2652,2780-2794"},"intent":"Reading exact requirement contracts"},"id":"61c22f56","parentId":"e3ad979d","timestamp":"2026-09-16T03:16:07.415Z"}
{"type":"message","id":"28ff984e","parentId":"61c22f56","timestamp":"2026-09-16T03:16:07.421Z","message":{"role":"toolResult","toolCallId":"call_NG6HNcYPdlWO2cJ4LtpKqlxc|fc_0cf46cef591a107c016aaa09f7573087d2a3615559577c081a","toolName":"read","content":[{"type":"text","text":"fn broker_spawns_the_pty_child_in_the_requested_cwd() {\n…\n    brain\n        .read_output_until(leaf.as_bytes(), &mut out)\n        .expect(\"child prints its cwd\");\n    assert!(\n        count(&out, leaf.as_bytes()) >= 1,\n        \"the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}\",\n        String::from_utf8_lossy(&out)\n    );\n    brain.kill_session().expect(\"cleanup kill\");\n}\n\n// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain\n// crash mid-attach: the successor re-serves the SAME broker-held stream from\n// seq 0 (worst-case stale cursor) — the replayed Request/input/output all\n// dedup through the journals, input B sent INTO the dead window arrives, and\n// B's viewport holds every marker exactly once.\n// [unit->REQ-INST-8]\n#[test]\nfn attach_survives_target_brain_restart_exactly_once() {\n    let _home = init_home();\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let (name_a, name_b) = (unique_name(), unique_name());\n    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\n\n    // Target A life 1: spawn the child; operator B dials + requests + types.\n    let mut life1 = connect_retry(&name_a);\n    let sid = life1\n        .spawn_session(echo_spawn_req())\n        .expect(\"spawn echo child\");\n    let a_addr = life1.net_status().expect(\"a status\").addr;\n\n    let mut operator = connect_retry(&name_b);\n    let conn = operator\n        .net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1)))\n        .expect(\"dial\");\n    let stream_b = request_attach(\n        &mut operator,\n        conn.conn_id,\n        sid,\n        0,\n        MintedOp::new(Minter::Rc, 2),\n        AttachIntent::Control,\n    )\n    .expect(\"request\");\n    operator\n        .net_stream_subscribe(stream_b, 0)\n        .expect(\"operator subscribe\");\n    send_attach_input(&mut operator, stream_b, b\"MARKER_ONE\\r\", 10).expect(\"input 1\");\n\n    // Life 1 serves manually (the serve loop's exact steps, stepped so the\n    // crash point is deterministic): handle the Request + input, pump output\n    // records until MARKER_ONE went onto the wire — then die.\n    let (stream_a, origin) = wait_for_stream(&mut life1, &[]);\n    life1\n        .net_stream_subscribe(stream_a, 0)\n        .expect(\"life1 subscribe\");\n    let mut decoder = AttachDecoder::new();\n    let mut sent: Vec<u8> = Vec::new(); // bytes life 1 pumped onto the wire\n    let deadline = life1.call_deadline();\n    'life1: loop {\n        match life1.read_event_until(deadline).expect(\"life1 event\") {\n            BrokerEvent::NetStreamData {\n                stream_id, bytes, ..\n            } if stream_id == stream_a => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Request {\n                            session_id,\n                            from_seq,\n                            ..\n                        } => {\n                            life1.attach(session_id, from_seq).expect(\"attach\");\n                        }\n                        AttachRecord::Input { data_b64, op_id } => {\n                            let b = decode_bytes(&data_b64).expect(\"b64\");\n                            life1\n                                .send_effect(MintedOp::new(Minter::Rc, op_id), &b)\n                                .expect(\"journaled input\");\n                        }\n                        _ => {}\n                    }\n                }\n            }\n            BrokerEvent::Output { seq, bytes, .. } => {\n                let line = ndjson::encode_line(&AttachRecord::Output {\n                    seq,\n                    data_b64: spt_daemon::msg::encode_bytes(&bytes),\n                });\n                life1\n                    .net_stream_send(stream_a, &line, None, false)\n                    .expect(\"output send\");\n                sent.extend_from_slice(&bytes);\n                if count(&sent, b\"MARKER_ONE\") >= 1 {\n                    break 'life1; // crash with the marker on the wire\n                }\n            }\n            BrokerEvent::Error { message } => panic!(\"life1 broker error: {message}\"),\n            _ => {}\n        }\n    }\n    let state = life1.snapshot();\n    drop(life1); // the crash — broker A, session, stream, journal all survive\n\n    // Into the DEAD window: B types the second marker (buffered by A's broker).\n    send_attach_input(&mut operator, stream_b, b\"MARKER_TWO\\r\", 11).expect(\"input 2\");\n\n    // Life 2: handoff, then re-serve the SAME stream from seq 0 — the\n    // worst-case replay. serve_attach re-handles the replayed Request (gate +\n    // re-attach from 0, so every output record re-transmits — B's render\n    // cursor drops the duplicates), replays MARKER_ONE's input (PTY-write\n    // journal dedups op 10 — typed once), and pumps the dead window's\n    // MARKER_TWO.\n    let mut life2 = Brain::handoff(&name_a, state, 2).expect(\"handoff\");\n    let origin2 = origin.clone();\n    let serve_broker = name_a.clone();\n    let server = thread::spawn(move || {\n        let outcome = serve_attach(\n            &mut life2,\n            &serve_broker,\n            stream_a,\n            \"ling\",\n            &origin2,\n            0,\n            None,\n            None,\n        )\n            .expect(\"re-serve\");\n        (outcome, life2)\n    });\n\n    // B renders through the dead-window marker, then detaches.\n    let mut op_decoder = AttachDecoder::new();\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\n    render_until(\n        &mut operator,\n        stream_b,\n        &mut op_decoder,\n        &mut cursor,\n        &mut viewport,\n        b\"MARKER_TWO\",\n    );\n    operator\n        .net_stream_send(stream_b, &[], None, true)\n        .expect(\"detach\");\n    let (outcome, mut life2) = server.join().expect(\"serve thread\");\n    assert_eq!(outcome, AttachServeOutcome::Detached);\n\n    // Exactly-once, stated against the SCREEN MODEL (#6 / ADR-0031). Attach no\n    // longer replays the raw ring — the broker synthesizes a clean current-screen\n    // repaint — so the old \"remote wire byte-identical to the raw local log\" proxy\n    // no longer holds (that raw replay is exactly what #6 removed). Instead we feed\n    // the operator's ENTIRE received byte stream (early live frames + the\n    // re-serve's repaint + the dead-window frames) through a `ScreenGrid` and assert\n    // the resulting authoritative screen still shows BOTH markers. The screen is the\n…\n}\n\n[Showing lines 849-1003 of 2144. Use :1004 to continue]"}],"details":{"truncation":{"content":"    brain\r\n        .read_output_until(leaf.as_bytes(), &mut out)\r\n        .expect(\"child prints its cwd\");\r\n    assert!(\r\n        count(&out, leaf.as_bytes()) >= 1,\r\n        \"the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}\",\r\n        String::from_utf8_lossy(&out)\r\n    );\r\n    brain.kill_session().expect(\"cleanup kill\");\r\n}\r\n\r\n// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain\r\n// crash mid-attach: the successor re-serves the SAME broker-held stream from\r\n// seq 0 (worst-case stale cursor) — the replayed Request/input/output all\r\n// dedup through the journals, input B sent INTO the dead window arrives, and\r\n// B's viewport holds every marker exactly once.\r\n// [unit->REQ-INST-8]\r\n#[test]\r\nfn attach_survives_target_brain_restart_exactly_once() {\r\n    let _home = init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let (name_a, name_b) = (unique_name(), unique_name());\r\n    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\r\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\r\n\r\n    // Target A life 1: spawn the child; operator B dials + requests + types.\r\n    let mut life1 = connect_retry(&name_a);\r\n    let sid = life1\r\n        .spawn_session(echo_spawn_req())\r\n        .expect(\"spawn echo child\");\r\n    let a_addr = life1.net_status().expect(\"a status\").addr;\r\n\r\n    let mut operator = connect_retry(&name_b);\r\n    let conn = operator\r\n        .net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1)))\r\n        .expect(\"dial\");\r\n    let stream_b = request_attach(\r\n        &mut operator,\r\n        conn.conn_id,\r\n        sid,\r\n        0,\r\n        MintedOp::new(Minter::Rc, 2),\r\n        AttachIntent::Control,\r\n    )\r\n    .expect(\"request\");\r\n    operator\r\n        .net_stream_subscribe(stream_b, 0)\r\n        .expect(\"operator subscribe\");\r\n    send_attach_input(&mut operator, stream_b, b\"MARKER_ONE\\r\", 10).expect(\"input 1\");\r\n\r\n    // Life 1 serves manually (the serve loop's exact steps, stepped so the\r\n    // crash point is deterministic): handle the Request + input, pump output\r\n    // records until MARKER_ONE went onto the wire — then die.\r\n    let (stream_a, origin) = wait_for_stream(&mut life1, &[]);\r\n    life1\r\n        .net_stream_subscribe(stream_a, 0)\r\n        .expect(\"life1 subscribe\");\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut sent: Vec<u8> = Vec::new(); // bytes life 1 pumped onto the wire\r\n    let deadline = life1.call_deadline();\r\n    'life1: loop {\r\n        match life1.read_event_until(deadline).expect(\"life1 event\") {\r\n            BrokerEvent::NetStreamData {\r\n                stream_id, bytes, ..\r\n            } if stream_id == stream_a => {\r\n                for rec in decoder.push(&bytes) {\r\n                    match rec {\r\n                        AttachRecord::Request {\r\n                            session_id,\r\n                            from_seq,\r\n                            ..\r\n                        } => {\r\n                            life1.attach(session_id, from_seq).expect(\"attach\");\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id } => {\r\n                            let b = decode_bytes(&data_b64).expect(\"b64\");\r\n                            life1\r\n                                .send_effect(MintedOp::new(Minter::Rc, op_id), &b)\r\n                                .expect(\"journaled input\");\r\n                        }\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n            BrokerEvent::Output { seq, bytes, .. } => {\r\n                let line = ndjson::encode_line(&AttachRecord::Output {\r\n                    seq,\r\n                    data_b64: spt_daemon::msg::encode_bytes(&bytes),\r\n                });\r\n                life1\r\n                    .net_stream_send(stream_a, &line, None, false)\r\n                    .expect(\"output send\");\r\n                sent.extend_from_slice(&bytes);\r\n                if count(&sent, b\"MARKER_ONE\") >= 1 {\r\n                    break 'life1; // crash with the marker on the wire\r\n                }\r\n            }\r\n            BrokerEvent::Error { message } => panic!(\"life1 broker error: {message}\"),\r\n            _ => {}\r\n        }\r\n    }\r\n    let state = life1.snapshot();\r\n    drop(life1); // the crash — broker A, session, stream, journal all survive\r\n\r\n    // Into the DEAD window: B types the second marker (buffered by A's broker).\r\n    send_attach_input(&mut operator, stream_b, b\"MARKER_TWO\\r\", 11).expect(\"input 2\");\r\n\r\n    // Life 2: handoff, then re-serve the SAME stream from seq 0 — the\r\n    // worst-case replay. serve_attach re-handles the replayed Request (gate +\r\n    // re-attach from 0, so every output record re-transmits — B's render\r\n    // cursor drops the duplicates), replays MARKER_ONE's input (PTY-write\r\n    // journal dedups op 10 — typed once), and pumps the dead window's\r\n    // MARKER_TWO.\r\n    let mut life2 = Brain::handoff(&name_a, state, 2).expect(\"handoff\");\r\n    let origin2 = origin.clone();\r\n    let serve_broker = name_a.clone();\r\n    let server = thread::spawn(move || {\r\n        let outcome = serve_attach(\r\n            &mut life2,\r\n            &serve_broker,\r\n            stream_a,\r\n            \"ling\",\r\n            &origin2,\r\n            0,\r\n            None,\r\n            None,\r\n        )\r\n            .expect(\"re-serve\");\r\n        (outcome, life2)\r\n    });\r\n\r\n    // B renders through the dead-window marker, then detaches.\r\n    let mut op_decoder = AttachDecoder::new();\r\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\r\n    render_until(\r\n        &mut operator,\r\n        stream_b,\r\n        &mut op_decoder,\r\n        &mut cursor,\r\n        &mut viewport,\r\n        b\"MARKER_TWO\",\r\n    );\r\n    operator\r\n        .net_stream_send(stream_b, &[], None, true)\r\n        .expect(\"detach\");\r\n    let (outcome, mut life2) = server.join().expect(\"serve thread\");\r\n    assert_eq!(outcome, AttachServeOutcome::Detached);\r\n\r\n    // Exactly-once, stated against the SCREEN MODEL (#6 / ADR-0031). Attach no\r\n    // longer replays the raw ring — the broker synthesizes a clean current-screen\r\n    // repaint — so the old \"remote wire byte-identical to the raw local log\" proxy\r\n    // no longer holds (that raw replay is exactly what #6 removed). Instead we feed\r\n    // the operator's ENTIRE received byte stream (early live frames + the\r\n    // re-serve's repaint + the dead-window frames) through a `ScreenGrid` and assert\r\n    // the resulting authoritative screen still shows BOTH markers. The screen is the\r","truncated":true,"truncatedBy":"lines","totalLines":1296,"totalBytes":6574,"outputLines":155,"outputBytes":6574,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":2144,"displayContent":{"text":"fn broker_spawns_the_pty_child_in_the_requested_cwd() {\n…\n    brain\r\n        .read_output_until(leaf.as_bytes(), &mut out)\r\n        .expect(\"child prints its cwd\");\r\n    assert!(\r\n        count(&out, leaf.as_bytes()) >= 1,\r\n        \"the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}\",\r\n        String::from_utf8_lossy(&out)\r\n    );\r\n    brain.kill_session().expect(\"cleanup kill\");\r\n}\r\n\r\n// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain\r\n// crash mid-attach: the successor re-serves the SAME broker-held stream from\r\n// seq 0 (worst-case stale cursor) — the replayed Request/input/output all\r\n// dedup through the journals, input B sent INTO the dead window arrives, and\r\n// B's viewport holds every marker exactly once.\r\n// [unit->REQ-INST-8]\r\n#[test]\r\nfn attach_survives_target_brain_restart_exactly_once() {\r\n    let _home = init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let (name_a, name_b) = (unique_name(), unique_name());\r\n    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\r\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\r\n\r\n    // Target A life 1: spawn the child; operator B dials + requests + types.\r\n    let mut life1 = connect_retry(&name_a);\r\n    let sid = life1\r\n        .spawn_session(echo_spawn_req())\r\n        .expect(\"spawn echo child\");\r\n    let a_addr = life1.net_status().expect(\"a status\").addr;\r\n\r\n    let mut operator = connect_retry(&name_b);\r\n    let conn = operator\r\n        .net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1)))\r\n        .expect(\"dial\");\r\n    let stream_b = request_attach(\r\n        &mut operator,\r\n        conn.conn_id,\r\n        sid,\r\n        0,\r\n        MintedOp::new(Minter::Rc, 2),\r\n        AttachIntent::Control,\r\n    )\r\n    .expect(\"request\");\r\n    operator\r\n        .net_stream_subscribe(stream_b, 0)\r\n        .expect(\"operator subscribe\");\r\n    send_attach_input(&mut operator, stream_b, b\"MARKER_ONE\\r\", 10).expect(\"input 1\");\r\n\r\n    // Life 1 serves manually (the serve loop's exact steps, stepped so the\r\n    // crash point is deterministic): handle the Request + input, pump output\r\n    // records until MARKER_ONE went onto the wire — then die.\r\n    let (stream_a, origin) = wait_for_stream(&mut life1, &[]);\r\n    life1\r\n        .net_stream_subscribe(stream_a, 0)\r\n        .expect(\"life1 subscribe\");\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut sent: Vec<u8> = Vec::new(); // bytes life 1 pumped onto the wire\r\n    let deadline = life1.call_deadline();\r\n    'life1: loop {\r\n        match life1.read_event_until(deadline).expect(\"life1 event\") {\r\n            BrokerEvent::NetStreamData {\r\n                stream_id, bytes, ..\r\n            } if stream_id == stream_a => {\r\n                for rec in decoder.push(&bytes) {\r\n                    match rec {\r\n                        AttachRecord::Request {\r\n                            session_id,\r\n                            from_seq,\r\n                            ..\r\n                        } => {\r\n                            life1.attach(session_id, from_seq).expect(\"attach\");\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id } => {\r\n                            let b = decode_bytes(&data_b64).expect(\"b64\");\r\n                            life1\r\n                                .send_effect(MintedOp::new(Minter::Rc, op_id), &b)\r\n                                .expect(\"journaled input\");\r\n                        }\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n            BrokerEvent::Output { seq, bytes, .. } => {\r\n                let line = ndjson::encode_line(&AttachRecord::Output {\r\n                    seq,\r\n                    data_b64: spt_daemon::msg::encode_bytes(&bytes),\r\n                });\r\n                life1\r\n                    .net_stream_send(stream_a, &line, None, false)\r\n                    .expect(\"output send\");\r\n                sent.extend_from_slice(&bytes);\r\n                if count(&sent, b\"MARKER_ONE\") >= 1 {\r\n                    break 'life1; // crash with the marker on the wire\r\n                }\r\n            }\r\n            BrokerEvent::Error { message } => panic!(\"life1 broker error: {message}\"),\r\n            _ => {}\r\n        }\r\n    }\r\n    let state = life1.snapshot();\r\n    drop(life1); // the crash — broker A, session, stream, journal all survive\r\n\r\n    // Into the DEAD window: B types the second marker (buffered by A's broker).\r\n    send_attach_input(&mut operator, stream_b, b\"MARKER_TWO\\r\", 11).expect(\"input 2\");\r\n\r\n    // Life 2: handoff, then re-serve the SAME stream from seq 0 — the\r\n    // worst-case replay. serve_attach re-handles the replayed Request (gate +\r\n    // re-attach from 0, so every output record re-transmits — B's render\r\n    // cursor drops the duplicates), replays MARKER_ONE's input (PTY-write\r\n    // journal dedups op 10 — typed once), and pumps the dead window's\r\n    // MARKER_TWO.\r\n    let mut life2 = Brain::handoff(&name_a, state, 2).expect(\"handoff\");\r\n    let origin2 = origin.clone();\r\n    let serve_broker = name_a.clone();\r\n    let server = thread::spawn(move || {\r\n        let outcome = serve_attach(\r\n            &mut life2,\r\n            &serve_broker,\r\n            stream_a,\r\n            \"ling\",\r\n            &origin2,\r\n            0,\r\n            None,\r\n            None,\r\n        )\r\n            .expect(\"re-serve\");\r\n        (outcome, life2)\r\n    });\r\n\r\n    // B renders through the dead-window marker, then detaches.\r\n    let mut op_decoder = AttachDecoder::new();\r\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\r\n    render_until(\r\n        &mut operator,\r\n        stream_b,\r\n        &mut op_decoder,\r\n        &mut cursor,\r\n        &mut viewport,\r\n        b\"MARKER_TWO\",\r\n    );\r\n    operator\r\n        .net_stream_send(stream_b, &[], None, true)\r\n        .expect(\"detach\");\r\n    let (outcome, mut life2) = server.join().expect(\"serve thread\");\r\n    assert_eq!(outcome, AttachServeOutcome::Detached);\r\n\r\n    // Exactly-once, stated against the SCREEN MODEL (#6 / ADR-0031). Attach no\r\n    // longer replays the raw ring — the broker synthesizes a clean current-screen\r\n    // repaint — so the old \"remote wire byte-identical to the raw local log\" proxy\r\n    // no longer holds (that raw replay is exactly what #6 removed). Instead we feed\r\n    // the operator's ENTIRE received byte stream (early live frames + the\r\n    // re-serve's repaint + the dead-window frames) through a `ScreenGrid` and assert\r\n    // the resulting authoritative screen still shows BOTH markers. The screen is the\r\n…\n}","startLine":809,"lineNumbers":[809,null,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,null,1033]},"fileSize":89554,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon\\tests\\attach.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2144,"totalBytes":6574,"outputLines":155,"outputBytes":6574,"shownRange":{"start":849,"end":1003},"nextOffset":1004}}},"isError":false,"timestamp":1789528567421}}
{"type":"message","id":"4a989710","parentId":"28ff984e","timestamp":"2026-09-16T03:16:07.424Z","message":{"role":"toolResult","toolCallId":"call_OvWBgopjcKp3bsG2ssLcO4th|fc_0cf46cef591a107c016aaa09f7573c87d2bafcd149649ad571","toolName":"read","content":[{"type":"text","text":"pub fn serve_attach(\n…\n    remote_session: Option<u64>,\n    seed_request: Option<AttachRecord>,\n) -> io::Result<AttachServeOutcome> {\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\n    // session output (broker→serve) — including the controller's synchronous,\n    // inline, multi-frame ring replay (`become_controller`), which holds the\n    // broker's single-threaded per-conn handler for the whole replay. If the\n    // forwards rode `brain` too, that stuck handler could not read them →\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\n    // two directions on independent single-threaded handlers cannot block each\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\n    // a second conn is correct. Order is preserved: this one serve thread reads\n    // every Output in seq order on `brain` and forwards it in that same order on\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\n    // the controller onto that async path would remove the inline-replay block at\n    // the source, but it touches the authoritative cursor-advancing path.)\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\n    let mut decoder = AttachDecoder::new();\n    let mut attached = false;\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\n    // race on the input gate).\n    let mut intent = AttachIntent::Control;\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\n    // controller input that races ahead of the reply is BUFFERED, not dropped\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\n    // we know we control the session (the busy-refuse case).\n    let mut role: Option<ServeRole> = None;\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\n…\n    // The attached session — captured from the Request so the terminal paths can\n    // explicitly release this worker's role (clearing driven_by / viewer_count\n    // promptly, not only on connection drop).\n    let mut attached_session: Option<u64> = None;\n    // This worker's attach GENERATION (ADR-0038 Amendment fix 6) — set when\n    // its Request lands; every terminal release carries it so a STALE worker\n    // (an older generation) can never clear a replacement controller.\n    let mut serve_gen: u64 = 0;\n    // Recovered from the pinned Request, not from the serving daemon's version.\n    // [impl->REQ-ATTACH-CLIENT-STALE]\n    let mut client_identity = None;\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\n    // intermittent live bursts, never a CPU spin.\n    let mut last_resubscribe: Option<Instant> = None;\n\n    // Durable-opener seed (ADR-0038 B): process the dispatcher-pinned Request\n    // BEFORE the event loop, through the exact wire-Request path — a rolled\n    // ring can no longer replay it. Non-Request seeds are ignored (the opener\n    // of an attach stream is always its Request).\n    if let Some(AttachRecord::Request {\n        session_id,\n        from_seq,\n        intent: req_intent,\n        endpoint_id: _,\n        gen: req_gen,\n        code: req_code,\n        seal_ceremony: req_seal_ceremony,\n        client,\n    }) = seed_request\n    {\n        if let Some(out) = w2b_probe!(\"seed_request\", handle_attach_request(\n            brain,\n            &mut wire,\n            stream_id,\n            endpoint,\n            origin_node,\n            remote_session,\n            session_id,\n            from_seq,\n            req_intent,\n            req_gen,\n            req_code,\n            req_seal_ceremony,\n            &mut intent,\n            &mut attached_session,\n            &mut attached,\n            &mut serve_gen,\n        ))? {\n            return Ok(out);\n        }\n        client_identity = client;\n    }\n\n    // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n    // Stream lifetime is subscriber-owned; any silence bound belongs per read, not per call.\n    let deadline: Option<Instant> = None;\n    loop {\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\n        // role resolves no Output flows, so a plain read is correct then.\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n        let event = w2b_probe!(\"read_event\", match (role, attached_session) {\n            (Some(ServeRole::Controller), Some(sid)) => {\n                match read_controller_event_resuming(brain, sid, serve_gen, Some(origin_node)) {\n                    Ok(ev) => ev,\n                    Err(e)\n                        if e.get_ref()\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\n                    {\n                        // Irrecoverable: the dropped frames rolled out of the ring.\n                        // SURFACE a marked truncation to the operator (never a\n                        // silent skip = B2 lie, never a spin), then end cleanly.\n                        // Graceful continuation is deferred\n                        // (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND).\n                        let notice =\n                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\n                        let line = ndjson::encode_line(&AttachRecord::Output {\n                            seq: brain.controller_resume_floor(),\n                            data_b64: encode_bytes(notice),\n                        });\n…\n                    }\n…\n                }\n            }\n…\n        });\n…\n            BrokerEvent::NetStreamData {\n…\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n…\n                        AttachRecord::Input { data_b64, op_id }\n                            if attached && intent != AttachIntent::Viewer =>\n                        {\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                            match role {\n                                // Confirmed controller — exactly-once at the PTY\n                                // (the operator-minted id rides the B5 journal). NO-ACK\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\n                                // fire-and-forward (the ack is discarded here) and can\n                                // flood N input frames on this one conn; an acked write\n                                // back would fill the return direction and deadlock the\n                                // broker's per-conn handler. Dedup still holds at the\n                                // broker (op_id applied-set).\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n                                Some(ServeRole::Controller) => {\n                                    // The op_id is operator-minted (rc attach OpMinter),\n                                    // so it carries the `rc` journal tag (ADR-0034).\n                                    w2b_probe!(\"live_input\", brain.send_effect_no_ack(\n                                        MintedOp::new(Minter::Rc, op_id),\n                                        &bytes,\n                                    ))?\n                                }\n                                // Outcome not yet known — buffer; flushed on a\n                                // controller confirmation, discarded on busy/viewer.\n                                None => pending_input.push((op_id, bytes)),\n                                // A viewer never reaches here (intent gate).\n                                Some(ServeRole::Viewer) => {}\n                            }\n                        }\n                        AttachRecord::Resize { rows, cols }\n                            if attached && intent != AttachIntent::Viewer =>\n…\n                        }\n…\n                        // Input/Resize before Request, a viewer's input/resize,\n                        // or target-direction records echoed back: noise — ignore.\n                        _ => {}\n                    }\n                }\n            }\n\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\n            BrokerEvent::Subscribed { outcome, note, .. } => {\n                use crate::msg::SubscribeOutcome as O;\n                match outcome {\n                    O::Controller | O::TookControl => {\n                        role = Some(ServeRole::Controller);\n                        // Relay the broker's seat sentence to the human before\n                        // any session output, so the grant is read as a fact\n                        // about the attach and not as something the session\n                        // said. Controller-only: a viewer took no seat, and a\n                        // refused attach gets its own refusal path.\n                        // [impl->REQ-ENGINEROOM-ADMIN-BRINGUP-EMPOWERS]\n                        if let Some(text) = note {\n                            let line = ndjson::encode_line(&AttachRecord::Notice { text });\n                            w2b_probe!(\"seat_notice\", wire.net_stream_send(stream_id, &line, None, false))?;\n                        }\n                        // A driver attached — the wake edge (controller-only;\n                        // idempotent on an already-active seat).\n                        w2b_probe!(\"wake\", feed_rest(endpoint, crate::resting::RestEvent::Wake));\n                        // Flush any input that raced ahead of the reply (operator-\n                        // minted ops carry the `rc` tag).\n                        let pending_count = pending_input.len();\n                        if w2b_trace {\n                            eprintln!(\"[DEBUG-w2b] ms={} pid={} thread={:?} stream={} pending={} enter=pending_flush\",\n                                now_ms(), std::process::id(), std::thread::current().id(), stream_id, pending_count);\n                        }\n                        for (index, (op, b)) in pending_input.drain(..).enumerate() {\n                            if w2b_trace {\n                                eprintln!(\"[DEBUG-w2b] ms={} pid={} thread={:?} stream={} remaining={} op={} bytes={} enter=pending_send_effect\",\n                                    now_ms(), std::process::id(), std::thread::current().id(), stream_id, pending_count - index, op, b.len());\n                            }\n                            let sent = brain.send_effect(MintedOp::new(Minter::Rc, op), &b);\n                            if w2b_trace {\n                                eprintln!(\"[DEBUG-w2b] ms={} pid={} thread={:?} stream={} remaining={} op={} ok={} exit=pending_send_effect\",\n                                    now_ms(), std::process::id(), std::thread::current().id(), stream_id, pending_count - index - 1, op, sent.is_ok());\n                            }\n                            sent?;\n                        }\n                        if w2b_trace {\n                            eprintln!(\"[DEBUG-w2b] ms={} pid={} thread={:?} stream={} pending={} exit=pending_flush\",\n                                now_ms(), std::process::id(), std::thread::current().id(), stream_id, pending_input.len());\n                        }\n                    }\n                    O::Viewer => {\n                        role = Some(ServeRole::Viewer);\n                        // Wake-neutral: a viewer never wakes the endpoint, never\n                        // keeps it awake (Gap B). Discard any racing input.\n                        pending_input.clear();\n                    }\n                    O::BusyControlled { .. } => {\n                        // A different operator controls it — refuse (no PTY input\n                        // ever reached it; the buffer is discarded). The rc\n                        // renders the busy guidance client-side from driven_by.\n                        wire.net_stream_send(stream_id, &[], None, true)?;\n                        return Ok(AttachServeOutcome::Refused);\n                    }\n                }\n                // Both roles use the same transport. A future viewer-specific\n                // policy reaches a surviving viewer after daemon reconstruction.\n                // Old clients omitted identity and receive no advisory at all.\n                // [impl->REQ-ATTACH-CLIENT-STALE]\n                if let Some(client) = client_identity.as_ref() {\n…\n                }\n            }\n…\n    }\n}"}],"details":{"displayContent":{"text":"pub fn serve_attach(\n…\n    remote_session: Option<u64>,\n    seed_request: Option<AttachRecord>,\n) -> io::Result<AttachServeOutcome> {\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\n    // session output (broker→serve) — including the controller's synchronous,\n    // inline, multi-frame ring replay (`become_controller`), which holds the\n    // broker's single-threaded per-conn handler for the whole replay. If the\n    // forwards rode `brain` too, that stuck handler could not read them →\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\n    // two directions on independent single-threaded handlers cannot block each\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\n    // a second conn is correct. Order is preserved: this one serve thread reads\n    // every Output in seq order on `brain` and forwards it in that same order on\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\n    // the controller onto that async path would remove the inline-replay block at\n    // the source, but it touches the authoritative cursor-advancing path.)\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\n    let mut decoder = AttachDecoder::new();\n    let mut attached = false;\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\n    // race on the input gate).\n    let mut intent = AttachIntent::Control;\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\n    // controller input that races ahead of the reply is BUFFERED, not dropped\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\n    // we know we control the session (the busy-refuse case).\n    let mut role: Option<ServeRole> = None;\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\n…\n    // The attached session — captured from the Request so the terminal paths can\n    // explicitly release this worker's role (clearing driven_by / viewer_count\n    // promptly, not only on connection drop).\n    let mut attached_session: Option<u64> = None;\n    // This worker's attach GENERATION (ADR-0038 Amendment fix 6) — set when\n    // its Request lands; every terminal release carries it so a STALE worker\n    // (an older generation) can never clear a replacement controller.\n    let mut serve_gen: u64 = 0;\n    // Recovered from the pinned Request, not from the serving daemon's version.\n    // [impl->REQ-ATTACH-CLIENT-STALE]\n    let mut client_identity = None;\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\n    // intermittent live bursts, never a CPU spin.\n    let mut last_resubscribe: Option<Instant> = None;\n\n    // Durable-opener seed (ADR-0038 B): process the dispatcher-pinned Request\n    // BEFORE the event loop, through the exact wire-Request path — a rolled\n    // ring can no longer replay it. Non-Request seeds are ignored (the opener\n    // of an attach stream is always its Request).\n    if let Some(AttachRecord::Request {\n        session_id,\n        from_seq,\n        intent: req_intent,\n        endpoint_id: _,\n        gen: req_gen,\n        code: req_code,\n        seal_ceremony: req_seal_ceremony,\n        client,\n    }) = seed_request\n    {\n        if let Some(out) = w2b_probe!(\"seed_request\", handle_attach_request(\n            brain,\n            &mut wire,\n            stream_id,\n            endpoint,\n            origin_node,\n            remote_session,\n            session_id,\n            from_seq,\n            req_intent,\n            req_gen,\n            req_code,\n            req_seal_ceremony,\n            &mut intent,\n            &mut attached_session,\n            &mut attached,\n            &mut serve_gen,\n        ))? {\n            return Ok(out);\n        }\n        client_identity = client;\n    }\n\n    // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n    // Stream lifetime is subscriber-owned; any silence bound belongs per read, not per call.\n    let deadline: Option<Instant> = None;\n    loop {\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\n        // role resolves no Output flows, so a plain read is correct then.\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n        let event = w2b_probe!(\"read_event\", match (role, attached_session) {\n            (Some(ServeRole::Controller), Some(sid)) => {\n                match read_controller_event_resuming(brain, sid, serve_gen, Some(origin_node)) {\n                    Ok(ev) => ev,\n                    Err(e)\n                        if e.get_ref()\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\n                    {\n                        // Irrecoverable: the dropped frames rolled out of the ring.\n                        // SURFACE a marked truncation to the operator (never a\n                        // silent skip = B2 lie, never a spin), then end cleanly.\n                        // Graceful continuation is deferred\n                        // (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND).\n                        let notice =\n                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\n                        let line = ndjson::encode_line(&AttachRecord::Output {\n                            seq: brain.controller_resume_floor(),\n                            data_b64: encode_bytes(notice),\n                        });\n…\n                    }\n…\n                }\n            }\n…\n        });\n…\n            BrokerEvent::NetStreamData {\n…\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n…\n                        AttachRecord::Input { data_b64, op_id }\n                            if attached && intent != AttachIntent::Viewer =>\n                        {\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                            match role {\n                                // Confirmed controller — exactly-once at the PTY\n                                // (the operator-minted id rides the B5 journal). NO-ACK\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\n                                // fire-and-forward (the ack is discarded here) and can\n                                // flood N input frames on this one conn; an acked write\n                                // back would fill the return direction and deadlock the\n                                // broker's per-conn handler. Dedup still holds at the\n                                // broker (op_id applied-set).\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n                                Some(ServeRole::Controller) => {\n                                    // The op_id is operator-minted (rc attach OpMinter),\n                                    // so it carries the `rc` journal tag (ADR-0034).\n                                    w2b_probe!(\"live_input\", brain.send_effect_no_ack(\n                                        MintedOp::new(Minter::Rc, op_id),\n                                        &bytes,\n                                    ))?\n                                }\n                                // Outcome not yet known — buffer; flushed on a\n                                // controller confirmation, discarded on busy/viewer.\n                                None => pending_input.push((op_id, bytes)),\n                                // A viewer never reaches here (intent gate).\n                                Some(ServeRole::Viewer) => {}\n                            }\n                        }\n                        AttachRecord::Resize { rows, cols }\n                            if attached && intent != AttachIntent::Viewer =>\n…\n                        }\n…\n                        // Input/Resize before Request, a viewer's input/resize,\n                        // or target-direction records echoed back: noise — ignore.\n                        _ => {}\n                    }\n                }\n            }\n\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\n            BrokerEvent::Subscribed { outcome, note, .. } => {\n                use crate::msg::SubscribeOutcome as O;\n                match outcome {\n                    O::Controller | O::TookControl => {\n                        role = Some(ServeRole::Controller);\n                        // Relay the broker's seat sentence to the human before\n                        // any session output, so the grant is read as a fact\n                        // about the attach and not as something the session\n                        // said. Controller-only: a viewer took no seat, and a\n                        // refused attach gets its own refusal path.\n                        // [impl->REQ-ENGINEROOM-ADMIN-BRINGUP-EMPOWERS]\n                        if let Some(text) = note {\n                            let line = ndjson::encode_line(&AttachRecord::Notice { text });\n                            w2b_probe!(\"seat_notice\", wire.net_stream_send(stream_id, &line, None, false))?;\n                        }\n                        // A driver attached — the wake edge (controller-only;\n                        // idempotent on an already-active seat).\n                        w2b_probe!(\"wake\", feed_rest(endpoint, crate::resting::RestEvent::Wake));\n                        // Flush any input that raced ahead of the reply (operator-\n                        // minted ops carry the `rc` tag).\n                        let pending_count = pending_input.len();\n                        if w2b_trace {\n                            eprintln!(\"[DEBUG-w2b] ms={} pid={} thread={:?} stream={} pending={} enter=pending_flush\",\n                                now_ms(), std::process::id(), std::thread::current().id(), stream_id, pending_count);\n                        }\n                        for (index, (op, b)) in pending_input.drain(..).enumerate() {\n                            if w2b_trace {\n                                eprintln!(\"[DEBUG-w2b] ms={} pid={} thread={:?} stream={} remaining={} op={} bytes={} enter=pending_send_effect\",\n                                    now_ms(), std::process::id(), std::thread::current().id(), stream_id, pending_count - index, op, b.len());\n                            }\n                            let sent = brain.send_effect(MintedOp::new(Minter::Rc, op), &b);\n                            if w2b_trace {\n                                eprintln!(\"[DEBUG-w2b] ms={} pid={} thread={:?} stream={} remaining={} op={} ok={} exit=pending_send_effect\",\n                                    now_ms(), std::process::id(), std::thread::current().id(), stream_id, pending_count - index - 1, op, sent.is_ok());\n                            }\n                            sent?;\n                        }\n                        if w2b_trace {\n                            eprintln!(\"[DEBUG-w2b] ms={} pid={} thread={:?} stream={} pending={} exit=pending_flush\",\n                                now_ms(), std::process::id(), std::thread::current().id(), stream_id, pending_input.len());\n                        }\n                    }\n                    O::Viewer => {\n                        role = Some(ServeRole::Viewer);\n                        // Wake-neutral: a viewer never wakes the endpoint, never\n                        // keeps it awake (Gap B). Discard any racing input.\n                        pending_input.clear();\n                    }\n                    O::BusyControlled { .. } => {\n                        // A different operator controls it — refuse (no PTY input\n                        // ever reached it; the buffer is discarded). The rc\n                        // renders the busy guidance client-side from driven_by.\n                        wire.net_stream_send(stream_id, &[], None, true)?;\n                        return Ok(AttachServeOutcome::Refused);\n                    }\n                }\n                // Both roles use the same transport. A future viewer-specific\n                // policy reaches a surviving viewer after daemon reconstruction.\n                // Old clients omitted identity and receive no advisory at all.\n                // [impl->REQ-ATTACH-CLIENT-STALE]\n                if let Some(client) = client_identity.as_ref() {\n…\n                }\n            }\n…\n    }\n}","startLine":473,"lineNumbers":[473,null,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,null,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,null,620,null,625,626,null,628,null,643,null,647,648,649,null,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,null,723,null,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,null,831,832,null,967,968]},"fileSize":60886,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon\\src\\attach.rs"}}},"isError":false,"timestamp":1789528567424}}
{"type":"message","id":"1605ef7e","parentId":"4a989710","timestamp":"2026-09-16T03:16:07.431Z","message":{"role":"toolResult","toolCallId":"call_giaDL1fZp5XJTqiyYh4hlqwn|fc_0cf46cef591a107c016aaa09f7571c87d29bedefc7657879dc","toolName":"read","content":[{"type":"text","text":"fn echo_spawn_req(endpoint: &str) -> SpawnReq {\n…\n    SpawnReq {\n…\n        adapter: String::new(),\n        install_dir: None,\n    }\n}\n\n/// One REAL dispatcher generation: the `dispatch_fixture` child running\n/// production `run_dispatch_loop` against `broker_name`. Killed = brain death\n/// (worker conns drop). Stderr lands in `log` for replay-storm assertions.\nstruct Generation {\n    child: Child,\n    log: std::path::PathBuf,\n}\n\nimpl Generation {\n    fn spawn(broker_name: &str, log: std::path::PathBuf, extra_env: &[(&str, &str)]) -> Generation {\n        let f = std::fs::File::create(&log).expect(\"gen stderr log\");\n        let mut cmd = Command::new(env!(\"CARGO_BIN_EXE_dispatch_fixture\"));\n        cmd.arg(broker_name)\n            .env(\"SPT_HOME\", std::env::var_os(\"SPT_HOME\").expect(\"test home\"))\n            .stdout(Stdio::null())\n            .stderr(Stdio::from(f));\n        for (k, v) in extra_env {\n            cmd.env(k, v);\n        }\n        let child = cmd.spawn().expect(\"spawn dispatch_fixture\");\n        Generation { child, log }\n    }\n\n    fn stderr_log(&self) -> String {\n        std::fs::read_to_string(&self.log).unwrap_or_default()\n    }\n\n    fn kill(mut self) {\n        let _ = self.child.kill();\n        let _ = self.child.wait();\n    }\n}\n\n…\nfn opener_evicted_active_stream_resumes_after_dispatcher_restart() {\n    let _home = init_home();\n    // Shrink the ring BEFORE the broker starts (read once at NetShared::new).\n    // nextest = one process per test: no cross-test env bleed.\n    std::env::set_var(\"SPT_STREAM_RING_CHUNKS\", \"8\");\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let (name_a, name_b) = (unique_name(), unique_name());\n    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\n\n    let mut probe = connect_retry(&name_a);\n    let sid = probe\n        .spawn_session(echo_spawn_req(\"rd2-ep\"))\n        .expect(\"spawn echo child\");\n    let a_addr = probe.net_status().expect(\"a status\").addr;\n\n    let gen1 = Generation::spawn(&name_a, dir.path().join(\"gen1.log\"), &[]);\n\n    let mut operator = connect_retry(&name_b);\n    let conn = operator\n        .net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1)))\n        .expect(\"dial\");\n    let s1 = request_attach(\n        &mut operator,\n        conn.conn_id,\n        sid,\n        0,\n        MintedOp::new(Minter::Rc, 2),\n        AttachIntent::Control,\n    )\n    .expect(\"request\");\n    operator.net_stream_subscribe(s1, 0).expect(\"subscribe\");\n    send_attach_input(&mut operator, s1, b\"MARK-A\\r\", 10).expect(\"input pre\");\n    let mut dec = AttachDecoder::new();\n    let (mut cur, mut view) = (0u64, Vec::new());\n    render_until(\n        &mut operator,\n        s1,\n        &mut dec,\n        &mut cur,\n        &mut view,\n        b\"MARK-A\",\n        Duration::from_secs(20),\n    );\n    let s1_a = *peer_stream_ids(&mut probe).first().expect(\"s1 row on A\");\n\n    // Roll the opener out of the (8-chunk) ring: enough spaced input records\n    // that the floor advances past seq 0.\n    for i in 0..40u64 {\n        send_attach_input(&mut operator, s1, format!(\"f{i}\\r\").as_bytes(), 20 + i)\n            .expect(\"flood input\");\n        thread::sleep(Duration::from_millis(10));\n    }\n    let end = Instant::now() + Duration::from_secs(20);\n    loop {\n        let rolled = probe\n            .net_streams()\n            .expect(\"net-streams\")\n            .streams\n            .iter()\n            .any(|s| s.stream_id == s1_a && s.floor_seq > 0);\n        if rolled {\n            break;\n        }\n        assert!(\n            Instant::now() < end,\n            \"the ring never rolled past the opener\"\n        );\n        thread::sleep(Duration::from_millis(50));\n    }\n    // The durable fact is still there — this is what gen2 will classify from.\n    let pinned = probe.net_stream_opener(s1_a).expect(\"opener query answers\");\n    assert!(\n        pinned.is_some(),\n        \"the opener stays pinned across the ring roll\"\n    );\n\n    // Restart the dispatcher generation; recovery must classify from the\n    // pinned opener (ring seq 0 is GONE) and resume THIS operator stream.\n    gen1.kill();\n    let gen2 = Generation::spawn(&name_a, dir.path().join(\"gen2.log\"), &[]);\n\n    send_attach_input(&mut operator, s1, b\"MARK-B\\r\", 100).expect(\"input post\");\n    let saw_eof = render_until(\n        &mut operator,\n        s1,\n        &mut dec,\n        &mut cur,\n        &mut view,\n        b\"MARK-B\",\n        Duration::from_secs(30),\n    );\n    assert!(!saw_eof, \"the resumed stream must not be torn down\");\n    let gen2_log = gen2.stderr_log();\n    assert!(\n        !gen2_log.contains(&format!(\"DISPATCH:{s1_a}:Unknown\")),\n        \"gen2 classified the rolled-opener stream Unknown — durable classification failed: {gen2_log}\"\n    );\n\n    gen2.kill();\n    let _ = probe.kill_session();\n}\n\n// [int->REQ-DISPATCH-CLAIM-RETRY] D3 — one injected transient worker-start\n// failure (the SPT_DISPATCH_INJECT_TRANSIENT_ONCE test valve, armed on the\n// generation's env): the FIRST worker attempt fails before setup, the claim\n// requeues (bounded backoff), and the retry serves the stream — the operator\n// still gets the viewport, exactly one controller. Pre-fix the one-shot\n// claimed-set burn abandoned the stream permanently on that first failure."}],"details":{"displayContent":{"text":"fn echo_spawn_req(endpoint: &str) -> SpawnReq {\n…\n    SpawnReq {\n…\n        adapter: String::new(),\n        install_dir: None,\n    }\n}\n\n/// One REAL dispatcher generation: the `dispatch_fixture` child running\n/// production `run_dispatch_loop` against `broker_name`. Killed = brain death\n/// (worker conns drop). Stderr lands in `log` for replay-storm assertions.\nstruct Generation {\n    child: Child,\n    log: std::path::PathBuf,\n}\n\nimpl Generation {\n    fn spawn(broker_name: &str, log: std::path::PathBuf, extra_env: &[(&str, &str)]) -> Generation {\n        let f = std::fs::File::create(&log).expect(\"gen stderr log\");\n        let mut cmd = Command::new(env!(\"CARGO_BIN_EXE_dispatch_fixture\"));\n        cmd.arg(broker_name)\n            .env(\"SPT_HOME\", std::env::var_os(\"SPT_HOME\").expect(\"test home\"))\n            .stdout(Stdio::null())\n            .stderr(Stdio::from(f));\n        for (k, v) in extra_env {\n            cmd.env(k, v);\n        }\n        let child = cmd.spawn().expect(\"spawn dispatch_fixture\");\n        Generation { child, log }\n    }\n\n    fn stderr_log(&self) -> String {\n        std::fs::read_to_string(&self.log).unwrap_or_default()\n    }\n\n    fn kill(mut self) {\n        let _ = self.child.kill();\n        let _ = self.child.wait();\n    }\n}\n\n…\nfn opener_evicted_active_stream_resumes_after_dispatcher_restart() {\n    let _home = init_home();\n    // Shrink the ring BEFORE the broker starts (read once at NetShared::new).\n    // nextest = one process per test: no cross-test env bleed.\n    std::env::set_var(\"SPT_STREAM_RING_CHUNKS\", \"8\");\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let (name_a, name_b) = (unique_name(), unique_name());\n    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\n\n    let mut probe = connect_retry(&name_a);\n    let sid = probe\n        .spawn_session(echo_spawn_req(\"rd2-ep\"))\n        .expect(\"spawn echo child\");\n    let a_addr = probe.net_status().expect(\"a status\").addr;\n\n    let gen1 = Generation::spawn(&name_a, dir.path().join(\"gen1.log\"), &[]);\n\n    let mut operator = connect_retry(&name_b);\n    let conn = operator\n        .net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1)))\n        .expect(\"dial\");\n    let s1 = request_attach(\n        &mut operator,\n        conn.conn_id,\n        sid,\n        0,\n        MintedOp::new(Minter::Rc, 2),\n        AttachIntent::Control,\n    )\n    .expect(\"request\");\n    operator.net_stream_subscribe(s1, 0).expect(\"subscribe\");\n    send_attach_input(&mut operator, s1, b\"MARK-A\\r\", 10).expect(\"input pre\");\n    let mut dec = AttachDecoder::new();\n    let (mut cur, mut view) = (0u64, Vec::new());\n    render_until(\n        &mut operator,\n        s1,\n        &mut dec,\n        &mut cur,\n        &mut view,\n        b\"MARK-A\",\n        Duration::from_secs(20),\n    );\n    let s1_a = *peer_stream_ids(&mut probe).first().expect(\"s1 row on A\");\n\n    // Roll the opener out of the (8-chunk) ring: enough spaced input records\n    // that the floor advances past seq 0.\n    for i in 0..40u64 {\n        send_attach_input(&mut operator, s1, format!(\"f{i}\\r\").as_bytes(), 20 + i)\n            .expect(\"flood input\");\n        thread::sleep(Duration::from_millis(10));\n    }\n    let end = Instant::now() + Duration::from_secs(20);\n    loop {\n        let rolled = probe\n            .net_streams()\n            .expect(\"net-streams\")\n            .streams\n            .iter()\n            .any(|s| s.stream_id == s1_a && s.floor_seq > 0);\n        if rolled {\n            break;\n        }\n        assert!(\n            Instant::now() < end,\n            \"the ring never rolled past the opener\"\n        );\n        thread::sleep(Duration::from_millis(50));\n    }\n    // The durable fact is still there — this is what gen2 will classify from.\n    let pinned = probe.net_stream_opener(s1_a).expect(\"opener query answers\");\n    assert!(\n        pinned.is_some(),\n        \"the opener stays pinned across the ring roll\"\n    );\n\n    // Restart the dispatcher generation; recovery must classify from the\n    // pinned opener (ring seq 0 is GONE) and resume THIS operator stream.\n    gen1.kill();\n    let gen2 = Generation::spawn(&name_a, dir.path().join(\"gen2.log\"), &[]);\n\n    send_attach_input(&mut operator, s1, b\"MARK-B\\r\", 100).expect(\"input post\");\n    let saw_eof = render_until(\n        &mut operator,\n        s1,\n        &mut dec,\n        &mut cur,\n        &mut view,\n        b\"MARK-B\",\n        Duration::from_secs(30),\n    );\n    assert!(!saw_eof, \"the resumed stream must not be torn down\");\n    let gen2_log = gen2.stderr_log();\n    assert!(\n        !gen2_log.contains(&format!(\"DISPATCH:{s1_a}:Unknown\")),\n        \"gen2 classified the rolled-opener stream Unknown — durable classification failed: {gen2_log}\"\n    );\n\n    gen2.kill();\n    let _ = probe.kill_session();\n}\n\n// [int->REQ-DISPATCH-CLAIM-RETRY] D3 — one injected transient worker-start\n// failure (the SPT_DISPATCH_INJECT_TRANSIENT_ONCE test valve, armed on the\n// generation's env): the FIRST worker attempt fails before setup, the claim\n// requeues (bounded backoff), and the retry serves the stream — the operator\n// still gets the viewport, exactly one controller. Pre-fix the one-shot\n// claimed-set burn abandoned the stream permanently on that first failure.","startLine":86,"lineNumbers":[86,null,91,null,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,null,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687]},"fileSize":28348,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon\\tests\\redispatch.rs"}}},"isError":false,"timestamp":1789528567430}}
{"type":"message","id":"a9ad2815","parentId":"1605ef7e","timestamp":"2026-09-16T03:16:07.440Z","message":{"role":"toolResult","toolCallId":"call_MmeTVxZLsnWz9Td4nf9BOICi|fc_0cf46cef591a107c016aaa09f7574487d2a158b6d4e8f18ffe","toolName":"read","content":[{"type":"text","text":"fn poisoned_subscriber_cancels_the_paired_forwarding_leg_then_recovery_serves() {\n    let _home = init_home();\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let (name_a, name_b) = (unique_name(), unique_name());\n    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\n    let mut probe = connect_retry(&name_a);\n\n    let opener = b\"{\\\"hello\\\":2}\\n\";\n    let (mut operator, _sid_b, sid_a) = open_traffic_stream(&name_a, &name_b, &mut probe, opener);\n\n    // The forwarding leg: an A-side conn tick-sending on the stream (ack'd, so\n    // a broker-side refusal surfaces as Err). Reports (ok_ticks, final_err).\n    let fw_name = name_a.clone();\n    let (fw_tx, fw_rx) = std::sync::mpsc::channel::<(u32, String)>();\n    let forwarder = thread::spawn(move || {\n        let mut fw = connect_retry(&fw_name);\n        let mut ok = 0u32;\n        for i in 0..600u64 {\n            match fw.net_stream_send(\n                sid_a,\n                b\"tick\\n\",\n                Some(MintedOp::new(Minter::Cli, 100 + i)),\n                false,\n            ) {\n                Ok(_) => ok += 1,\n                Err(e) => {\n                    let _ = fw_tx.send((ok, e.to_string()));\n                    return;\n                }\n            }\n            thread::sleep(Duration::from_millis(100));\n        }\n        let _ = fw_tx.send((ok, String::new()));\n    });\n\n    // Let the leg prove itself healthy, then wedge: fill the ring and attach\n    // the never-reading subscriber — replay wedge → poison → lease cancel.\n    thread::sleep(Duration::from_millis(500));\n    let chunk = vec![0x41u8; FLOOD_CHUNK];\n…\n    assert!(\n…\n        \"the leg served while the subscriber was healthy\"\n    );\n    assert!(\n        err.contains(\"lease canceled\"),\n        \"the leg must terminate on the LEASE refusal (fix 4), got: {err:?}\"\n    );\n\n    // Recovery: a new subscriber generation renews the lease; a fresh send\n    // serves again — one poison never permanently dead-ends the stream.\n    // Subscribe the new generation from the ring TAIL (empty replay): the\n    // recovery contract under test is the LEASE renewal, not the replay (T1\n    // proves that) — and a from-0 subscriber that never drains the 1.5MB\n    // flood would legitimately wedge and re-cancel, testing nothing.\n    let renewed = {\n        let end = Instant::now() + Duration::from_secs(20);\n        let mut probe_op = 900u64;\n        loop {\n            assert!(\n                Instant::now() < end,\n                \"the lease never renewed for a new generation\"\n            );\n            let tail = probe\n                .net_streams()\n                .expect(\"net-streams\")\n                .streams\n                .iter()\n                .find(|s| s.stream_id == sid_a)\n                .map(|s| s.next_seq)\n                .unwrap_or(0);\n            let mut f = connect_retry(&name_a);\n            f.net_stream_subscribe(sid_a, tail)\n                .expect(\"recovery subscribe\");\n            // Give the fire-and-forget subscribe a beat to process, then\n            // probe the lease with a real send on a separate conn.\n            thread::sleep(Duration::from_millis(300));\n            let mut s = connect_retry(&name_a);\n            let sent = s.net_stream_send(\n                sid_a,\n                b\"recovered\\n\",\n                Some(MintedOp::new(Minter::Cli, probe_op)),\n                false,\n            );\n            probe_op += 1;\n            match sent {\n                Ok(_) => break true,\n                Err(e)\n                    if e.to_string().contains(\"lease canceled\")\n                    || e.to_string().contains(\"subscriber busy\") =>\n                {\n                    thread::sleep(Duration::from_millis(300));\n                }\n                Err(e) => panic!(\"unexpected recovery send failure: {e}\"),\n            }\n        }\n    };\n    assert!(renewed);\n    drop(wedged);\n}\n…\nfn stale_unfinished_attach_replay_neither_takes_nor_clears_the_replacement_controller() {\n    let _home = init_home();\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let (name_a, name_b) = (unique_name(), unique_name());\n    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\n\n    let mut probe = connect_retry(&name_a);\n    let sid = probe\n        .spawn_session(echo_spawn_req(\"rds6-ep\"))\n        .expect(\"spawn echo child\");\n    let a_addr = probe.net_status().expect(\"a status\").addr;\n\n    let gen1 = Generation::spawn(&name_a, dir.path().join(\"gen1.log\"), &[]);\n\n    let mut operator = connect_retry(&name_b);\n    let conn = operator\n        .net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1)))\n        .expect(\"dial\");\n\n    // s1: attached + driven under gen1 — opener pinned, Request + input in\n    // the ring, everything a stale replay needs to steal.\n    let s1 = request_attach(\n        &mut operator,\n        conn.conn_id,\n        sid,\n        0,\n        MintedOp::new(Minter::Rc, 2),\n        AttachIntent::Control,\n    )\n    .expect(\"request s1\");\n    operator.net_stream_subscribe(s1, 0).expect(\"subscribe s1\");\n    send_attach_input(&mut operator, s1, b\"S1-MARK\\r\", 10).expect(\"input s1\");\n    let mut dec1 = AttachDecoder::new();\n    let (mut cur1, mut view1) = (0u64, Vec::new());\n    render_until(\n        &mut operator,\n        s1,\n        &mut dec1,\n        &mut cur1,\n        &mut view1,\n        b\"S1-MARK\",\n        Duration::from_secs(20),\n    );\n    let s1_a = *peer_stream_ids(&mut probe).first().expect(\"s1 row on A\");\n\n    // The raw-close: kill gen1 mid-serve. NO detach follows — s1 stays\n    // UNFINISHED (and unretired) on a live conn: invisible to the\n    // finished-row discriminator, eligible for redispatch.\n    gen1.kill();\n    {\n        // Sanity: the row is genuinely unfinished (the T6 precondition that\n        // separates this from D1b's finished-unretired shape).\n        let rows = probe.net_streams().expect(\"net-streams\").streams;\n        let row = rows.iter().find(|s| s.stream_id == s1_a).expect(\"s1 row\");\n        assert!(\n            !row.finished,\n            \"T6 precondition: the stale row must be UNFINISHED\"\n        );\n    }\n\n    // The REPLACEMENT: same operator identity, same session, NEWER attach\n    // generation (client-minted wall-ms). gen2 replays s1 (unfinished →\n    // served) AND serves s2 — the stale Request's older generation must\n    // refuse against the newer controller whichever order the workers land.\n    let gen2 = Generation::spawn(&name_a, dir.path().join(\"gen2.log\"), &[]);\n    let s2 = request_attach(\n        &mut operator,\n        conn.conn_id,\n        sid,\n        0,\n        MintedOp::new(Minter::Rc, 3),\n        AttachIntent::Control,\n    )\n    .expect(\"request s2\");\n    operator.net_stream_subscribe(s2, 0).expect(\"subscribe s2\");\n    let mut dec2 = AttachDecoder::new();\n    let (mut cur2, mut view2) = (0u64, Vec::new());\n\n    // Several seconds of live driving across many 50ms dispatcher polls —\n    // the window in which the stale replay lands (and must bounce off).\n    for round in 0..3u32 {\n        let marker = format!(\"CUR-{round}\\r\");\n        send_attach_input(&mut operator, s2, marker.as_bytes(), 20 + u64::from(round))\n            .expect(\"input s2\");\n        let eof = render_until(\n            &mut operator,\n            s2,\n            &mut dec2,\n            &mut cur2,\n            &mut view2,\n            format!(\"CUR-{round}\").as_bytes(),\n            Duration::from_secs(20),\n        );\n        assert!(\n            !eof,\n            \"round {round}: the replacement controller's stream saw an EOF — the \\\n             stale generation's worker cleared it (the fix-6 steal)\"\n        );\n        thread::sleep(Duration::from_millis(700));\n    }\n…\n}"}],"details":{"displayContent":{"text":"fn poisoned_subscriber_cancels_the_paired_forwarding_leg_then_recovery_serves() {\n    let _home = init_home();\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let (name_a, name_b) = (unique_name(), unique_name());\n    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\n    let mut probe = connect_retry(&name_a);\n\n    let opener = b\"{\\\"hello\\\":2}\\n\";\n    let (mut operator, _sid_b, sid_a) = open_traffic_stream(&name_a, &name_b, &mut probe, opener);\n\n    // The forwarding leg: an A-side conn tick-sending on the stream (ack'd, so\n    // a broker-side refusal surfaces as Err). Reports (ok_ticks, final_err).\n    let fw_name = name_a.clone();\n    let (fw_tx, fw_rx) = std::sync::mpsc::channel::<(u32, String)>();\n    let forwarder = thread::spawn(move || {\n        let mut fw = connect_retry(&fw_name);\n        let mut ok = 0u32;\n        for i in 0..600u64 {\n            match fw.net_stream_send(\n                sid_a,\n                b\"tick\\n\",\n                Some(MintedOp::new(Minter::Cli, 100 + i)),\n                false,\n            ) {\n                Ok(_) => ok += 1,\n                Err(e) => {\n                    let _ = fw_tx.send((ok, e.to_string()));\n                    return;\n                }\n            }\n            thread::sleep(Duration::from_millis(100));\n        }\n        let _ = fw_tx.send((ok, String::new()));\n    });\n\n    // Let the leg prove itself healthy, then wedge: fill the ring and attach\n    // the never-reading subscriber — replay wedge → poison → lease cancel.\n    thread::sleep(Duration::from_millis(500));\n    let chunk = vec![0x41u8; FLOOD_CHUNK];\n…\n    assert!(\n…\n        \"the leg served while the subscriber was healthy\"\n    );\n    assert!(\n        err.contains(\"lease canceled\"),\n        \"the leg must terminate on the LEASE refusal (fix 4), got: {err:?}\"\n    );\n\n    // Recovery: a new subscriber generation renews the lease; a fresh send\n    // serves again — one poison never permanently dead-ends the stream.\n    // Subscribe the new generation from the ring TAIL (empty replay): the\n    // recovery contract under test is the LEASE renewal, not the replay (T1\n    // proves that) — and a from-0 subscriber that never drains the 1.5MB\n    // flood would legitimately wedge and re-cancel, testing nothing.\n    let renewed = {\n        let end = Instant::now() + Duration::from_secs(20);\n        let mut probe_op = 900u64;\n        loop {\n            assert!(\n                Instant::now() < end,\n                \"the lease never renewed for a new generation\"\n            );\n            let tail = probe\n                .net_streams()\n                .expect(\"net-streams\")\n                .streams\n                .iter()\n                .find(|s| s.stream_id == sid_a)\n                .map(|s| s.next_seq)\n                .unwrap_or(0);\n            let mut f = connect_retry(&name_a);\n            f.net_stream_subscribe(sid_a, tail)\n                .expect(\"recovery subscribe\");\n            // Give the fire-and-forget subscribe a beat to process, then\n            // probe the lease with a real send on a separate conn.\n            thread::sleep(Duration::from_millis(300));\n            let mut s = connect_retry(&name_a);\n            let sent = s.net_stream_send(\n                sid_a,\n                b\"recovered\\n\",\n                Some(MintedOp::new(Minter::Cli, probe_op)),\n                false,\n            );\n            probe_op += 1;\n            match sent {\n                Ok(_) => break true,\n                Err(e)\n                    if e.to_string().contains(\"lease canceled\")\n                    || e.to_string().contains(\"subscriber busy\") =>\n                {\n                    thread::sleep(Duration::from_millis(300));\n                }\n                Err(e) => panic!(\"unexpected recovery send failure: {e}\"),\n            }\n        }\n    };\n    assert!(renewed);\n    drop(wedged);\n}\n…\nfn stale_unfinished_attach_replay_neither_takes_nor_clears_the_replacement_controller() {\n    let _home = init_home();\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let (name_a, name_b) = (unique_name(), unique_name());\n    let _broker_a = net_broker(&name_a, &dir.path().join(\"a\"));\n    let _broker_b = net_broker(&name_b, &dir.path().join(\"b\"));\n\n    let mut probe = connect_retry(&name_a);\n    let sid = probe\n        .spawn_session(echo_spawn_req(\"rds6-ep\"))\n        .expect(\"spawn echo child\");\n    let a_addr = probe.net_status().expect(\"a status\").addr;\n\n    let gen1 = Generation::spawn(&name_a, dir.path().join(\"gen1.log\"), &[]);\n\n    let mut operator = connect_retry(&name_b);\n    let conn = operator\n        .net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1)))\n        .expect(\"dial\");\n\n    // s1: attached + driven under gen1 — opener pinned, Request + input in\n    // the ring, everything a stale replay needs to steal.\n    let s1 = request_attach(\n        &mut operator,\n        conn.conn_id,\n        sid,\n        0,\n        MintedOp::new(Minter::Rc, 2),\n        AttachIntent::Control,\n    )\n    .expect(\"request s1\");\n    operator.net_stream_subscribe(s1, 0).expect(\"subscribe s1\");\n    send_attach_input(&mut operator, s1, b\"S1-MARK\\r\", 10).expect(\"input s1\");\n    let mut dec1 = AttachDecoder::new();\n    let (mut cur1, mut view1) = (0u64, Vec::new());\n    render_until(\n        &mut operator,\n        s1,\n        &mut dec1,\n        &mut cur1,\n        &mut view1,\n        b\"S1-MARK\",\n        Duration::from_secs(20),\n    );\n    let s1_a = *peer_stream_ids(&mut probe).first().expect(\"s1 row on A\");\n\n    // The raw-close: kill gen1 mid-serve. NO detach follows — s1 stays\n    // UNFINISHED (and unretired) on a live conn: invisible to the\n    // finished-row discriminator, eligible for redispatch.\n    gen1.kill();\n    {\n        // Sanity: the row is genuinely unfinished (the T6 precondition that\n        // separates this from D1b's finished-unretired shape).\n        let rows = probe.net_streams().expect(\"net-streams\").streams;\n        let row = rows.iter().find(|s| s.stream_id == s1_a).expect(\"s1 row\");\n        assert!(\n            !row.finished,\n            \"T6 precondition: the stale row must be UNFINISHED\"\n        );\n    }\n\n    // The REPLACEMENT: same operator identity, same session, NEWER attach\n    // generation (client-minted wall-ms). gen2 replays s1 (unfinished →\n    // served) AND serves s2 — the stale Request's older generation must\n    // refuse against the newer controller whichever order the workers land.\n    let gen2 = Generation::spawn(&name_a, dir.path().join(\"gen2.log\"), &[]);\n    let s2 = request_attach(\n        &mut operator,\n        conn.conn_id,\n        sid,\n        0,\n        MintedOp::new(Minter::Rc, 3),\n        AttachIntent::Control,\n    )\n    .expect(\"request s2\");\n    operator.net_stream_subscribe(s2, 0).expect(\"subscribe s2\");\n    let mut dec2 = AttachDecoder::new();\n    let (mut cur2, mut view2) = (0u64, Vec::new());\n\n    // Several seconds of live driving across many 50ms dispatcher polls —\n    // the window in which the stale replay lands (and must bounce off).\n    for round in 0..3u32 {\n        let marker = format!(\"CUR-{round}\\r\");\n        send_attach_input(&mut operator, s2, marker.as_bytes(), 20 + u64::from(round))\n            .expect(\"input s2\");\n        let eof = render_until(\n            &mut operator,\n            s2,\n            &mut dec2,\n            &mut cur2,\n            &mut view2,\n            format!(\"CUR-{round}\").as_bytes(),\n            Duration::from_secs(20),\n        );\n        assert!(\n            !eof,\n            \"round {round}: the replacement controller's stream saw an EOF — the \\\n             stale generation's worker cleared it (the fix-6 steal)\"\n        );\n        thread::sleep(Duration::from_millis(700));\n    }\n…\n}","startLine":415,"lineNumbers":[415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,null,476,null,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,null,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,null,797]},"fileSize":36184,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon\\tests\\redispatch_stall.rs"}}},"isError":false,"timestamp":1789528567439}}
{"type":"message","id":"802d2afe","parentId":"a9ad2815","timestamp":"2026-09-16T03:16:07.505Z","message":{"role":"toolResult","toolCallId":"call_BPgyqW28rEdANgqMfx45RD4m|fc_0cf46cef591a107c016aaa09f7574c87d296b0ff9b592bf956","toolName":"read","content":[{"type":"text","text":"[[requirements]]\nid = \"REQ-UPDATE-PROMOTE-DRAINED\"\ntitle = \"W3 (LIFECYCLE-TRUTH, mechanic-d MOVED FROM W2 per doyle gate verdict @e5ae7a9 — binding): the update-apply brain-generation promotion completes only when the OLD generation's broker subscriber connection is CLOSED or stall-EVICTED — never while blocked writes still pend on it. ROOT: `brain.ready` != subscribers drained; W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-promote window to BRAIN_WRITE_DEADLINE (15s), it does NOT close it — a new brain can signal ready inside that window while the old gen's conn is still wedged, so the apply 'promotes' onto a still-frozen control plane (the 22:47 incident-night false-promote). FIX: the promotion gate (ADR-0018 brain-trial, brainproc.rs) adds an explicit DRAINED preconditio…\nrequired_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED LIFECYCLE-TRUTH W3 drained-condition slice (todlando 2026-07-07, work-start); brainproc.rs run_trial DRAINED gate + broker any_local_controller_wedged truth + false-promote int rig — closes the W2 15s residual (KH 7.36)\n\n[[requirements]]\nid = \"REQ-UPDATE-TRIAL-DRAIN-DRIVE\"\ntitle = \"UPDATE-WEDGE (counter-54, doyle-ruled 2026-07-09 — regression of the v0.29.0 seamless brain-swap): a brain generation DRIVES the broker's controller-liveness reap (a KIND_SESSIONS poll) each heartbeat throughout its boot/trial loop, so a hard-KILLED prior generation's black-holed LOCAL controller conn (by:None) is stall-evicted within the trial window and can never permanently strand the promotion DRAINED gate. ROOT (2026-07-09 field freeze, `spt update fetch --apply` v0.30.0->v0.30.2 froze all 7 live PTYs ~30s then rolled back): the promote gate (run_trial, brainproc.rs:657-661) needs BOTH `ready_generation==gen` AND `old_gen_drained()`; `old_gen_drained()` = `!any_local_controller_wedged()` (brainproc.rs:534) is a PURE READ of `write_blocked_since…\nrequired_stages = [\"doc\", \"impl\", \"int\"]  # ACTIVATED counter-54 UPDATE-WEDGE fix (doyle ruled brain-side 2026-07-09; todlando builds). doc = a CONTEXT.md/LIFECYCLE note that the trial/boot brain drives controller-reap so a killed prior generation cannot strand old_gen_drained. impl = the brain.sessions() KIND_SESSIONS drive in run_brain's heartbeat loop (brainproc.rs, first-tick + every tick, trial + recovery brain). UNIT DELIBERATELY DROPPED (doyle FORK-2 ruling 2026-07-09): the impl is ONE unconditional branchless brain.sessions() call in the heartbeat loop — no pure branch/decision to unit-test; forcing a call-ordering unit would require a trait-seam refactor of the broker-critical brain heartbeat path purely to satisfy the stage (test theater, adds risk…\n\n[[requirements]]\nid = \"REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE\"\ntitle = \"SEED (DEFERRED, doyle 2026-07-09 — post-counter-54 root-hardening for UPDATE-WEDGE; mint now, impl a FUTURE milestone): on a PLANNED brain-restart (`BRAIN_UPDATE_RESTART`, the seamless update-apply brain-cycle), the outgoing brain's LOCAL (by:None) controller conns are GRACEFULLY CLEAN-CLOSED as the brain is cycled, instead of hard-killed and left to black-hole. ROOT (field-pinned 2026-07-09, daemon.stderr.log L24277-24303): the update-restart path hard-kills the outgoing brain (`child.kill()`, brainproc.rs:851); its live-agent controller conns then block on dead pipes (never EOF) → the broker reads them WEDGED (broker.rs:2695-2700) → the new candidate's promotion DRAINED gate (`any_local_controller_wedged`, broker.rs:2704) stays true until the W2 s…\nrequired_stages = []  # SEED minted DEFERRED (doyle 2026-07-09); activate post-counter-54 at the milestone that delivers HITCH-FREE seamless updates. Supersedes the livehost-reattach 'Fix Y' framing (wrong site). The counter-54 fix (REQ-UPDATE-TRIAL-DRAIN-DRIVE reap-drive) makes updates SELF-HEAL in ~15s; this seed removes the residual hitch.\n\n[[requirements]]\nid = \"REQ-LIVEHOST-RECONCILE-TRIAL-SILENT\"\ntitle = \"SEED (DEFERRED investigation, doyle 2026-07-09 — UPDATE-WEDGE follow-up): determine WHY the trial/rollback brain's livehost reconcile loop did NOT drive the broker controller-reap (nor re-host the live agents) during the ~30s field update-trial window, when livehost polls `query_live_session_endpoints()` → `brain.sessions()` (KIND_SESSIONS) UNCONDITIONALLY every `LIVE_RECONCILE_INTERVAL_MS`=5000ms (livehost.rs:1026). CONTEXT (surfaced building the counter-54 rig): livehost's 5s KIND_SESSIONS poll drives the SAME broker `reap_dead_controller` sweep the fix drives — so it would otherwise reap the 15s-matured wedge by ~T20 < the 30s trial and SELF-HEAL. It didn't (field froze 30s → rollback), so the field trial-brain livehost was silent/delayed (PIN Q2…\nrequired_stages = []  # SEED deferred investigation (doyle 2026-07-09); non-blocking for counter-54 (the core-loop reap-drive in REQ-UPDATE-TRIAL-DRAIN-DRIVE covers the wedge independently of livehost). Activate once the livehost-silence mechanism is pinned.\n\n[[requirements]]\nid = \"REQ-BRAIN-RESUME-NO-CONTROL-STEAL\"\ntitle = \"UPDATE-WEDGE round 2 (v0.30.4, doyle-ruled 2026-07-09 — field incident on the counter-54 fetch--apply): a brain-respawn must NEVER steal, then stall-evict, the controller of a broker PTY session the daemon brain does not DRIVE. ROOT (field-pinned + SME, docs/UPDATE-WEDGE-2-RCA.md + docs/UPDATE-WEDGE-2-SME-todlando.md): `resume_sessions` (brain.rs:985) re-attaches EVERY session `KIND_SESSIONS` returns via `subscribe` = `subscribe_with(AttachIntent::Control, by:None)` (brain.rs:1450-1455). The docstring's 'a None identity never displaces (falls back to viewer)' is a MYTH for FREE / SAME-LOCAL-IDENTITY slots: `resolve_subscribe` (broker.rs:1134-1153) stall-evicts FIRST, then `become_controller` if the slot is now free OR the incumbent is also local (No…\nrequired_stages = [\"doc\", \"impl\", \"int\"]  # ACTIVATED v0.30.4 UPDATE-WEDGE-2 fix (doyle ruled 2026-07-09; todlando builds). doc = brain.rs resume docstring corrected (kill the by:None-never-steals MYTH) + a CONTEXT/LIFECYCLE note that a brain-respawn re-attaches non-driven sessions as viewer. impl = resume path re-attaches AttachIntent::Viewer (Control only for genuinely-driven sessions, empty today). unit DROPPED if the impl is a branchless intent-flip (same FORK-2 reasoning as REQ-UPDATE-TRIAL-DRAIN-DRIVE — add a unit only if a real driven-vs-not predicate branch exists). int = the multi-broker-PTY-session respawn rig, all 3 assertions RED-first.\n\n…\n[[requirements]]\nid = \"REQ-DAEMON-REFRESH\"\ntitle = \"THE-FORKENING W4 (operator add 2026-07-14): `spt daemon refresh` — restart the daemon BRAIN without a binary swap and WITHOUT touching the broker: exactly the apply_staged brain-cycle path (brain stop -> respawn -> readiness trial -> promote, incl. the trial-drain drive REQ-UPDATE-TRIAL-DRAIN-DRIVE and viewer-only resume REQ-BRAIN-RESUME-NO-CONTROL-STEAL) minus the swap. Recovery verb for wedged brain-held state (field motivator 2026-07-14: endpoint bringup broken on a live daemon + deployah down — today's only remedy is a full daemon bounce that kills every PTY). Broker + PTYs survive by construction (handoff invariant). Failure = the existing trial rollback semantics (old brain resumes; refresh reports loud). Gate: unit — verb routes the brain-cyc…\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # FLIPPED in the W4 build commit (todlando 2026-07-15).\n\n…\n[[requirements]]\n…\n\n[[requirements]]\nid = \"REQ-DISPATCH-FALLBACK-CIRCUIT\"\ntitle = \"REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 1+5): the opener ring-peek fallback fires ONLY on the explicit UnsupportedVerb/old-broker answer — transport timeout/EOF/poison classify Failed and requeue bounded, NEVER a second replay subscriber (today first_line's Err(_) arm at dispatch.rs:414 catches everything; the comment intends old-broker-only). Retries are CLASSIFIED: pre-setup transient may retry; a deadline-poisoned replay is CIRCUIT-BROKEN (global backoff) and a replacement subscriber is never installed until the prior subscriber is fully gone. NOT a revert to the v0.33 burn-the-claim abandonment (rejected): the stream must recover after the breaker window — show it in test. Gate: impl — narrowed fallback arm + breaker; unit — timeout/EOF/p…\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = unsupported-verb-only fallback arm + classified retries + circuit breaker (no reinstall until prior gone); unit = error-class routing + breaker trip/reset/no-reinstall; int = T2 + T5 recovery + T7 real-traffic N-1; doc = ADR-0038 Amendment.\n\n[[requirements]]\nid = \"REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE\"\ntitle = \"REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 2+3+4): StreamLog subscriber write discipline — (a) replay and live fan-out HALT at the first failed subscriber write and the failed subscriber is REMOVED, at ALL sites (attach replay, append, finish; PresenceLog mirrors ride the same change — today let _ = sub.write() discards errors and iteration continues, nethost.rs:322-346); (b) subscriber I/O moves OFF the StreamLog mutex: bounded per-subscriber writer queue, enqueue-under-lock / I/O-outside, overflow = detach + resume-from-cursor (the existing gapless-resume contract), never a producer stall; (c) attach worker completion + forwarding BOUNDED and cancelable — a poisoned brain subscriber cancels its paired serve/wire worker, no orphan forwarding le…\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = halt+remove all sites (attach/append/finish + PresenceLog) + per-subscriber writer queue off the mutex (overflow=detach+resume-from-cursor) + cancelable serve/wire pairing; unit = per-site halt/remove + overflow-detach cursor + append-never-blocks; int = T1 + T3 + T4; doc = ADR-0038 Amendment.\n\n[[requirements]]\nid = \"REQ-SERVE-OWNERSHIP-GENERATION\"\ntitle = \"REDISPATCH-STALL W1 (ADR-0038 Amendment, fix 6): terminal-exclusion enforced PRE-SERVE + ownership/generation validation on attach/detach — a stale worker can never detach or displace a REPLACEMENT controller (today detach_if compares Arc ptr identity only; the serve path re-checks nothing at completion). Covers the UNFINISHED-stale-row control-steal shape (raw-close no-FIN viewports, emphasys C2 leak class feeding it) that finished-row retirement (D1/D1b) definitionally cannot see — the discriminating field observable on the next live steal catch = the stolen row's finished+retired flags. Gate: impl — pre-serve terminal exclusion + generation/ownership tokens on attach/detach; unit — stale-generation detach refused while the same-generation detach …\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = pre-serve terminal exclusion + attach/detach generation tokens; unit = stale-generation detach refused, same-generation lands; int = T6 unfinished-stale steal; doc = ADR-0038 Amendment.\n\n\n[Some lines truncated to 768 chars]"}],"details":{"displayContent":{"text":"[[requirements]]\nid = \"REQ-UPDATE-PROMOTE-DRAINED\"\ntitle = \"W3 (LIFECYCLE-TRUTH, mechanic-d MOVED FROM W2 per doyle gate verdict @e5ae7a9 — binding): the update-apply brain-generation promotion completes only when the OLD generation's broker subscriber connection is CLOSED or stall-EVICTED — never while blocked writes still pend on it. ROOT: `brain.ready` != subscribers drained; W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-promote window to BRAIN_WRITE_DEADLINE (15s), it does NOT close it — a new brain can signal ready inside that window while the old gen's conn is still wedged, so the apply 'promotes' onto a still-frozen control plane (the 22:47 incident-night false-promote). FIX: the promotion gate (ADR-0018 brain-trial, brainproc.rs) adds an explicit DRAINED preconditio…\nrequired_stages = [\"impl\", \"unit\", \"int\"]  # ACTIVATED LIFECYCLE-TRUTH W3 drained-condition slice (todlando 2026-07-07, work-start); brainproc.rs run_trial DRAINED gate + broker any_local_controller_wedged truth + false-promote int rig — closes the W2 15s residual (KH 7.36)\n\n[[requirements]]\nid = \"REQ-UPDATE-TRIAL-DRAIN-DRIVE\"\ntitle = \"UPDATE-WEDGE (counter-54, doyle-ruled 2026-07-09 — regression of the v0.29.0 seamless brain-swap): a brain generation DRIVES the broker's controller-liveness reap (a KIND_SESSIONS poll) each heartbeat throughout its boot/trial loop, so a hard-KILLED prior generation's black-holed LOCAL controller conn (by:None) is stall-evicted within the trial window and can never permanently strand the promotion DRAINED gate. ROOT (2026-07-09 field freeze, `spt update fetch --apply` v0.30.0->v0.30.2 froze all 7 live PTYs ~30s then rolled back): the promote gate (run_trial, brainproc.rs:657-661) needs BOTH `ready_generation==gen` AND `old_gen_drained()`; `old_gen_drained()` = `!any_local_controller_wedged()` (brainproc.rs:534) is a PURE READ of `write_blocked_since…\nrequired_stages = [\"doc\", \"impl\", \"int\"]  # ACTIVATED counter-54 UPDATE-WEDGE fix (doyle ruled brain-side 2026-07-09; todlando builds). doc = a CONTEXT.md/LIFECYCLE note that the trial/boot brain drives controller-reap so a killed prior generation cannot strand old_gen_drained. impl = the brain.sessions() KIND_SESSIONS drive in run_brain's heartbeat loop (brainproc.rs, first-tick + every tick, trial + recovery brain). UNIT DELIBERATELY DROPPED (doyle FORK-2 ruling 2026-07-09): the impl is ONE unconditional branchless brain.sessions() call in the heartbeat loop — no pure branch/decision to unit-test; forcing a call-ordering unit would require a trait-seam refactor of the broker-critical brain heartbeat path purely to satisfy the stage (test theater, adds risk…\n\n[[requirements]]\nid = \"REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE\"\ntitle = \"SEED (DEFERRED, doyle 2026-07-09 — post-counter-54 root-hardening for UPDATE-WEDGE; mint now, impl a FUTURE milestone): on a PLANNED brain-restart (`BRAIN_UPDATE_RESTART`, the seamless update-apply brain-cycle), the outgoing brain's LOCAL (by:None) controller conns are GRACEFULLY CLEAN-CLOSED as the brain is cycled, instead of hard-killed and left to black-hole. ROOT (field-pinned 2026-07-09, daemon.stderr.log L24277-24303): the update-restart path hard-kills the outgoing brain (`child.kill()`, brainproc.rs:851); its live-agent controller conns then block on dead pipes (never EOF) → the broker reads them WEDGED (broker.rs:2695-2700) → the new candidate's promotion DRAINED gate (`any_local_controller_wedged`, broker.rs:2704) stays true until the W2 s…\nrequired_stages = []  # SEED minted DEFERRED (doyle 2026-07-09); activate post-counter-54 at the milestone that delivers HITCH-FREE seamless updates. Supersedes the livehost-reattach 'Fix Y' framing (wrong site). The counter-54 fix (REQ-UPDATE-TRIAL-DRAIN-DRIVE reap-drive) makes updates SELF-HEAL in ~15s; this seed removes the residual hitch.\n\n[[requirements]]\nid = \"REQ-LIVEHOST-RECONCILE-TRIAL-SILENT\"\ntitle = \"SEED (DEFERRED investigation, doyle 2026-07-09 — UPDATE-WEDGE follow-up): determine WHY the trial/rollback brain's livehost reconcile loop did NOT drive the broker controller-reap (nor re-host the live agents) during the ~30s field update-trial window, when livehost polls `query_live_session_endpoints()` → `brain.sessions()` (KIND_SESSIONS) UNCONDITIONALLY every `LIVE_RECONCILE_INTERVAL_MS`=5000ms (livehost.rs:1026). CONTEXT (surfaced building the counter-54 rig): livehost's 5s KIND_SESSIONS poll drives the SAME broker `reap_dead_controller` sweep the fix drives — so it would otherwise reap the 15s-matured wedge by ~T20 < the 30s trial and SELF-HEAL. It didn't (field froze 30s → rollback), so the field trial-brain livehost was silent/delayed (PIN Q2…\nrequired_stages = []  # SEED deferred investigation (doyle 2026-07-09); non-blocking for counter-54 (the core-loop reap-drive in REQ-UPDATE-TRIAL-DRAIN-DRIVE covers the wedge independently of livehost). Activate once the livehost-silence mechanism is pinned.\n\n[[requirements]]\nid = \"REQ-BRAIN-RESUME-NO-CONTROL-STEAL\"\ntitle = \"UPDATE-WEDGE round 2 (v0.30.4, doyle-ruled 2026-07-09 — field incident on the counter-54 fetch--apply): a brain-respawn must NEVER steal, then stall-evict, the controller of a broker PTY session the daemon brain does not DRIVE. ROOT (field-pinned + SME, docs/UPDATE-WEDGE-2-RCA.md + docs/UPDATE-WEDGE-2-SME-todlando.md): `resume_sessions` (brain.rs:985) re-attaches EVERY session `KIND_SESSIONS` returns via `subscribe` = `subscribe_with(AttachIntent::Control, by:None)` (brain.rs:1450-1455). The docstring's 'a None identity never displaces (falls back to viewer)' is a MYTH for FREE / SAME-LOCAL-IDENTITY slots: `resolve_subscribe` (broker.rs:1134-1153) stall-evicts FIRST, then `become_controller` if the slot is now free OR the incumbent is also local (No…\nrequired_stages = [\"doc\", \"impl\", \"int\"]  # ACTIVATED v0.30.4 UPDATE-WEDGE-2 fix (doyle ruled 2026-07-09; todlando builds). doc = brain.rs resume docstring corrected (kill the by:None-never-steals MYTH) + a CONTEXT/LIFECYCLE note that a brain-respawn re-attaches non-driven sessions as viewer. impl = resume path re-attaches AttachIntent::Viewer (Control only for genuinely-driven sessions, empty today). unit DROPPED if the impl is a branchless intent-flip (same FORK-2 reasoning as REQ-UPDATE-TRIAL-DRAIN-DRIVE — add a unit only if a real driven-vs-not predicate branch exists). int = the multi-broker-PTY-session respawn rig, all 3 assertions RED-first.\n\n…\n[[requirements]]\nid = \"REQ-DAEMON-REFRESH\"\ntitle = \"THE-FORKENING W4 (operator add 2026-07-14): `spt daemon refresh` — restart the daemon BRAIN without a binary swap and WITHOUT touching the broker: exactly the apply_staged brain-cycle path (brain stop -> respawn -> readiness trial -> promote, incl. the trial-drain drive REQ-UPDATE-TRIAL-DRAIN-DRIVE and viewer-only resume REQ-BRAIN-RESUME-NO-CONTROL-STEAL) minus the swap. Recovery verb for wedged brain-held state (field motivator 2026-07-14: endpoint bringup broken on a live daemon + deployah down — today's only remedy is a full daemon bounce that kills every PTY). Broker + PTYs survive by construction (handoff invariant). Failure = the existing trial rollback semantics (old brain resumes; refresh reports loud). Gate: unit — verb routes the brain-cyc…\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # FLIPPED in the W4 build commit (todlando 2026-07-15).\n\n…\n[[requirements]]\n…\n\n[[requirements]]\nid = \"REQ-DISPATCH-FALLBACK-CIRCUIT\"\ntitle = \"REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 1+5): the opener ring-peek fallback fires ONLY on the explicit UnsupportedVerb/old-broker answer — transport timeout/EOF/poison classify Failed and requeue bounded, NEVER a second replay subscriber (today first_line's Err(_) arm at dispatch.rs:414 catches everything; the comment intends old-broker-only). Retries are CLASSIFIED: pre-setup transient may retry; a deadline-poisoned replay is CIRCUIT-BROKEN (global backoff) and a replacement subscriber is never installed until the prior subscriber is fully gone. NOT a revert to the v0.33 burn-the-claim abandonment (rejected): the stream must recover after the breaker window — show it in test. Gate: impl — narrowed fallback arm + breaker; unit — timeout/EOF/p…\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = unsupported-verb-only fallback arm + classified retries + circuit breaker (no reinstall until prior gone); unit = error-class routing + breaker trip/reset/no-reinstall; int = T2 + T5 recovery + T7 real-traffic N-1; doc = ADR-0038 Amendment.\n\n[[requirements]]\nid = \"REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE\"\ntitle = \"REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 2+3+4): StreamLog subscriber write discipline — (a) replay and live fan-out HALT at the first failed subscriber write and the failed subscriber is REMOVED, at ALL sites (attach replay, append, finish; PresenceLog mirrors ride the same change — today let _ = sub.write() discards errors and iteration continues, nethost.rs:322-346); (b) subscriber I/O moves OFF the StreamLog mutex: bounded per-subscriber writer queue, enqueue-under-lock / I/O-outside, overflow = detach + resume-from-cursor (the existing gapless-resume contract), never a producer stall; (c) attach worker completion + forwarding BOUNDED and cancelable — a poisoned brain subscriber cancels its paired serve/wire worker, no orphan forwarding le…\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = halt+remove all sites (attach/append/finish + PresenceLog) + per-subscriber writer queue off the mutex (overflow=detach+resume-from-cursor) + cancelable serve/wire pairing; unit = per-site halt/remove + overflow-detach cursor + append-never-blocks; int = T1 + T3 + T4; doc = ADR-0038 Amendment.\n\n[[requirements]]\nid = \"REQ-SERVE-OWNERSHIP-GENERATION\"\ntitle = \"REDISPATCH-STALL W1 (ADR-0038 Amendment, fix 6): terminal-exclusion enforced PRE-SERVE + ownership/generation validation on attach/detach — a stale worker can never detach or displace a REPLACEMENT controller (today detach_if compares Arc ptr identity only; the serve path re-checks nothing at completion). Covers the UNFINISHED-stale-row control-steal shape (raw-close no-FIN viewports, emphasys C2 leak class feeding it) that finished-row retirement (D1/D1b) definitionally cannot see — the discriminating field observable on the next live steal catch = the stolen row's finished+retired flags. Gate: impl — pre-serve terminal exclusion + generation/ownership tokens on attach/detach; unit — stale-generation detach refused while the same-generation detach …\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = pre-serve terminal exclusion + attach/detach generation tokens; unit = stale-generation detach refused, same-generation lands; int = T6 unfinished-stale steal; doc = ADR-0038 Amendment.\n","startLine":2375,"lineNumbers":[2375,2376,2377,2378,2379,2380,2381,2382,2383,2384,2385,2386,2387,2388,2389,2390,2391,2392,2393,2394,2395,2396,2397,2398,2399,null,2649,2650,2651,2652,2653,null,2776,null,2780,2781,2782,2783,2784,2785,2786,2787,2788,2789,2790,2791,2792,2793,2794,2795]},"fileSize":2076989,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\traceable-reqs.toml"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1789528567504}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_lPYYsjMRwdd4lmTLg1skxeCb|fc_0cf46cef591a107c016aaa0a0c6df087d2b07c4604f9c52462","toolName":"grep","startedAt":"2026-09-16T03:16:28.581Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/support/refresh_freeze/rig.py"},"intent":"Locating rig predicates and cadence"},"id":"b9694a3c","parentId":"802d2afe","timestamp":"2026-09-16T03:16:28.581Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_yCIZixkWKoRL00hrxaVWcTbd|fc_0cf46cef591a107c016aaa0a0c6e0487d2a2de766294b7cf0c","toolName":"grep","startedAt":"2026-09-16T03:16:28.582Z","args":{"path":".worktrees/49-267-refresh-discriminator/docs/CONTEXT.md;.worktrees/49-267-refresh-discriminator/docs/LIFECYCLE.md;.worktrees/49-267-refresh-discriminator/docs/49-267-REFRESH-OBSERVATION-PLAN.md"},"intent":"Locating refresh documentation sections"},"id":"379bcde7","parentId":"b9694a3c","timestamp":"2026-09-16T03:16:28.582Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Aifypti4nZhhAs3unI5xD8VQ|fc_0cf46cef591a107c016aaa0a0c6e1087d2977153a5eec17c35","toolName":"read","startedAt":"2026-09-16T03:16:28.582Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs:2850-2872,13305-13345"},"intent":"Reading controller exclusion predicate"},"id":"47b4e2a9","parentId":"379bcde7","timestamp":"2026-09-16T03:16:28.582Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_CLPf7wLkTzK5ZHHdJat2X4af|fc_0cf46cef591a107c016aaa0a0c6e1887d281334cc8c0abdf25","toolName":"grep","startedAt":"2026-09-16T03:16:28.582Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/msg.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/s…"},"intent":"Locating existing ack unit seams"},"id":"0126af88","parentId":"47b4e2a9","timestamp":"2026-09-16T03:16:28.582Z"}
{"type":"message","id":"d2adc4d3","parentId":"0126af88","timestamp":"2026-09-16T03:16:28.583Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":""},{"type":"toolCall","id":"call_lPYYsjMRwdd4lmTLg1skxeCb|fc_0cf46cef591a107c016aaa0a0c6df087d2b07c4604f9c52462","name":"grep","arguments":{"i":"Locating rig predicates and cadence","pattern":"baseline|refresh|ack_bound|recovery_bound|poison|detach|sleep\\(|input","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/support/refresh_freeze/rig.py","case":false,"gitignore":false,"skip":null},"intent":"Locating rig predicates and cadence"},{"type":"toolCall","id":"call_yCIZixkWKoRL00hrxaVWcTbd|fc_0cf46cef591a107c016aaa0a0c6e0487d2a2de766294b7cf0c","name":"grep","arguments":{"i":"Locating refresh documentation sections","pattern":"REQ-DAEMON-REFRESH|daemon refresh|old_gen_drained|REQ-UPDATE-TRIAL-DRAIN-DRIVE","path":".worktrees/49-267-refresh-discriminator/docs/CONTEXT.md;.worktrees/49-267-refresh-discriminator/docs/LIFECYCLE.md;.worktrees/49-267-refresh-discriminator/docs/49-267-REFRESH-OBSERVATION-PLAN.md","case":true,"gitignore":false,"skip":null},"intent":"Locating refresh documentation sections"},{"type":"toolCall","id":"call_Aifypti4nZhhAs3unI5xD8VQ|fc_0cf46cef591a107c016aaa0a0c6e1087d2977153a5eec17c35","name":"read","arguments":{"i":"Reading controller exclusion predicate","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs:2850-2872,13305-13345"},"intent":"Reading controller exclusion predicate"},{"type":"toolCall","id":"call_CLPf7wLkTzK5ZHHdJat2X4af|fc_0cf46cef591a107c016aaa0a0c6e1887d281334cc8c0abdf25","name":"grep","arguments":{"i":"Locating existing ack unit seams","pattern":"fn .*ack|fn .*dedup|REQ-HAZARD-INPUT-ACK-BACKPRESSURE","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/msg.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/brain.rs","case":true,"gitignore":false,"skip":null},"intent":"Locating existing ack unit seams"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":11177,"output":408,"cacheRead":66432,"cacheWrite":0,"totalTokens":78017,"reasoningTokens":13,"cost":{"input":0.11177000000000001,"output":0.0204,"cacheRead":0.06643199999999999,"cacheWrite":0,"total":0.198602}},"stopReason":"toolUse","timestamp":1789528567568,"responseId":"resp_0cf46cef591a107c016aaa09f7c89887d28d836bdd22b702aa","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cf46cef591a107c016aaa09f9377c87d28bc16138d838114f","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqgoMnQBpoeYc4GE62mRHn9-ZAVFz6i01W9KzTreXtBTdXXra33sWEd0Gu5OwOztyaEbPNx3etfmWtTcLKR-xrQHlPS_lPH9uj_ebOSpMp8HQxxcdEhxOXr7kHdgiS81i-erp1WGkJtPen2-JXjwuOD-x2Ngr1jUIdeBLcTsuV4CGwg0yaEeco0Qy1EVajrB71vTaH6kdJ8bs8Cof-qvEEbc9IJqOk3bEH9HdBaWTI_8_YzFrYHau7x7MBA4QxUei-V_fuD0Q-mW5_rd6zF7Y5xlLf0ACbqv6Uz_DsUIGzfUMuJoOS9w4BPw6D14xa1dBS62Syq-zPgiGggJ7oKPOhsjsba0wLJvRwbpBOMuWk5k_0S1QBlIzj1KCtn7-ijjpIKFzEIM4MU3ujGRGeZWECxP_Iaw7NPMZVRzWO5GVLLr-SxdjnvO9PH4m7zo-RGT6E2evUNUACWiVELcFvOA-_37qthPrKGrBKupVtI2nHq2-M8FRXhNTSyUA_2KU83mXC4uh8T1iDElYe5YYkldhpnGt3Mg7KECsw9GgxtB71Xdg9CNrqxX1z0LuhPjcV9vewRHQ0cPyh1KAyFv5iAikvR3_4eYe23YtxaJ8Se5i_KWL9ThCy-Kkw43fLuEb5Nqu5a8BlSOArCNViZxfE4M_oiKtdoZfCu2dNUwuTCft_wvIwpsYc7Ac3aNgWgL5R8byPIM7gUZqYcKdXA_aTHcp1ZR99ZACgrlGYsuozR_Fc2QUj11Eq5RGL4Wxdt1RK1HPH8Xaq0vKrIpnbW9Fjh5kY5s8k2UAw-ddRQoVAdkooEjGUizhD0C1YqYHgo5Sd75sEr-K9rl2czJngJH6vlQYWnTaIYJqt_H3M5PIDyel98Ddt8BhuNSafcFeQ7LpheVHLY-DZg5hq1z9rAvJiOipQmiMdqUVUmePaqwihBGq9VE30KJeHiC1gF3M0dY2z3Re8-rmeoPkpurQssSj2RHVwsKXwtMpir70GEWf9BmNFHmLwmsFNl23VgATP3IwXoMejwgOBXWJvsvI1Tp7wCNABzQDEsk3tZXc4fRDRWhz7VOuNQbA1j3hQtsiFpFEASNDHE8Q7IzUlLzGGE0PVS9R105e6L6uibt8A8ygvY1Pw6-kGSnp7xo0z0E9IujJEXOaS-e6IV7QKq9ienQGeDYQwSK3VhHryYXR2ZpA6MddmqZcHQfb6fLrqidA0BqpXlqBOWI80gh48JTt9pXfLUcM81cP4osLKi5YfUtisApcsMMyD4fHef-KlJtBcCRfdnC7YYcsWOCTjjW5X1oC4XMuQFSrnzlX3mmLdXan1niN8ZeAZ2S0oS2MJ2jqF_VJvZXP8vBat6jdUT5vogowdT9Ngqp07g==","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"summary":[],"metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa0a0c6df087d2b07c4604f9c52462","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating rig predicates and cadence\",\"pattern\":\"baseline|refresh|ack_bound|recovery_bound|poison|detach|sleep\\\\(|input\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/support/refresh_freeze/rig.py\",\"case\":false,\"gitignore\":false,\"skip\":null}","call_id":"call_lPYYsjMRwdd4lmTLg1skxeCb","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"grep","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa0a0c6e0487d2a2de766294b7cf0c","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating refresh documentation sections\",\"pattern\":\"REQ-DAEMON-REFRESH|daemon refresh|old_gen_drained|REQ-UPDATE-TRIAL-DRAIN-DRIVE\",\"path\":\".worktrees/49-267-refresh-discriminator/docs/CONTEXT.md;.worktrees/49-267-refresh-discriminator/docs/LIFECYCLE.md;.worktrees/49-267-refresh-discriminator/docs/49-267-REFRESH-OBSERVATION-PLAN.md\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_yCIZixkWKoRL00hrxaVWcTbd","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"grep","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa0a0c6e1087d2977153a5eec17c35","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading controller exclusion predicate\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs:2850-2872,13305-13345\"}","call_id":"call_Aifypti4nZhhAs3unI5xD8VQ","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"read","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa0a0c6e1887d281334cc8c0abdf25","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating existing ack unit seams\",\"pattern\":\"fn .*ack|fn .*dedup|REQ-HAZARD-INPUT-ACK-BACKPRESSURE\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/msg.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/brain.rs\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_CLPf7wLkTzK5ZHHdJat2X4af","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"grep","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}}]},"duration":21007.634200000204,"ttft":1693.315700000152,"completedAt":1789528588578,"contextSnapshot":{"promptTokens":77609,"nonMessageTokens":10304,"compactionEpoch":0}}}
{"type":"message","id":"dcff4d11","parentId":"d2adc4d3","timestamp":"2026-09-16T03:16:28.597Z","message":{"role":"toolResult","toolCallId":"call_yCIZixkWKoRL00hrxaVWcTbd|fc_0cf46cef591a107c016aaa0a0c6e0487d2a2de766294b7cf0c","toolName":"grep","content":[{"type":"text","text":" 53|\n*54|**Before `daemon refresh` and before `daemon stop`, all four checks must pass. Any failure and the\n 55|command does not run:**\n 56|\n 57|1. `SPT_HOME` resolves to `<ISO_HOME>` — read back from the command's own environment, not assumed.\n...\n 142|evidence that the heartbeat's drain-driving `brain.sessions()` call completed — that call is what\n*143|makes a wedge drainable at all (REQ-UPDATE-TRIAL-DRAIN-DRIVE), and a trial that never drove it looks\n 144|identical here to one that drove it and still waited. It also names **no session**: the gate answers a\n 145|single bool over every hosted session, and naming the wedged one would require a second, separately\n 146|taken scan that could disagree with the read the promote decision actually used. The narrower fact is\n...\n 232|   unresolved because the gate is a pure read with no false-branch emission —\n*233|   `old_gen_drained` → `any_local_controller_wedged` → `local_controller_wedged` (broker.rs:2816).\n 234|   `BRAIN_PROMOTE_GATE_NOT_DRAINED` now fires once per trial on the first not-drained read, from the\n 235|   same observation the promote decision uses. It establishes that the gate was evaluated, never that\n 236|   the drain-driving heartbeat call completed (§4.1).\n\nSkipped missing paths: .worktrees/49-267-refresh-discriminator/docs/CONTEXT.md, .worktrees/49-267-refresh-discriminator/docs/LIFECYCLE.md"}],"details":{"scopePath":".worktrees/49-267-refresh-discriminator/docs/49-267-REFRESH-OBSERVATION-PLAN.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\docs\\49-267-REFRESH-OBSERVATION-PLAN.md","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":3,"fileCount":1,"files":[".worktrees/49-267-refresh-discriminator/docs/49-267-REFRESH-OBSERVATION-PLAN.md"],"fileMatches":[{"path":".worktrees/49-267-refresh-discriminator/docs/49-267-REFRESH-OBSERVATION-PLAN.md","count":3}],"truncated":false,"displayContent":"  53│\n *54│**Before `daemon refresh` and before `daemon stop`, all four checks must pass. Any failure and the\n  55│command does not run:**\n  56│\n  57│1. `SPT_HOME` resolves to `<ISO_HOME>` — read back from the command's own environment, not assumed.\n    │...\n 142│evidence that the heartbeat's drain-driving `brain.sessions()` call completed — that call is what\n*143│makes a wedge drainable at all (REQ-UPDATE-TRIAL-DRAIN-DRIVE), and a trial that never drove it looks\n 144│identical here to one that drove it and still waited. It also names **no session**: the gate answers a\n 145│single bool over every hosted session, and naming the wedged one would require a second, separately\n 146│taken scan that could disagree with the read the promote decision actually used. The narrower fact is\n    │...\n 232│   unresolved because the gate is a pure read with no false-branch emission —\n*233│   `old_gen_drained` → `any_local_controller_wedged` → `local_controller_wedged` (broker.rs:2816).\n 234│   `BRAIN_PROMOTE_GATE_NOT_DRAINED` now fires once per trial on the first not-drained read, from the\n 235│   same observation the promote decision uses. It establishes that the gate was evaluated, never that\n 236│   the drain-driving heartbeat call completed (§4.1).","missingPaths":[".worktrees/49-267-refresh-discriminator/docs/CONTEXT.md",".worktrees/49-267-refresh-discriminator/docs/LIFECYCLE.md"]},"isError":false,"timestamp":1789528588596}}
{"type":"message","id":"2905ab61","parentId":"dcff4d11","timestamp":"2026-09-16T03:16:28.612Z","message":{"role":"toolResult","toolCallId":"call_Aifypti4nZhhAs3unI5xD8VQ|fc_0cf46cef591a107c016aaa0a0c6e1087d2977153a5eec17c35","toolName":"read","content":[{"type":"text","text":"    /// Whether this session's controller is the brain's OWN LOCAL conn (`by: None`)\n    /// AND wedged past `deadline` — the brain-generation promotion DRAINED gate reads\n    /// it (REQ-UPDATE-PROMOTE-DRAINED). A killed OLD brain generation whose local\n    /// controller conn is black-holed reads wedged here until the W2 stall-evict\n    /// releases it (or the conn EOFs); a cleanly-closed conn is simply absent. A\n    /// REMOTE operator's wedged conn (`by: Some`) is the W2 stall-evict concern, NOT\n    /// an old-gen brain conn, so it is EXCLUDED — a stuck remote viewer must never\n    /// gate a brain promotion. Pure over the injected `now`/`deadline` (no thread).\n    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n    fn local_controller_wedged(&self, now: Instant, deadline: Duration) -> bool {\n        self.controller.as_ref().is_some_and(|c| c.by.is_none())\n            && self.controller_write_stalled(now, deadline)\n    }\n\n    /// Stall-evict a controller wedged past [`BRAIN_WRITE_DEADLINE`]: clear the slot\n    /// and re-stamp `driven_by`/`controlled` (the BROKER-side release — no brain\n    /// round-trip, so `controlled-by` can never latch behind a dead transit) and\n    /// record the evict for observability (broker log now; W3 persists the stderr,\n    /// the tally surfaces on `daemon status`). The take/reattach path and the\n    /// `KIND_SESSIONS` reap call this so a suspended brain subscriber can never hold\n    /// control on an idle session (the output-driven [`mark_controller_gone`] never\n    /// fires without new output). Returns true iff it evicted. `stamp` selects whether\n    /// the perch re-stamp happens inline (the take/reattach path, which already stamps\n…\n\n    // [unit->REQ-UPDATE-PROMOTE-DRAINED] the promotion DRAINED gate's broker-truth\n    // read is LOCAL-scoped: only the brain's OWN wedged conn (by:None) reads wedged;\n    // a wedged REMOTE operator conn (by:Some) is the W2 concern and must NOT gate a\n    // brain promotion; an idle local controller (nothing in flight) is not wedged.\n    #[test]\n    fn local_controller_wedged_scopes_to_the_brains_own_conn() {\n        let now = Instant::now();\n        // A LOCAL (by:None) controller wedged past the deadline → wedged.\n        let mut local = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n        seed_controller_with_block(&mut local, None, Some(wedged_since()));\n        assert!(\n            local.local_controller_wedged(now, BRAIN_WRITE_DEADLINE),\n            \"the brain's own wedged local conn gates promotion\"\n        );\n        local.controller = None;\n\n        // A REMOTE (by:Some) controller wedged the SAME way → NOT counted (W2 concern,\n        // not an old-gen brain conn — a stuck remote viewer must never gate a promote).\n        let mut remote = OutputLog::new(2, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n        seed_controller_with_block(&mut remote, Some(\"operator\"), Some(wedged_since()));\n        assert!(\n            !remote.local_controller_wedged(now, BRAIN_WRITE_DEADLINE),\n            \"a wedged REMOTE controller is excluded from the drained gate\"\n        );\n        remote.controller = None;\n\n        // A LOCAL controller that is merely IDLE (nothing in flight) → not wedged.\n        let mut idle = OutputLog::new(3, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n        seed_controller_with_block(&mut idle, None, None);\n        assert!(\n            !idle.local_controller_wedged(now, BRAIN_WRITE_DEADLINE),\n            \"an idle local controller is drained, not wedged\"\n        );\n        idle.controller = None;\n    }\n\n    // [unit->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP] the single-flight wake truth table: an\n    // already-live endpoint dedups (AlreadyLive) regardless of claim/deadline; a\n    // claimed-and-within-window endpoint stands down (Racing) waiting for the racer's\n    // session; a free endpoint — or a claim that OVERRAN the wait window (leaked from a"}],"details":{"displayContent":{"text":"    /// Whether this session's controller is the brain's OWN LOCAL conn (`by: None`)\n    /// AND wedged past `deadline` — the brain-generation promotion DRAINED gate reads\n    /// it (REQ-UPDATE-PROMOTE-DRAINED). A killed OLD brain generation whose local\n    /// controller conn is black-holed reads wedged here until the W2 stall-evict\n    /// releases it (or the conn EOFs); a cleanly-closed conn is simply absent. A\n    /// REMOTE operator's wedged conn (`by: Some`) is the W2 stall-evict concern, NOT\n    /// an old-gen brain conn, so it is EXCLUDED — a stuck remote viewer must never\n    /// gate a brain promotion. Pure over the injected `now`/`deadline` (no thread).\n    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n    fn local_controller_wedged(&self, now: Instant, deadline: Duration) -> bool {\n        self.controller.as_ref().is_some_and(|c| c.by.is_none())\n            && self.controller_write_stalled(now, deadline)\n    }\n\n    /// Stall-evict a controller wedged past [`BRAIN_WRITE_DEADLINE`]: clear the slot\n    /// and re-stamp `driven_by`/`controlled` (the BROKER-side release — no brain\n    /// round-trip, so `controlled-by` can never latch behind a dead transit) and\n    /// record the evict for observability (broker log now; W3 persists the stderr,\n    /// the tally surfaces on `daemon status`). The take/reattach path and the\n    /// `KIND_SESSIONS` reap call this so a suspended brain subscriber can never hold\n    /// control on an idle session (the output-driven [`mark_controller_gone`] never\n    /// fires without new output). Returns true iff it evicted. `stamp` selects whether\n    /// the perch re-stamp happens inline (the take/reattach path, which already stamps\n…\n\n    // [unit->REQ-UPDATE-PROMOTE-DRAINED] the promotion DRAINED gate's broker-truth\n    // read is LOCAL-scoped: only the brain's OWN wedged conn (by:None) reads wedged;\n    // a wedged REMOTE operator conn (by:Some) is the W2 concern and must NOT gate a\n    // brain promotion; an idle local controller (nothing in flight) is not wedged.\n    #[test]\n    fn local_controller_wedged_scopes_to_the_brains_own_conn() {\n        let now = Instant::now();\n        // A LOCAL (by:None) controller wedged past the deadline → wedged.\n        let mut local = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n        seed_controller_with_block(&mut local, None, Some(wedged_since()));\n        assert!(\n            local.local_controller_wedged(now, BRAIN_WRITE_DEADLINE),\n            \"the brain's own wedged local conn gates promotion\"\n        );\n        local.controller = None;\n\n        // A REMOTE (by:Some) controller wedged the SAME way → NOT counted (W2 concern,\n        // not an old-gen brain conn — a stuck remote viewer must never gate a promote).\n        let mut remote = OutputLog::new(2, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n        seed_controller_with_block(&mut remote, Some(\"operator\"), Some(wedged_since()));\n        assert!(\n            !remote.local_controller_wedged(now, BRAIN_WRITE_DEADLINE),\n            \"a wedged REMOTE controller is excluded from the drained gate\"\n        );\n        remote.controller = None;\n\n        // A LOCAL controller that is merely IDLE (nothing in flight) → not wedged.\n        let mut idle = OutputLog::new(3, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n        seed_controller_with_block(&mut idle, None, None);\n        assert!(\n            !idle.local_controller_wedged(now, BRAIN_WRITE_DEADLINE),\n            \"an idle local controller is drained, not wedged\"\n        );\n        idle.controller = None;\n    }\n\n    // [unit->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP] the single-flight wake truth table: an\n    // already-live endpoint dedups (AlreadyLive) regardless of claim/deadline; a\n    // claimed-and-within-window endpoint stands down (Racing) waiting for the racer's\n    // session; a free endpoint — or a claim that OVERRAN the wait window (leaked from a","startLine":2850,"lineNumbers":[2850,2851,2852,2853,2854,2855,2856,2857,2858,2859,2860,2861,2862,2863,2864,2865,2866,2867,2868,2869,2870,2871,2872,null,13305,13306,13307,13308,13309,13310,13311,13312,13313,13314,13315,13316,13317,13318,13319,13320,13321,13322,13323,13324,13325,13326,13327,13328,13329,13330,13331,13332,13333,13334,13335,13336,13337,13338,13339,13340,13341,13342,13343,13344,13345]},"fileSize":877537,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon\\src\\broker.rs"}}},"isError":false,"timestamp":1789528588612}}
{"type":"message","id":"ae0733ed","parentId":"2905ab61","timestamp":"2026-09-16T03:16:28.651Z","message":{"role":"toolResult","toolCallId":"call_lPYYsjMRwdd4lmTLg1skxeCb|fc_0cf46cef591a107c016aaa0a0c6df087d2b07c4604f9c52462","toolName":"grep","content":[{"type":"text","text":"*1|\"\"\"Windows real-daemon/real-RC refresh regression; run in the native owned job.\n 2|\n 3|Only this run's copied executable, fresh SPT_HOME and guarded daemon are mutated.\n*4|The independent producer ledger is authoritative for child input, never PTY echo.\n 5|\"\"\"\n 6|import argparse\n 7|import base64\n...\n 133|        # The still-running driver can itself retain conhost after all subjects\n*134|        # exit. Detach only this process; final job accounting remains mandatory.\n 135|        query = self.k32.GetConsoleProcessList\n 136|        query.argtypes, query.restype = [ctypes.POINTER(wintypes.DWORD), wintypes.DWORD], wintypes.DWORD\n 137|        pids = (wintypes.DWORD * 256)()\n...\n 147|        self.check(self.k32.FreeConsole())\n*148|        return {\"attached\": True, \"members\": members, \"detached\": True}\n 149|\n 150|    def snapshot(self):\n 151|        handle = self.k32.CreateToolhelp32Snapshot(2, 0)\n...\n 223|        self.broker = self.rc = self.reader = None\n*224|        self.streams, self.records, self.inputs, self.reader_errors = [], [], [], []\n 225|        self.owned, self.ledger_seen = {}, {}\n 226|        self.sockets = []\n 227|        self.native = None\n*228|        self.refresh = None\n*229|        self.baseline = None\n 230|        self.cmd_number = 0\n*231|        self.home = out / \"home-refreshrig\"\n 232|        self.spt = out / \"bin\" / \"spt.exe\"\n 233|        self.pin = config[\"sha256\"].lower()\n 234|        self.run_id = uuid.uuid4().hex[:12]\n...\n 374|                last_error = repr(error)\n*375|                time.sleep(min(.1, max(0, end - time.monotonic())))\n 376|        raise RuntimeError(f\"daemon startup bound exhausted: {last_error}\")\n 377|\n 378|    def fixture(self):\n...\n 393|        (source / \"manifest.toml\").write_text(manifest, encoding=\"utf-8\")\n*394|        for args in ([\"adapter\", \"add\", str(source)], [\"endpoint\", \"create\", \"refreshrig\", \"--adapter\", \"evidencerig\"],\n*395|                     [\"endpoint\", \"start\", \"refreshrig\"]):\n 396|            result = self.command(args, mutate=True, timeout=25)\n 397|            require(result.returncode == 0, f\"fixture command refused: {result.stderr}\")\n 398|        self.guard(\"rc attach\")\n...\n 400|        self.streams.append(err)\n*401|        self.rc = subprocess.Popen([str(self.spt), \"rc\", \"refreshrig\"], env=self.env(), cwd=self.out,\n 402|                                   stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=err, bufsize=0,\n 403|                                   creationflags=subprocess.CREATE_NO_WINDOW)\n 404|        rc = self.identify(self.rc.pid, \"rc\")\n...\n 419|            require(self.rc.poll() is None and not self.reader_errors, \"RC failed before progress admission\")\n*420|            time.sleep(.05)\n 421|        raise RuntimeError(\"real RC progress not observed within attach bound\")\n 422|\n 423|    def read_output(self):\n...\n 458|\n*459|    def write_input(self, payload):\n 460|        finished, errors = threading.Event(), []\n 461|\n 462|        def write():\n...\n 471|        threading.Thread(target=write, daemon=True).start()\n*472|        require(finished.wait(min(1, self.remaining())), \"RC input write exceeded 1s bound\")\n*473|        require(not errors, f\"RC input write failed: {errors}\")\n 474|\n 475|    def send(self, index):\n*476|        self.guard(\"tagged RC input\")\n*477|        require(self.rc.poll() is None, \"RC exited before tagged input\")\n 478|        tag = f\"TAG-{self.run_id}-{self.phase}-{index:03}\"\n 479|        row = {\"tag\": tag, \"phase\": self.phase, \"sent_ms\": now_ms(), \"sent_mono\": time.monotonic()}\n*480|        self.inputs.append(row)\n*481|        self.event(\"INPUT_SENT\", **row)\n*482|        self.write_input((tag + \"\\r\").encode(\"ascii\"))\n 483|\n 484|    def ledger(self):\n*485|        path = self.home / \"generator-input.jsonl\"\n 486|        if not path.exists():\n 487|            return []\n 488|        data = path.read_bytes()\n...\n 504|        samples = []\n*505|        for sent in self.inputs:\n 506|            if sent[\"phase\"] != phase:\n 507|                continue\n 508|            sample = {**sent, \"complete\": False, \"timely\": False}\n...\n 523|                    sample.update(complete=True, rtt_ms=rtt, completed_mono=completed,\n*524|                                  timely=0 <= rtt <= self.config[\"ack_bound_ms\"]\n*525|                                  and (state[\"received_mono\"] - sent[\"sent_mono\"]) * 1000 <= self.config[\"ack_bound_ms\"])\n 526|            if not sample[\"complete\"]:\n 527|                sample[\"interpretation\"] = \"not observed within bound; not proven lost\"\n 528|            samples.append(sample)\n...\n 536|        # Leave the last ACK bound inside the observation for the final tag to settle.\n*537|        send_end = end - self.config[\"ack_bound_ms\"] / 1000\n 538|        index, next_send = 0, begin\n 539|        self.event(\"PHASE_START\", seconds=seconds, send_until_mono=send_end)\n 540|        while time.monotonic() < end:\n...\n 546|            self.ledger()\n*547|            time.sleep(min(.025, max(0, end - time.monotonic())))\n 548|        self.event(\"PHASE_END\", actual_s=time.monotonic() - begin)\n 549|        return self.samples(phase)\n 550|\n...\n 556|        save(self.out / \"net-admission.json\", payload)\n*557|        self.baseline = self.observe(\"baseline\", self.config[\"baseline_seconds\"])\n*558|        save(self.out / \"baseline.json\", self.baseline)\n*559|        require(sum(row[\"timely\"] for row in self.baseline) >= 12, \"baseline lacks 12 unique timely ACK+state tags\")\n*560|        self.phase = \"refresh\"\n*561|        self.guard(\"real node refresh\")\n 562|        durable = self.home / \"logs/daemon.stderr.log\"\n 563|        require(durable.is_file(), \"durable daemon stderr unavailable\")\n*564|        self.refresh = {\"started_ms\": now_ms(), \"started_mono\": time.monotonic(),\n 565|                        \"log_offset\": durable.stat().st_size, \"initial_brain\": self.initial_brain}\n*566|        save(self.out / \"refresh.json\", self.refresh)\n*567|        result = self.command([\"node\", \"refresh\"], mutate=True, timeout=20)\n*568|        self.refresh.update(exit=result.returncode, returned_ms=now_ms(), returned_mono=time.monotonic())\n*569|        save(self.out / \"refresh.json\", self.refresh)\n*570|        require(result.returncode == 0, f\"real node refresh refused: {result.stderr}\")\n*571|        self.post = self.observe(\"post-refresh\", self.config[\"post_refresh_seconds\"])\n*572|        save(self.out / \"post-refresh.json\", self.post)\n 573|        replacement = json.loads((self.home / \"brain.ready\").read_text(encoding=\"utf-8\"))\n 574|        brain = self.identify(replacement[\"pid\"], \"replacement-brain\")\n 575|        require(Path(brain[\"exe\"]) == self.spt and self.broker.pid in [row[\"pid\"] for row in brain[\"ancestors\"]],\n 576|                \"replacement brain image/ancestry mismatch\")\n*577|        self.refresh[\"replacement_brain\"] = brain\n 578|        save(self.out / \"brain-ready-replacement.json\", replacement)\n*579|        save(self.out / \"refresh.json\", self.refresh)\n*580|        self.guard(\"post-refresh complete\")\n 581|        self.observation_end_ms = now_ms()\n 582|        shutil.copy2(durable, self.out / \"daemon-before-cleanup.log\")\n 583|        return self.predicates()\n...\n 587|        lines = raw.decode(\"utf-8\", \"replace\").splitlines()\n*588|        after = raw[self.refresh[\"log_offset\"]:].decode(\"utf-8\", \"replace\").splitlines()\n 589|        closures = [line for line in lines if \"CONTROLLER_SLOT_CLOSED:\" in line]\n*590|        bad_closures = [line for line in closures if not re.search(r\"\\bcause=detach(?:\\s|$)\", line)]\n*591|        poison, old_poison = [], []\n 592|        for line in after:\n*593|            if \"CONN_WRITE_POISONED:\" not in line:\n 594|                continue\n 595|            first = re.search(r\"\\bfirst_write_wall_ms=(\\d+)\", line)\n*596|            if first and int(first.group(1)) < self.refresh[\"started_ms\"]:\n*597|                old_poison.append(line)\n 598|            else:\n*599|                poison.append(line)\n*600|        recovery_deadline = self.refresh[\"started_mono\"] + self.config[\"recovery_bound_ms\"] / 1000\n 601|        timely_candidates = [row for row in self.post if row[\"timely\"]\n 602|                             and row[\"completed_mono\"] <= recovery_deadline]\n 603|        candidate = next((row for row in timely_candidates\n...\n 609|                      for row in self.post)\n*610|        new_brain = self.refresh[\"replacement_brain\"]\n*611|        real_refresh = ((new_brain[\"pid\"], new_brain[\"birth\"]) !=\n 612|                        (self.initial_brain[\"pid\"], self.initial_brain[\"birth\"])\n 613|                        and any(\"BRAIN_UPDATE_RESTART:\" in line for line in after)\n 614|                        and any(\"BRAIN_UP:\" in line for line in after))\n*615|        checks = {\"real_refresh\": real_refresh, \"baseline_12_timely_tags\": sum(row[\"timely\"] for row in self.baseline) >= 12,\n 616|                  \"ack_recovered_within_bound\": recovered, \"sustained_timely_ack_and_state\": sustained,\n 617|                  \"all_post_tags_accounted_within_bound\": bool(self.post) and bounded,\n*618|                  \"zero_replacement_write_poison\": not poison,\n*619|                  \"controller_closures_detach_only\": bool(closures) and not bad_closures,\n 620|                  \"reader_healthy\": not self.reader_errors}\n*621|        return {\"checks\": checks, \"pass\": all(checks.values()), \"refresh\": self.refresh,\n*622|                \"recovery_ms\": ((candidate[\"completed_mono\"] - self.refresh[\"started_mono\"]) * 1000 if candidate else None),\n*623|                \"baseline\": self.baseline, \"post_refresh\": self.post, \"controller_closures\": closures,\n*624|                \"non_detach_closures\": bad_closures, \"replacement_write_poison\": poison,\n*625|                \"old_connection_write_poison\": old_poison, \"reader_errors\": self.reader_errors,\n 626|                \"observation_end_ms\": self.observation_end_ms}\n 627|\n 628|    def capture_descendants(self):\n...\n 645|        self.deadline = time.monotonic() + self.config[\"teardown_seconds\"]\n*646|        errors, detach_errors = [], []\n 647|        self.event(\"CLEANUP_START\")\n 648|        durable = self.home / \"logs/daemon.stderr.log\"\n 649|        if durable.exists():\n...\n 653|                try:\n*654|                    self.guard(\"rc detach\")\n*655|                    self.write_input(b\"\\x02d\")\n 656|                    self.rc.wait(timeout=min(5, self.remaining()))\n 657|                except Exception as error:\n*658|                    detach_errors.append(repr(error))\n 659|            try:\n 660|                self.guard(\"capture owned descendants\")\n 661|                self.capture_descendants()\n...\n 690|                    break\n*691|                time.sleep(.05)\n 692|        if self.reader:\n 693|            self.reader.join(timeout=max(0, min(.25, self.deadline - time.monotonic())))\n 694|            if self.reader.is_alive():\n...\n 699|        row = {\"confirmed_before_job_close\": confirmed, \"errors\": errors, \"survivors\": survivors,\n*700|               \"unknown\": unknown, \"job_survivors\": job_survivors, \"detach_errors\": detach_errors,\n 701|               \"sockets\": sockets, \"retained_home\": str(self.home),\n 702|               \"outer_job_is_final_containment\": True}\n 703|        save(self.out / \"teardown.json\", row)\n...\n 736|        require(re.fullmatch(r\"[0-9a-fA-F]{64}\", config[\"sha256\"]) is not None, \"sha256 must contain 64 hex digits\")\n*737|        for name in (\"observation_seconds\", \"teardown_seconds\", \"ack_bound_ms\", \"recovery_bound_ms\", \"baseline_seconds\", \"post_refresh_seconds\"):\n 738|            require(type(config[name]) in (int, float) and 0 < config[name] < 3_600_000, f\"invalid bound {name}\")\n*739|        require(config[\"ack_bound_ms\"] <= 2000 and config[\"recovery_bound_ms\"] <= 15000, \"correctness bounds cannot be weakened\")\n*740|        require(config[\"baseline_seconds\"] >= 15 and config[\"post_refresh_seconds\"] >= 25, \"observation windows too short\")\n 741|        require(config[\"observation_seconds\"] <= 150 and config[\"teardown_seconds\"] <= 60, \"outer phase bounds exceeded\")\n 742|        rig = Rig(config, out)\n 743|        rig.admit()\n...\n 750|        receipt[\"traceback\"] = traceback.format_exc()\n*751|        # Once an admitted baseline and successful real refresh exist, observation failure is red.\n*752|        if rig and rig.refresh and rig.refresh.get(\"exit\") == 0:\n 753|            receipt[\"status\"] = \"REGRESSION\"\n 754|        traceback.print_exc()\n 755|    finally:"}],"details":{"scopePath":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/support/refresh_freeze/rig.py","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon\\tests\\support\\refresh_freeze\\rig.py","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":72,"fileCount":1,"files":[".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/support/refresh_freeze/rig.py"],"fileMatches":[{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/support/refresh_freeze/rig.py","count":72}],"truncated":false,"displayContent":"  *1│\"\"\"Windows real-daemon/real-RC refresh regression; run in the native owned job.\n   2│\n   3│Only this run's copied executable, fresh SPT_HOME and guarded daemon are mutated.\n  *4│The independent producer ledger is authoritative for child input, never PTY echo.\n   5│\"\"\"\n   6│import argparse\n   7│import base64\n    │...\n 133│        # The still-running driver can itself retain conhost after all subjects\n*134│        # exit. Detach only this process; final job accounting remains mandatory.\n 135│        query = self.k32.GetConsoleProcessList\n 136│        query.argtypes, query.restype = [ctypes.POINTER(wintypes.DWORD), wintypes.DWORD], wintypes.DWORD\n 137│        pids = (wintypes.DWORD * 256)()\n    │...\n 147│        self.check(self.k32.FreeConsole())\n*148│        return {\"attached\": True, \"members\": members, \"detached\": True}\n 149│\n 150│    def snapshot(self):\n 151│        handle = self.k32.CreateToolhelp32Snapshot(2, 0)\n    │...\n 223│        self.broker = self.rc = self.reader = None\n*224│        self.streams, self.records, self.inputs, self.reader_errors = [], [], [], []\n 225│        self.owned, self.ledger_seen = {}, {}\n 226│        self.sockets = []\n 227│        self.native = None\n*228│        self.refresh = None\n*229│        self.baseline = None\n 230│        self.cmd_number = 0\n*231│        self.home = out / \"home-refreshrig\"\n 232│        self.spt = out / \"bin\" / \"spt.exe\"\n 233│        self.pin = config[\"sha256\"].lower()\n 234│        self.run_id = uuid.uuid4().hex[:12]\n    │...\n 374│                last_error = repr(error)\n*375│                time.sleep(min(.1, max(0, end - time.monotonic())))\n 376│        raise RuntimeError(f\"daemon startup bound exhausted: {last_error}\")\n 377│\n 378│    def fixture(self):\n    │...\n 393│        (source / \"manifest.toml\").write_text(manifest, encoding=\"utf-8\")\n*394│        for args in ([\"adapter\", \"add\", str(source)], [\"endpoint\", \"create\", \"refreshrig\", \"--adapter\", \"evidencerig\"],\n*395│                     [\"endpoint\", \"start\", \"refreshrig\"]):\n 396│            result = self.command(args, mutate=True, timeout=25)\n 397│            require(result.returncode == 0, f\"fixture command refused: {result.stderr}\")\n 398│        self.guard(\"rc attach\")\n    │...\n 400│        self.streams.append(err)\n*401│        self.rc = subprocess.Popen([str(self.spt), \"rc\", \"refreshrig\"], env=self.env(), cwd=self.out,\n 402│                                   stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=err, bufsize=0,\n 403│                                   creationflags=subprocess.CREATE_NO_WINDOW)\n 404│        rc = self.identify(self.rc.pid, \"rc\")\n    │...\n 419│            require(self.rc.poll() is None and not self.reader_errors, \"RC failed before progress admission\")\n*420│            time.sleep(.05)\n 421│        raise RuntimeError(\"real RC progress not observed within attach bound\")\n 422│\n 423│    def read_output(self):\n    │...\n 458│\n*459│    def write_input(self, payload):\n 460│        finished, errors = threading.Event(), []\n 461│\n 462│        def write():\n    │...\n 471│        threading.Thread(target=write, daemon=True).start()\n*472│        require(finished.wait(min(1, self.remaining())), \"RC input write exceeded 1s bound\")\n*473│        require(not errors, f\"RC input write failed: {errors}\")\n 474│\n 475│    def send(self, index):\n*476│        self.guard(\"tagged RC input\")\n*477│        require(self.rc.poll() is None, \"RC exited before tagged input\")\n 478│        tag = f\"TAG-{self.run_id}-{self.phase}-{index:03}\"\n 479│        row = {\"tag\": tag, \"phase\": self.phase, \"sent_ms\": now_ms(), \"sent_mono\": time.monotonic()}\n*480│        self.inputs.append(row)\n*481│        self.event(\"INPUT_SENT\", **row)\n*482│        self.write_input((tag + \"\\r\").encode(\"ascii\"))\n 483│\n 484│    def ledger(self):\n*485│        path = self.home / \"generator-input.jsonl\"\n 486│        if not path.exists():\n 487│            return []\n 488│        data = path.read_bytes()\n    │...\n 504│        samples = []\n*505│        for sent in self.inputs:\n 506│            if sent[\"phase\"] != phase:\n 507│                continue\n 508│            sample = {**sent, \"complete\": False, \"timely\": False}\n    │...\n 523│                    sample.update(complete=True, rtt_ms=rtt, completed_mono=completed,\n*524│                                  timely=0 <= rtt <= self.config[\"ack_bound_ms\"]\n*525│                                  and (state[\"received_mono\"] - sent[\"sent_mono\"]) * 1000 <= self.config[\"ack_bound_ms\"])\n 526│            if not sample[\"complete\"]:\n 527│                sample[\"interpretation\"] = \"not observed within bound; not proven lost\"\n 528│            samples.append(sample)\n    │...\n 536│        # Leave the last ACK bound inside the observation for the final tag to settle.\n*537│        send_end = end - self.config[\"ack_bound_ms\"] / 1000\n 538│        index, next_send = 0, begin\n 539│        self.event(\"PHASE_START\", seconds=seconds, send_until_mono=send_end)\n 540│        while time.monotonic() < end:\n    │...\n 546│            self.ledger()\n*547│            time.sleep(min(.025, max(0, end - time.monotonic())))\n 548│        self.event(\"PHASE_END\", actual_s=time.monotonic() - begin)\n 549│        return self.samples(phase)\n 550│\n    │...\n 556│        save(self.out / \"net-admission.json\", payload)\n*557│        self.baseline = self.observe(\"baseline\", self.config[\"baseline_seconds\"])\n*558│        save(self.out / \"baseline.json\", self.baseline)\n*559│        require(sum(row[\"timely\"] for row in self.baseline) >= 12, \"baseline lacks 12 unique timely ACK+state tags\")\n*560│        self.phase = \"refresh\"\n*561│        self.guard(\"real node refresh\")\n 562│        durable = self.home / \"logs/daemon.stderr.log\"\n 563│        require(durable.is_file(), \"durable daemon stderr unavailable\")\n*564│        self.refresh = {\"started_ms\": now_ms(), \"started_mono\": time.monotonic(),\n 565│                        \"log_offset\": durable.stat().st_size, \"initial_brain\": self.initial_brain}\n*566│        save(self.out / \"refresh.json\", self.refresh)\n*567│        result = self.command([\"node\", \"refresh\"], mutate=True, timeout=20)\n*568│        self.refresh.update(exit=result.returncode, returned_ms=now_ms(), returned_mono=time.monotonic())\n*569│        save(self.out / \"refresh.json\", self.refresh)\n*570│        require(result.returncode == 0, f\"real node refresh refused: {result.stderr}\")\n*571│        self.post = self.observe(\"post-refresh\", self.config[\"post_refresh_seconds\"])\n*572│        save(self.out / \"post-refresh.json\", self.post)\n 573│        replacement = json.loads((self.home / \"brain.ready\").read_text(encoding=\"utf-8\"))\n 574│        brain = self.identify(replacement[\"pid\"], \"replacement-brain\")\n 575│        require(Path(brain[\"exe\"]) == self.spt and self.broker.pid in [row[\"pid\"] for row in brain[\"ancestors\"]],\n 576│                \"replacement brain image/ancestry mismatch\")\n*577│        self.refresh[\"replacement_brain\"] = brain\n 578│        save(self.out / \"brain-ready-replacement.json\", replacement)\n*579│        save(self.out / \"refresh.json\", self.refresh)\n*580│        self.guard(\"post-refresh complete\")\n 581│        self.observation_end_ms = now_ms()\n 582│        shutil.copy2(durable, self.out / \"daemon-before-cleanup.log\")\n 583│        return self.predicates()\n    │...\n 587│        lines = raw.decode(\"utf-8\", \"replace\").splitlines()\n*588│        after = raw[self.refresh[\"log_offset\"]:].decode(\"utf-8\", \"replace\").splitlines()\n 589│        closures = [line for line in lines if \"CONTROLLER_SLOT_CLOSED:\" in line]\n*590│        bad_closures = [line for line in closures if not re.search(r\"\\bcause=detach(?:\\s|$)\", line)]\n*591│        poison, old_poison = [], []\n 592│        for line in after:\n*593│            if \"CONN_WRITE_POISONED:\" not in line:\n 594│                continue\n 595│            first = re.search(r\"\\bfirst_write_wall_ms=(\\d+)\", line)\n*596│            if first and int(first.group(1)) < self.refresh[\"started_ms\"]:\n*597│                old_poison.append(line)\n 598│            else:\n*599│                poison.append(line)\n*600│        recovery_deadline = self.refresh[\"started_mono\"] + self.config[\"recovery_bound_ms\"] / 1000\n 601│        timely_candidates = [row for row in self.post if row[\"timely\"]\n 602│                             and row[\"completed_mono\"] <= recovery_deadline]\n 603│        candidate = next((row for row in timely_candidates\n    │...\n 609│                      for row in self.post)\n*610│        new_brain = self.refresh[\"replacement_brain\"]\n*611│        real_refresh = ((new_brain[\"pid\"], new_brain[\"birth\"]) !=\n 612│                        (self.initial_brain[\"pid\"], self.initial_brain[\"birth\"])\n 613│                        and any(\"BRAIN_UPDATE_RESTART:\" in line for line in after)\n 614│                        and any(\"BRAIN_UP:\" in line for line in after))\n*615│        checks = {\"real_refresh\": real_refresh, \"baseline_12_timely_tags\": sum(row[\"timely\"] for row in self.baseline) >= 12,\n 616│                  \"ack_recovered_within_bound\": recovered, \"sustained_timely_ack_and_state\": sustained,\n 617│                  \"all_post_tags_accounted_within_bound\": bool(self.post) and bounded,\n*618│                  \"zero_replacement_write_poison\": not poison,\n*619│                  \"controller_closures_detach_only\": bool(closures) and not bad_closures,\n 620│                  \"reader_healthy\": not self.reader_errors}\n*621│        return {\"checks\": checks, \"pass\": all(checks.values()), \"refresh\": self.refresh,\n*622│                \"recovery_ms\": ((candidate[\"completed_mono\"] - self.refresh[\"started_mono\"]) * 1000 if candidate else None),\n*623│                \"baseline\": self.baseline, \"post_refresh\": self.post, \"controller_closures\": closures,\n*624│                \"non_detach_closures\": bad_closures, \"replacement_write_poison\": poison,\n*625│                \"old_connection_write_poison\": old_poison, \"reader_errors\": self.reader_errors,\n 626│                \"observation_end_ms\": self.observation_end_ms}\n 627│\n 628│    def capture_descendants(self):\n    │...\n 645│        self.deadline = time.monotonic() + self.config[\"teardown_seconds\"]\n*646│        errors, detach_errors = [], []\n 647│        self.event(\"CLEANUP_START\")\n 648│        durable = self.home / \"logs/daemon.stderr.log\"\n 649│        if durable.exists():\n    │...\n 653│                try:\n*654│                    self.guard(\"rc detach\")\n*655│                    self.write_input(b\"\\x02d\")\n 656│                    self.rc.wait(timeout=min(5, self.remaining()))\n 657│                except Exception as error:\n*658│                    detach_errors.append(repr(error))\n 659│            try:\n 660│                self.guard(\"capture owned descendants\")\n 661│                self.capture_descendants()\n    │...\n 690│                    break\n*691│                time.sleep(.05)\n 692│        if self.reader:\n 693│            self.reader.join(timeout=max(0, min(.25, self.deadline - time.monotonic())))\n 694│            if self.reader.is_alive():\n    │...\n 699│        row = {\"confirmed_before_job_close\": confirmed, \"errors\": errors, \"survivors\": survivors,\n*700│               \"unknown\": unknown, \"job_survivors\": job_survivors, \"detach_errors\": detach_errors,\n 701│               \"sockets\": sockets, \"retained_home\": str(self.home),\n 702│               \"outer_job_is_final_containment\": True}\n 703│        save(self.out / \"teardown.json\", row)\n    │...\n 736│        require(re.fullmatch(r\"[0-9a-fA-F]{64}\", config[\"sha256\"]) is not None, \"sha256 must contain 64 hex digits\")\n*737│        for name in (\"observation_seconds\", \"teardown_seconds\", \"ack_bound_ms\", \"recovery_bound_ms\", \"baseline_seconds\", \"post_refresh_seconds\"):\n 738│            require(type(config[name]) in (int, float) and 0 < config[name] < 3_600_000, f\"invalid bound {name}\")\n*739│        require(config[\"ack_bound_ms\"] <= 2000 and config[\"recovery_bound_ms\"] <= 15000, \"correctness bounds cannot be weakened\")\n*740│        require(config[\"baseline_seconds\"] >= 15 and config[\"post_refresh_seconds\"] >= 25, \"observation windows too short\")\n 741│        require(config[\"observation_seconds\"] <= 150 and config[\"teardown_seconds\"] <= 60, \"outer phase bounds exceeded\")\n 742│        rig = Rig(config, out)\n 743│        rig.admit()\n    │...\n 750│        receipt[\"traceback\"] = traceback.format_exc()\n*751│        # Once an admitted baseline and successful real refresh exist, observation failure is red.\n*752│        if rig and rig.refresh and rig.refresh.get(\"exit\") == 0:\n 753│            receipt[\"status\"] = \"REGRESSION\"\n 754│        traceback.print_exc()\n 755│    finally:"},"isError":false,"timestamp":1789528588650}}
{"type":"message","id":"cd3f6ce6","parentId":"ae0733ed","timestamp":"2026-09-16T03:16:28.675Z","message":{"role":"toolResult","toolCallId":"call_CLPf7wLkTzK5ZHHdJat2X4af|fc_0cf46cef591a107c016aaa0a0c6e1887d281334cc8c0abdf25","toolName":"grep","content":[{"type":"text","text":"# .worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/\n## broker.rs\n 765|/// — the echo-verify head match over concatenated ring bytes.)\n*766|fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {\n 767|    if needle.is_empty() {\n 768|        return true;\n 769|    }\n...\n 4327|    /// perch (raw broker tests) has nowhere to record.\n*4328|    fn stamp_backpressure(&self, dropping: bool) {\n 4329|        if self.endpoint.is_empty() {\n 4330|            return;\n 4331|        }\n...\n 8923|                // Ack exactly-once — but ONLY when the caller requested it\n*8924|                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). The fire-and-forward\n 8925|                // operator/rc path sends `ack=false`: writing the ack back onto the\n 8926|                // same brain↔broker conn it is flooding with input fills the return\n 8927|                // direction and deadlocks the per-conn handler (the wedge). With no\n...\n 8931|                // applied-set is the exactly-once anchor, not the ack frame).\n*8932|                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n 8933|                if req.ack {\n 8934|                    send_frame(\n 8935|                        send,\n...\n 9568|    // [impl->REQ-RC-1]\n*9569|    fn dispatch_net_dial_loopback(&self, send: &SharedSend) -> Result<(), String> {\n 9570|        let host = self\n 9571|            .net\n 9572|            .get()\n...\n 10629|    #[test]\n*10630|    fn wrapper_alive_prefers_the_table_and_falls_back_where_there_is_none() {\n 10631|        // Table speaks → it WINS, even when a retained handle says otherwise.\n 10632|        // This is the Windows defect in one line: handle says alive, table says\n 10633|        // gone, and the corpse must read GONE.\n...\n 11968|    #[test]\n*11969|    fn tracked_state_inside_the_window_surfaces_in_the_sync_frame() {\n 11970|        let (view_send, mut view_client, _view_recv) = controller_socket_pair();\n 11971|        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (4, 20));\n 11972|        assert!(log.append(b\"pre\").is_none());\n...\n 12032|    #[test]\n*12033|    fn untracked_toggles_defer_into_the_sync_frame_and_hyperlinks_drop() {\n 12034|        let (view_send, mut view_client, _view_recv) = controller_socket_pair();\n 12035|        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (4, 20));\n 12036|        assert!(log.append(b\"pre\").is_none());\n...\n 12492|    #[test]\n*12493|    fn apply_once_enqueues_ptywrite_exactly_once_and_dedups_replay() {\n 12494|        use crate::effect::{EffectJournal, EffectKey, EffectKind, Minter, Outcome};\n 12495|\n 12496|        let dir = tempfile::tempdir().unwrap();\n...\n 13471|    #[test]\n*13472|    fn select_endpoints_running_adapter_filters_dedups_sorts() {\n 13473|        use std::path::PathBuf;\n 13474|        let dir_a = PathBuf::from(\"/install/ep-a\");\n 13475|        let dir_b = PathBuf::from(\"/install/ep-b\");\n...\n 16425|    #[test]\n*16426|    fn a_failed_fido2_proof_spends_nothing_and_the_totp_fallback_still_admits() {\n 16427|        crate::test_home::with_home(|_| {\n 16428|            let (member, _admin) = provision_seal_subnet(\"sealnet\");\n 16429|            let broker = admit_test_broker();\n## msg.rs\n 767|    /// Whether the broker should reply a [`KIND_APPLIED`] ack for this op\n*768|    /// (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). `true` — the ack is sent (the\n 769|    /// `shellchan` one-at-a-time drain WAITS on it). `false` — NO ack frame is\n 770|    /// written back, so a FLOOD of operator input on one brain↔broker conn cannot\n 771|    /// fill the return direction and deadlock the per-conn handler (the rc/operator\n...\n 775|    /// brain's `input` (no `ack` field) still acks — today's behavior, N-1-safe.\n*776|    // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n 777|    #[serde(default = \"default_true\")]\n 778|    pub ack: bool,\n 779|}\n...\n 781|/// serde default for [`InputReq::ack`] — absent on the wire ⇒ `true` (the\n*782|/// pre-REQ-HAZARD-INPUT-ACK-BACKPRESSURE behavior: every `input` was acked).\n 783|fn default_true() -> bool {\n 784|    true\n 785|}\n...\n 2250|\n*2251|    // [unit->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] An `input` payload from an OLDER\n 2252|    // brain carries NO `ack` field on the wire (the field was added by this REQ).\n 2253|    // It MUST decode to `ack = true` so the old-brain path keeps acking exactly as\n 2254|    // it did before the fix (N-1 forward-compat: an old brain's `input` still gets\n...\n 2261|    #[test]\n*2262|    fn input_req_absent_ack_defaults_true_n_minus_1_safe() {\n 2263|        // The exact shape an N-1 brain emits: session + base64 + op_id, NO `ack`.\n 2264|        let n_minus_1 = json!({\n 2265|            \"session_id\": 7,\n...\n 2284|\n*2285|    // [unit->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] The new fire-and-forward path emits\n 2286|    // `ack = false` on the wire, and it round-trips faithfully (a present `false`\n 2287|    // is NOT clobbered back to the default `true`). This is the bit the broker's\n 2288|    // `dispatch_input` gate reads to decide whether to write the applied frame.\n...\n 2293|    #[test]\n*2294|    fn input_req_ack_false_round_trips_faithfully() {\n 2295|        let req = InputReq {\n 2296|            session_id: 3,\n 2297|            data_b64: encode_bytes(b\"PASTE\"),\n## brain.rs\n 918|    /// conn — use [`Brain::send_effect_no_ack`] to avoid the ack-backpressure\n*919|    /// deadlock (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\n 920|    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n 921|    pub fn send_effect(&mut self, op: MintedOp, bytes: &[u8]) -> io::Result<()> {\n 922|        self.send_effect_inner(op, bytes, true)\n...\n 925|    /// Inject input idempotently like [`Brain::send_effect`] but request NO\n*926|    /// [`KIND_APPLIED`] ack (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). The operator/rc\n 927|    /// attach path is fire-and-forward — `op_id` is for dedup only and the ack is\n 928|    /// discarded by `serve_attach` — and it can flood N input frames on ONE\n 929|    /// brain↔broker conn without returning to read. With no ack the broker's\n...\n 932|    /// broker still dedups by `(session, op_id)` at its applied-set.\n*933|    // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n*934|    pub fn send_effect_no_ack(&mut self, op: MintedOp, bytes: &[u8]) -> io::Result<()> {\n 935|        self.send_effect_inner(op, bytes, false)\n 936|    }\n 937|\n...\n 942|    /// uncompilable. The broker's `KIND_APPLIED` still echoes the bare `seq`.\n*943|    fn send_effect_inner(&mut self, op: MintedOp, bytes: &[u8], ack: bool) -> io::Result<()> {\n 944|        let sid = self.require_session()?;\n 945|        self.send(\n 946|            KIND_INPUT,\n...\n 2035|    // [impl->REQ-RC-1]\n*2036|    pub fn net_dial_loopback(&mut self) -> io::Result<NetDialed> {\n 2037|        self.send(KIND_NET_DIAL_LOOPBACK, serde_json::Value::Null)?;\n 2038|        // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n 2039|        let deadline = self.call_deadline();\n...\n 2691|/// Whether `haystack` contains the contiguous `needle`.\n*2692|fn contains(haystack: &[u8], needle: &[u8]) -> bool {\n 2693|    if needle.is_empty() {\n 2694|        return true;\n 2695|    }\n...\n 3019|    #[test]\n*3020|    fn a_flagged_sync_frame_baselines_forward_never_backward_on_both_paths() {\n 3021|        let sid = 1u64;\n 3022|\n 3023|        // ── LEGACY strict path ──────────────────────────────────────────────"}],"details":{"scopePath":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs, .worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/msg.rs, .worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/brain.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":26,"fileCount":3,"files":[".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/msg.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/brain.rs"],"fileMatches":[{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs","count":11},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/msg.rs","count":7},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/brain.rs","count":8}],"truncated":false,"displayContent":"# .worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/\n## broker.rs\n   765│/// — the echo-verify head match over concatenated ring bytes.)\n  *766│fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {\n   767│    if needle.is_empty() {\n   768│        return true;\n   769│    }\n      │...\n  4327│    /// perch (raw broker tests) has nowhere to record.\n *4328│    fn stamp_backpressure(&self, dropping: bool) {\n  4329│        if self.endpoint.is_empty() {\n  4330│            return;\n  4331│        }\n      │...\n  8923│                // Ack exactly-once — but ONLY when the caller requested it\n *8924│                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). The fire-and-forward\n  8925│                // operator/rc path sends `ack=false`: writing the ack back onto the\n  8926│                // same brain↔broker conn it is flooding with input fills the return\n  8927│                // direction and deadlocks the per-conn handler (the wedge). With no\n      │...\n  8931│                // applied-set is the exactly-once anchor, not the ack frame).\n *8932│                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n  8933│                if req.ack {\n  8934│                    send_frame(\n  8935│                        send,\n      │...\n  9568│    // [impl->REQ-RC-1]\n *9569│    fn dispatch_net_dial_loopback(&self, send: &SharedSend) -> Result<(), String> {\n  9570│        let host = self\n  9571│            .net\n  9572│            .get()\n      │...\n 10629│    #[test]\n*10630│    fn wrapper_alive_prefers_the_table_and_falls_back_where_there_is_none() {\n 10631│        // Table speaks → it WINS, even when a retained handle says otherwise.\n 10632│        // This is the Windows defect in one line: handle says alive, table says\n 10633│        // gone, and the corpse must read GONE.\n      │...\n 11968│    #[test]\n*11969│    fn tracked_state_inside_the_window_surfaces_in_the_sync_frame() {\n 11970│        let (view_send, mut view_client, _view_recv) = controller_socket_pair();\n 11971│        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (4, 20));\n 11972│        assert!(log.append(b\"pre\").is_none());\n      │...\n 12032│    #[test]\n*12033│    fn untracked_toggles_defer_into_the_sync_frame_and_hyperlinks_drop() {\n 12034│        let (view_send, mut view_client, _view_recv) = controller_socket_pair();\n 12035│        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (4, 20));\n 12036│        assert!(log.append(b\"pre\").is_none());\n      │...\n 12492│    #[test]\n*12493│    fn apply_once_enqueues_ptywrite_exactly_once_and_dedups_replay() {\n 12494│        use crate::effect::{EffectJournal, EffectKey, EffectKind, Minter, Outcome};\n 12495│\n 12496│        let dir = tempfile::tempdir().unwrap();\n      │...\n 13471│    #[test]\n*13472│    fn select_endpoints_running_adapter_filters_dedups_sorts() {\n 13473│        use std::path::PathBuf;\n 13474│        let dir_a = PathBuf::from(\"/install/ep-a\");\n 13475│        let dir_b = PathBuf::from(\"/install/ep-b\");\n      │...\n 16425│    #[test]\n*16426│    fn a_failed_fido2_proof_spends_nothing_and_the_totp_fallback_still_admits() {\n 16427│        crate::test_home::with_home(|_| {\n 16428│            let (member, _admin) = provision_seal_subnet(\"sealnet\");\n 16429│            let broker = admit_test_broker();\n## msg.rs\n  767│    /// Whether the broker should reply a [`KIND_APPLIED`] ack for this op\n *768│    /// (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). `true` — the ack is sent (the\n  769│    /// `shellchan` one-at-a-time drain WAITS on it). `false` — NO ack frame is\n  770│    /// written back, so a FLOOD of operator input on one brain↔broker conn cannot\n  771│    /// fill the return direction and deadlock the per-conn handler (the rc/operator\n     │...\n  775│    /// brain's `input` (no `ack` field) still acks — today's behavior, N-1-safe.\n *776│    // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n  777│    #[serde(default = \"default_true\")]\n  778│    pub ack: bool,\n  779│}\n     │...\n  781│/// serde default for [`InputReq::ack`] — absent on the wire ⇒ `true` (the\n *782│/// pre-REQ-HAZARD-INPUT-ACK-BACKPRESSURE behavior: every `input` was acked).\n  783│fn default_true() -> bool {\n  784│    true\n  785│}\n     │...\n 2250│\n*2251│    // [unit->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] An `input` payload from an OLDER\n 2252│    // brain carries NO `ack` field on the wire (the field was added by this REQ).\n 2253│    // It MUST decode to `ack = true` so the old-brain path keeps acking exactly as\n 2254│    // it did before the fix (N-1 forward-compat: an old brain's `input` still gets\n     │...\n 2261│    #[test]\n*2262│    fn input_req_absent_ack_defaults_true_n_minus_1_safe() {\n 2263│        // The exact shape an N-1 brain emits: session + base64 + op_id, NO `ack`.\n 2264│        let n_minus_1 = json!({\n 2265│            \"session_id\": 7,\n     │...\n 2284│\n*2285│    // [unit->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] The new fire-and-forward path emits\n 2286│    // `ack = false` on the wire, and it round-trips faithfully (a present `false`\n 2287│    // is NOT clobbered back to the default `true`). This is the bit the broker's\n 2288│    // `dispatch_input` gate reads to decide whether to write the applied frame.\n     │...\n 2293│    #[test]\n*2294│    fn input_req_ack_false_round_trips_faithfully() {\n 2295│        let req = InputReq {\n 2296│            session_id: 3,\n 2297│            data_b64: encode_bytes(b\"PASTE\"),\n## brain.rs\n  918│    /// conn — use [`Brain::send_effect_no_ack`] to avoid the ack-backpressure\n *919│    /// deadlock (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\n  920│    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n  921│    pub fn send_effect(&mut self, op: MintedOp, bytes: &[u8]) -> io::Result<()> {\n  922│        self.send_effect_inner(op, bytes, true)\n     │...\n  925│    /// Inject input idempotently like [`Brain::send_effect`] but request NO\n *926│    /// [`KIND_APPLIED`] ack (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). The operator/rc\n  927│    /// attach path is fire-and-forward — `op_id` is for dedup only and the ack is\n  928│    /// discarded by `serve_attach` — and it can flood N input frames on ONE\n  929│    /// brain↔broker conn without returning to read. With no ack the broker's\n     │...\n  932│    /// broker still dedups by `(session, op_id)` at its applied-set.\n *933│    // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n *934│    pub fn send_effect_no_ack(&mut self, op: MintedOp, bytes: &[u8]) -> io::Result<()> {\n  935│        self.send_effect_inner(op, bytes, false)\n  936│    }\n  937│\n     │...\n  942│    /// uncompilable. The broker's `KIND_APPLIED` still echoes the bare `seq`.\n *943│    fn send_effect_inner(&mut self, op: MintedOp, bytes: &[u8], ack: bool) -> io::Result<()> {\n  944│        let sid = self.require_session()?;\n  945│        self.send(\n  946│            KIND_INPUT,\n     │...\n 2035│    // [impl->REQ-RC-1]\n*2036│    pub fn net_dial_loopback(&mut self) -> io::Result<NetDialed> {\n 2037│        self.send(KIND_NET_DIAL_LOOPBACK, serde_json::Value::Null)?;\n 2038│        // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n 2039│        let deadline = self.call_deadline();\n     │...\n 2691│/// Whether `haystack` contains the contiguous `needle`.\n*2692│fn contains(haystack: &[u8], needle: &[u8]) -> bool {\n 2693│    if needle.is_empty() {\n 2694│        return true;\n 2695│    }\n     │...\n 3019│    #[test]\n*3020│    fn a_flagged_sync_frame_baselines_forward_never_backward_on_both_paths() {\n 3021│        let sid = 1u64;\n 3022│\n 3023│        // ── LEGACY strict path ──────────────────────────────────────────────"},"isError":false,"timestamp":1789528588667}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_IjiyRdl572OPgjtb6SjzdHiE|fc_0cf46cef591a107c016aaa0a16855c87d280c83b4eb9f04f5a","toolName":"glob","startedAt":"2026-09-16T03:16:38.614Z","args":{"path":".worktrees/49-267-refresh-discriminator/**/*LIFECYCLE*;.worktrees/49-267-refresh-discriminator/**/CONTEXT.md"},"intent":"Locating lifecycle reference documents"},"id":"109b7a41","parentId":"cd3f6ce6","timestamp":"2026-09-16T03:16:38.615Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_0jFrTARxFnkWkw0fPGvH97hd|fc_0cf46cef591a107c016aaa0a16857c87d29acda01495073fd9","toolName":"read","startedAt":"2026-09-16T03:16:38.615Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/support/refresh_freeze/rig.py:531-548,600-620"},"intent":"Reading rig observable contract"},"id":"02529074","parentId":"109b7a41","timestamp":"2026-09-16T03:16:38.615Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Iw8X2T3Vm9j2iib0Q1Roce1h|fc_0cf46cef591a107c016aaa0a16858c87d28f238b7b23cca7e6","toolName":"grep","startedAt":"2026-09-16T03:16:38.615Z","args":{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/effects.rs;.worktrees/49-267-refresh-discriminator/crates/spt-d…"},"intent":"Locating ack behavior integration evidence"},"id":"2e77315f","parentId":"02529074","timestamp":"2026-09-16T03:16:38.615Z"}
{"type":"message","id":"cea7a361","parentId":"2e77315f","timestamp":"2026-09-16T03:16:38.615Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":""},{"type":"toolCall","id":"call_IjiyRdl572OPgjtb6SjzdHiE|fc_0cf46cef591a107c016aaa0a16855c87d280c83b4eb9f04f5a","name":"glob","arguments":{"i":"Locating lifecycle reference documents","path":".worktrees/49-267-refresh-discriminator/**/*LIFECYCLE*;.worktrees/49-267-refresh-discriminator/**/CONTEXT.md","hidden":true,"gitignore":false,"limit":30},"intent":"Locating lifecycle reference documents"},{"type":"toolCall","id":"call_0jFrTARxFnkWkw0fPGvH97hd|fc_0cf46cef591a107c016aaa0a16857c87d29acda01495073fd9","name":"read","arguments":{"i":"Reading rig observable contract","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/support/refresh_freeze/rig.py:531-548,600-620"},"intent":"Reading rig observable contract"},{"type":"toolCall","id":"call_Iw8X2T3Vm9j2iib0Q1Roce1h|fc_0cf46cef591a107c016aaa0a16858c87d28f238b7b23cca7e6","name":"grep","arguments":{"i":"Locating ack behavior integration evidence","pattern":"ack.*false|ack_emitted|no_ack|REQ-HAZARD-INPUT-ACK-BACKPRESSURE","path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/effects.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests","case":true,"gitignore":false,"skip":null},"intent":"Locating ack behavior integration evidence"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":7418,"output":265,"cacheRead":77440,"cacheWrite":0,"totalTokens":85123,"reasoningTokens":9,"cost":{"input":0.07418000000000001,"output":0.013250000000000001,"cacheRead":0.07744,"cacheWrite":0,"total":0.16487000000000002}},"stopReason":"toolUse","timestamp":1789528588748,"responseId":"resp_0cf46cef591a107c016aaa0a0cfff087d294ca20ccb90e6327","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cf46cef591a107c016aaa0a0e9b3087d2b365b420a4869b11","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqgoWyhmjXOa1WuO-EBBiWMtkRnlM_v0DxVXW7TqYFfPtSM69sOd_s7G2U4cU7TjhSXtIwFI0-d7VBIKXtkxgcDaVJ76A3NdgFibddyMgLdu6Gmro98go7WKpV1Obf0g-9gt392MLMVMlUrymtWpwdynEU7EUDQ7KxRDW5M-jaBhU1wtjLu3u_N21u8zhr9ve4ii5HWqOtyyAJrsGbJquRgLuAzCueItsfVZUsdOksdAwF9MHZa05_5fT_5EmnWntBjJ_5G6TvR3xWlPsBWkV6aAu3kIb8qIaB-HOVLb8A1J0y8awp_ZVo0OaG2JuK5w2QkGqCyq-39nirKddLtRa_SO1xntpCwJjo2ZmvGFUKLAfar2lddl4coZOJxrOC1XTaRlMJJqg0F8jMrYr5FV_4HBAR1fsWnIaXCJQzLaqrZOUcxGyk4RK7q4mED-fL-K6KZ0DF26qHxY8kDeTEMJiUGeFxiG4iC9RUKkj1s2ThLNjdJYBRjgZwv0v5P7GOeczjYOI8uJGO4ry9kf9UGY7L1BNtI4NTazFiRn3T7jU32RFCHpi9fR9bqQ0LnKrXqnZiQYacmXpLjNHXOp1x719Ngotz-VJMWA5lwTQ4iVqVjV69Ace7oidRTcAzAwZWre0sxvE352tDjmb3Oca0SWRCqJeCfOuJ8IGy6kGji_Uw2sNr-G6PLkrqE_V78pdMoAS69YVWWE-ZH5cjr7mjk3GXmyDIAvlERtepktsoQ9avuxPGzGoUcE9Xc4RwkhgmBl9RBxKEu3BKnBynztrd9AOM1XIMxO6rJrlkv_0LMRrxam6pvP-AhaLFdSMdy5nK5IdJrkWJJcSaFSPrvCIXkF1g-UOl8hTzVvcegjk6tK-86pXSggSEBwU4od1CcVulv9QhDZTBces1LFrKDDgFBY8aHQtS95y-IAccmvaA_HylpkVX43YyyDGtTmzov3Aeik2yDzWaKR5da82pA6mTRlOq3HGlbFC1OyweAUAXYxUDDBJ6A0CGh_AQAaFTWKPKAVDDPzpN5qR53oisS0Ty5y_9JemI1adtQUykfeBXg9BPm3ovvECQPkWANgI7W3_4kHWLgqBjFixBIlvOtF2T9V4rlLO26P-DNJzCMefJhiD--MgNzghZqXxqTlO6gAbIFQSsbwZcg75CibY8Fd-q2QMgt9iCCoRnK46xjknWNVT7xMdKC8BZJvObB5aXcQ2LggYrSGnjGu1h2lWXj1yt57XsJ1aW5XYA3hP5N81FRhgqy4RVgH3hHsChZY1iiQ3NL8gS1qyzuqlLlYW1xOWW3g6R-omM010nIEm-58aEKFpni0JcXs=","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"summary":[],"metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa0a16855c87d280c83b4eb9f04f5a","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating lifecycle reference documents\",\"path\":\".worktrees/49-267-refresh-discriminator/**/*LIFECYCLE*;.worktrees/49-267-refresh-discriminator/**/CONTEXT.md\",\"hidden\":true,\"gitignore\":false,\"limit\":30}","call_id":"call_IjiyRdl572OPgjtb6SjzdHiE","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"glob","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa0a16857c87d29acda01495073fd9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading rig observable contract\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/support/refresh_freeze/rig.py:531-548,600-620\"}","call_id":"call_0jFrTARxFnkWkw0fPGvH97hd","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"read","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}},{"id":"fc_0cf46cef591a107c016aaa0a16858c87d28f238b7b23cca7e6","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating ack behavior integration evidence\",\"pattern\":\"ack.*false|ack_emitted|no_ack|REQ-HAZARD-INPUT-ACK-BACKPRESSURE\",\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/effects.rs;.worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_Iw8X2T3Vm9j2iib0Q1Roce1h","internal_chat_message_metadata_passthrough":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"grep","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}}]},"duration":9863.128499999642,"ttft":1880.2258000001311,"completedAt":1789528598612,"contextSnapshot":{"promptTokens":84858,"nonMessageTokens":10304,"compactionEpoch":0}}}
{"type":"message","id":"b13c5255","parentId":"cea7a361","timestamp":"2026-09-16T03:16:38.644Z","message":{"role":"toolResult","toolCallId":"call_0jFrTARxFnkWkw0fPGvH97hd|fc_0cf46cef591a107c016aaa0a16857c87d29acda01495073fd9","toolName":"read","content":[{"type":"text","text":"    def observe(self, phase, seconds):\n        self.phase = phase\n        begin = time.monotonic()\n        require(self.remaining() >= seconds, f\"insufficient budget for full {phase}\")\n        end = begin + seconds\n        # Leave the last ACK bound inside the observation for the final tag to settle.\n        send_end = end - self.config[\"ack_bound_ms\"] / 1000\n        index, next_send = 0, begin\n        self.event(\"PHASE_START\", seconds=seconds, send_until_mono=send_end)\n        while time.monotonic() < end:\n            require(not self.reader_errors, f\"RC reader failed: {self.reader_errors}\")\n            if time.monotonic() >= next_send and next_send < send_end:\n                self.send(index)\n                index += 1\n                next_send = begin + index\n            self.ledger()\n            time.sleep(min(.025, max(0, end - time.monotonic())))\n        self.event(\"PHASE_END\", actual_s=time.monotonic() - begin)\n        return self.samples(phase)\n…\n        recovery_deadline = self.refresh[\"started_mono\"] + self.config[\"recovery_bound_ms\"] / 1000\n        timely_candidates = [row for row in self.post if row[\"timely\"]\n                             and row[\"completed_mono\"] <= recovery_deadline]\n        candidate = next((row for row in timely_candidates\n                          if all(later[\"timely\"] for later in self.post\n                                 if later[\"sent_mono\"] >= row[\"sent_mono\"])), None)\n        recovered = bool(timely_candidates)\n        sustained = candidate is not None\n        bounded = all(row[\"complete\"] and (row[\"timely\"] or row[\"completed_mono\"] <= recovery_deadline)\n                      for row in self.post)\n        new_brain = self.refresh[\"replacement_brain\"]\n        real_refresh = ((new_brain[\"pid\"], new_brain[\"birth\"]) !=\n                        (self.initial_brain[\"pid\"], self.initial_brain[\"birth\"])\n                        and any(\"BRAIN_UPDATE_RESTART:\" in line for line in after)\n                        and any(\"BRAIN_UP:\" in line for line in after))\n        checks = {\"real_refresh\": real_refresh, \"baseline_12_timely_tags\": sum(row[\"timely\"] for row in self.baseline) >= 12,\n                  \"ack_recovered_within_bound\": recovered, \"sustained_timely_ack_and_state\": sustained,\n                  \"all_post_tags_accounted_within_bound\": bool(self.post) and bounded,\n                  \"zero_replacement_write_poison\": not poison,\n                  \"controller_closures_detach_only\": bool(closures) and not bad_closures,\n                  \"reader_healthy\": not self.reader_errors}"}],"details":{"displayContent":{"text":"    def observe(self, phase, seconds):\n        self.phase = phase\n        begin = time.monotonic()\n        require(self.remaining() >= seconds, f\"insufficient budget for full {phase}\")\n        end = begin + seconds\n        # Leave the last ACK bound inside the observation for the final tag to settle.\n        send_end = end - self.config[\"ack_bound_ms\"] / 1000\n        index, next_send = 0, begin\n        self.event(\"PHASE_START\", seconds=seconds, send_until_mono=send_end)\n        while time.monotonic() < end:\n            require(not self.reader_errors, f\"RC reader failed: {self.reader_errors}\")\n            if time.monotonic() >= next_send and next_send < send_end:\n                self.send(index)\n                index += 1\n                next_send = begin + index\n            self.ledger()\n            time.sleep(min(.025, max(0, end - time.monotonic())))\n        self.event(\"PHASE_END\", actual_s=time.monotonic() - begin)\n        return self.samples(phase)\n…\n        recovery_deadline = self.refresh[\"started_mono\"] + self.config[\"recovery_bound_ms\"] / 1000\n        timely_candidates = [row for row in self.post if row[\"timely\"]\n                             and row[\"completed_mono\"] <= recovery_deadline]\n        candidate = next((row for row in timely_candidates\n                          if all(later[\"timely\"] for later in self.post\n                                 if later[\"sent_mono\"] >= row[\"sent_mono\"])), None)\n        recovered = bool(timely_candidates)\n        sustained = candidate is not None\n        bounded = all(row[\"complete\"] and (row[\"timely\"] or row[\"completed_mono\"] <= recovery_deadline)\n                      for row in self.post)\n        new_brain = self.refresh[\"replacement_brain\"]\n        real_refresh = ((new_brain[\"pid\"], new_brain[\"birth\"]) !=\n                        (self.initial_brain[\"pid\"], self.initial_brain[\"birth\"])\n                        and any(\"BRAIN_UPDATE_RESTART:\" in line for line in after)\n                        and any(\"BRAIN_UP:\" in line for line in after))\n        checks = {\"real_refresh\": real_refresh, \"baseline_12_timely_tags\": sum(row[\"timely\"] for row in self.baseline) >= 12,\n                  \"ack_recovered_within_bound\": recovered, \"sustained_timely_ack_and_state\": sustained,\n                  \"all_post_tags_accounted_within_bound\": bool(self.post) and bounded,\n                  \"zero_replacement_write_poison\": not poison,\n                  \"controller_closures_detach_only\": bool(closures) and not bad_closures,\n                  \"reader_healthy\": not self.reader_errors}","startLine":531,"lineNumbers":[531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,null,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620]},"fileSize":42983,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon\\tests\\support\\refresh_freeze\\rig.py"}}},"isError":false,"timestamp":1789528598643}}
{"type":"message","id":"8eed32fa","parentId":"b13c5255","timestamp":"2026-09-16T03:16:38.649Z","message":{"role":"toolResult","toolCallId":"call_Iw8X2T3Vm9j2iib0Q1Roce1h|fc_0cf46cef591a107c016aaa0a16858c87d28f238b7b23cca7e6","toolName":"grep","content":[{"type":"text","text":"# .worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/\n## brain_decouple.rs\n 571|        // Wait for main's go, then fire the burst trigger. `\\r\\n` satisfies the first\n*572|        // Read-Host; ack=false (the rc/operator fire-and-forward path).\n 573|        let _ = go_rx.recv_timeout(Duration::from_secs(15));\n 574|        send(\n 575|            &mut r,\n...\n 581|                minter: None,\n*582|                ack: false,\n 583|            })\n 584|            .unwrap(),\n 585|        );\n## broker.rs\n 189|/// binary declared (this echo child), idle delivery has no working binary → the\n*190|/// broker acks `delivered=false` and writes NOTHING to the PTY (the caller spools;\n 191|/// raw `payload+\\r` was removed as a delivery path — it did not submit on a modern\n 192|/// TUI, the silent pseudo-delivery that masked F-019). An inject for an UNKNOWN\n 193|/// endpoint likewise replies `delivered=false` and writes nothing. (The happy-path\n...\n 267|        Some(false),\n*268|        \"a hosted session with no translation binary acks delivered=false (spool, not raw inject)\"\n 269|    );\n 270|    assert!(\n 271|        !String::from_utf8_lossy(&out).contains(\"ENDPOINT-INJECT-BETA\"),\n...\n 301|    }\n*302|    assert_eq!(miss, Some(false), \"unknown endpoint acks delivered=false\");\n 303|    assert!(\n 304|        !String::from_utf8_lossy(&out).contains(\"SHOULD-NOT-APPEAR\"),\n 305|        \"a miss writes nothing to any PTY\"\n...\n 536|/// (REQ-SEND-SPT-HOSTED + REQ-HAZARD-IDLE-SILENT-NONDELIVERY, ADR-0022 amendment):\n*537|/// the broker acks `delivered=false` and writes NOTHING to the PTY (raw inject was\n 538|/// removed; a bare `payload+\\r` does not submit on a modern TUI). The whole\n 539|/// \"bringup → SPT_ENDPOINT_ID set → send → spool\" chain through a REAL broker\n 540|/// and PTY. (Leg 2 reads the SENDER's ack, not a PTY echo — the echo never comes\n...\n 637|    // endpoint-keyed inject to a session with NO translation binary SPOOLS — the\n*638|    // broker acks delivered=false and writes NOTHING to the PTY (raw inject removed;\n 639|    // a bare payload+\\r does not submit on a modern TUI). Read the\n 640|    // KIND_ENDPOINT_INJECTED ack on the SENDER conn — it DOES arrive — and assert\n 641|    // delivered=false; do NOT spin on a PTY echo that never comes (the hang the old\n...\n 758|\n*759|/// REQ-HAZARD-INPUT-ACK-BACKPRESSURE — the ACK GATE, the crux unit. Against a\n 760|/// REAL broker hosting a REAL spawned PTY session, an `op_id`-carrying input with\n*761|/// `ack = false` produces NO `KIND_APPLIED` frame, while one with `ack = true`\n 762|/// does. This is the broker leg of the fix: `dispatch_input` writes the applied\n 763|/// envelope ONLY when `req.ack`, so the fire-and-forward operator/rc flood never\n 764|/// fills the broker→brain return direction (the deadlock the whole REQ cures).\n...\n 775|/// first → `first_applied_op == NO_ACK_OP` → the assert below flips to RED.\n*776|// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n 777|fn ack_gate_test() {\n 778|    const NO_ACK_OP: u64 = 100;\n 779|    const PROBE_OP: u64 = 200;\n...\n 798|\n*799|        // (1) The fire-and-forward write: ack=false. The byte must still reach the\n 800|        //     PTY (delivery is unaffected), but NO applied frame may come back for it.\n 801|        send_input_op(&mut conn, sid, NO_ACK_OP, b\"NOACK-LINE\\r\", false);\n 802|        // (2) The known-acked probe: ack=true. Its applied frame is the\n...\n 808|        let mut first_applied_op: Option<u64> = None;\n*809|        let mut noack_echoed = false;\n 810|        let mut probe_echoed = false;\n 811|        for _ in 0..4000 {\n 812|            if first_applied_op == Some(PROBE_OP) && noack_echoed && probe_echoed {\n...\n 856|        Some(NO_ACK_OP),\n*857|        \"an ack=false input must NOT produce a KIND_APPLIED frame (the gate); a frame \\\n 858|         on the return direction for every flooded input is exactly the deadlock \\\n*859|         REQ-HAZARD-INPUT-ACK-BACKPRESSURE cures\"\n 860|    );\n 861|    // Delivery is unaffected by the ack flag — both lines reached the PTY.\n 862|    assert!(\n 863|        noack_echoed,\n*864|        \"the ack=false input must still be DELIVERED to the PTY (only the ack frame \\\n 865|         is suppressed)\"\n 866|    );\n 867|    assert!(probe_echoed, \"the ack=true input must also reach the PTY\");\n...\n 870|#[test]\n*871|fn ack_false_input_suppresses_applied_frame() {\n 872|    ack_gate_test();\n 873|}\n 874|\n*875|/// REQ-HAZARD-INPUT-ACK-BACKPRESSURE — EXACTLY-ONCE on the NO-ACK path. The fix\n 876|/// must not weaken exactly-once: dedup happens at the broker's applied-set\n 877|/// (`apply_once`) regardless of whether the ack frame is emitted. Replay the SAME\n*878|/// `(session, op_id)` with `ack = false` MORE THAN ONCE; the byte is written to\n 879|/// the PTY exactly once (a second apply_once for the key is deduped, the PtyWrite\n 880|/// effect is not re-run), and the broker-owned applied-set holds the key once.\n 881|///\n...\n 892|/// so the broker anchor is the load-bearing assertion, corroborated by receipt.\n*893|// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n*894|fn exactly_once_no_ack_test() {\n 895|    const OP: u64 = 77;\n 896|\n 897|    let name = unique_name();\n...\n 915|\n*916|        // The SAME (session, op_id) sent THREE times, all ack=false (the fire-and-\n 917|        // forward replay an rc re-drive after a hiccup would produce). The broker\n 918|        // dedups by the key; the PtyWrite runs only on the first apply_once.\n 919|        let line = b\"ONCE-ONLY-LINE\\r\";\n...\n 921|            send_input_op(&mut conn, sid, OP, line, false);\n*922|            // No applied frame comes back (ack=false), so don't wait for one — just\n 923|            // give the broker a beat to process each frame in order.\n 924|            thread::sleep(Duration::from_millis(40));\n 925|        }\n## conn_blackhole_lifecycle.rs\n 354|        // Wait for main's go, then fire the burst trigger (`\\r\\n` satisfies the\n*355|        // first Read-Host; ack=false = the rc/operator fire-and-forward path).\n 356|        let _ = go_rx.recv_timeout(Duration::from_secs(15));\n 357|        send(\n 358|            &mut r,\n...\n 364|                minter: None,\n*365|                ack: false,\n 366|            })\n 367|            .unwrap(),\n 368|        );\n## controller_lease.rs\n 580|        line.push(b'\\r');\n*581|        a.send_effect_no_ack(MintedOp::new(Minter::Rc, 100 + i), &line)\n 582|            .expect(\"flood input\");\n 583|    }\n 584|    // Let the echo output pile into A's unread conn until the writer wedges.\n## inject_control_wedge.rs\n 475|    let inject_acked = ack.is_some();\n*476|    let spooled = ack == Some(false);\n 477|\n 478|    // Broker process still alive on a fresh connection.\n 479|    let broker_alive = probe(\n...\n 488|         broker_alive={broker_alive} \\\n*489|         (acked promptly + delivered=false → the 4 MiB spooled, never reached write_input, \\\n 490|         never parked the dispatch thread) ===\"\n 491|    );\n 492|\n...\n 2967|    // as the CONTAINMENT asserts failing or MORE backpressure — never as fast-drain, so\n*2968|    // backpressured=false is directionally INCOMPATIBLE with the hazard it guards.\n 2969|    //\n 2970|    // So: HARD-assert the reproduction only when SPT_FORKPTY_BACKPRESSURE_HARD is set\n 2971|    // (the gravity-linux acceptance / manual run keeps the deterministic reproduction);\n## input_ack_deadlock.rs\n*1|//! v0.13.0 P1b (REQ-HAZARD-INPUT-ACK-BACKPRESSURE) — the KEYSTONE, repro-first.\n 2|//!\n 3|//! ## The bug this gate locks down (the applied-ack full-duplex deadlock)\n 4|//!\n...\n 18|//! POST-FIX, the fire-and-forward operator path (`serve_attach`'s controller arm)\n*19|//! calls `Brain::send_effect_no_ack` (ack=false) → `dispatch_input` emits NO\n 20|//! applied frame → the broker's per-conn handler NEVER writes back while servicing\n 21|//! the flood → the return direction can't fill → the handler always drains → no\n 22|//! deadlock. Exactly-once is preserved (dedup at the broker's applied-set,\n...\n 267|\n*268|/// REQ-HAZARD-INPUT-ACK-BACKPRESSURE — the keystone gate. A FLOOD of N=64 operator\n 269|/// `Input` records is driven through the REAL `serve_attach` CONTROLLER path on ONE\n*270|/// conn (the rc → net-stream → serve_attach → send_effect_no_ack route), exceeding\n 271|/// the ~10-frame IPC pipe buffer. Post-fix the broker emits NO applied frame for\n 272|/// the fire-and-forward path, so its per-conn handler never wedges; a CONCURRENT\n 273|/// viewer attach still OPENS, receives a decodable repaint, and receives post-flood\n...\n 278|/// watchdog so a regression surfaces as a clean FAILURE, never a hang.\n*279|// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n 280|#[test]\n 281|fn input_flood_through_serve_attach_does_not_deadlock_broker() {\n 282|    let _home = init_home();\n...\n 309|    //    The target's serve_attach reads them in one decoder.push() batch and calls\n*310|    //    send_effect_no_ack per record (post-fix). The child is now silent: no\n 311|    //    concurrent output can confound the applied-ack deadlock substrate.\n 312|    //\n 313|    //    This whole leg runs on its own thread behind a result channel so a wedge\n...\n 361|        // CONTROLLER intent: this is the path the fix changed (the operator/rc\n*362|        // fire-and-forward route → send_effect_no_ack).\n 363|        let stream = match request_attach(\n 364|            &mut operator,\n 365|            dialed.conn_id,\n...\n 386|        // The target side serves the inbound attach (the REAL serve_attach pump —\n*387|        // its controller arm drives send_effect_no_ack for every flooded record).\n 388|        let mut target = connect_retry(&flood_name);\n 389|        let (stream_a, origin) = match wait_for_stream(&mut target) {\n 390|            Some(s) => s,\n...\n 673|         the immediate write error and operation; HelperDisconnected means the flood \\\n*674|         helper exited without reporting (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\"\n 675|    );\n 676|    // (2) The broker PROCESS stays alive on a fresh conn during/after the flood.\n 677|    assert!(\n...\n 692|         the broker's session table / dispatch must not be globally frozen \\\n*693|         (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\"\n 694|    );\n 695|    // (4) The post-flood VIEWER is serviced, receives a non-empty decodable cold\n 696|    //     repaint, then receives ACKDL_LIVE_OUTPUT as live fan-out after its\n## refresh_freeze.rs\n 36|// [int->REQ-DAEMON-REFRESH]\n*37|// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n 38|#[test]\n 39|fn refresh_resumes_bounded_input_ack_without_replacement_poison() {\n 40|    let binary = std::env::var_os(\"SPT_REFRESH_RIG_SPT\")\n## twohost_web.rs\n 637|    let mut sent = 0usize;\n*638|    let mut acked = false;\n 639|    while Instant::now() < deadline {\n 640|        match probe_socket.send_to(PROBE_MAGIC, target) {\n 641|            Ok(_) => sent += 1,\n\nSkipped missing paths: .worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input.rs, .worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/effects.rs"}],"details":{"scopePath":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\crates\\spt-daemon\\tests","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":40,"fileCount":8,"files":[".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/brain_decouple.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/broker.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/conn_blackhole_lifecycle.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/controller_lease.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/inject_control_wedge.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/twohost_web.rs"],"fileMatches":[{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/brain_decouple.rs","count":2},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/broker.rs","count":20},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","count":2},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/controller_lease.rs","count":1},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/inject_control_wedge.rs","count":3},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs","count":10},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs","count":1},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/twohost_web.rs","count":1}],"truncated":true,"perFileLimitReached":20,"displayContent":"# .worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/\n## brain_decouple.rs\n 571│        // Wait for main's go, then fire the burst trigger. `\\r\\n` satisfies the first\n*572│        // Read-Host; ack=false (the rc/operator fire-and-forward path).\n 573│        let _ = go_rx.recv_timeout(Duration::from_secs(15));\n 574│        send(\n 575│            &mut r,\n    │...\n 581│                minter: None,\n*582│                ack: false,\n 583│            })\n 584│            .unwrap(),\n 585│        );\n## broker.rs\n 189│/// binary declared (this echo child), idle delivery has no working binary → the\n*190│/// broker acks `delivered=false` and writes NOTHING to the PTY (the caller spools;\n 191│/// raw `payload+\\r` was removed as a delivery path — it did not submit on a modern\n 192│/// TUI, the silent pseudo-delivery that masked F-019). An inject for an UNKNOWN\n 193│/// endpoint likewise replies `delivered=false` and writes nothing. (The happy-path\n    │...\n 267│        Some(false),\n*268│        \"a hosted session with no translation binary acks delivered=false (spool, not raw inject)\"\n 269│    );\n 270│    assert!(\n 271│        !String::from_utf8_lossy(&out).contains(\"ENDPOINT-INJECT-BETA\"),\n    │...\n 301│    }\n*302│    assert_eq!(miss, Some(false), \"unknown endpoint acks delivered=false\");\n 303│    assert!(\n 304│        !String::from_utf8_lossy(&out).contains(\"SHOULD-NOT-APPEAR\"),\n 305│        \"a miss writes nothing to any PTY\"\n    │...\n 536│/// (REQ-SEND-SPT-HOSTED + REQ-HAZARD-IDLE-SILENT-NONDELIVERY, ADR-0022 amendment):\n*537│/// the broker acks `delivered=false` and writes NOTHING to the PTY (raw inject was\n 538│/// removed; a bare `payload+\\r` does not submit on a modern TUI). The whole\n 539│/// \"bringup → SPT_ENDPOINT_ID set → send → spool\" chain through a REAL broker\n 540│/// and PTY. (Leg 2 reads the SENDER's ack, not a PTY echo — the echo never comes\n    │...\n 637│    // endpoint-keyed inject to a session with NO translation binary SPOOLS — the\n*638│    // broker acks delivered=false and writes NOTHING to the PTY (raw inject removed;\n 639│    // a bare payload+\\r does not submit on a modern TUI). Read the\n 640│    // KIND_ENDPOINT_INJECTED ack on the SENDER conn — it DOES arrive — and assert\n 641│    // delivered=false; do NOT spin on a PTY echo that never comes (the hang the old\n    │...\n 758│\n*759│/// REQ-HAZARD-INPUT-ACK-BACKPRESSURE — the ACK GATE, the crux unit. Against a\n 760│/// REAL broker hosting a REAL spawned PTY session, an `op_id`-carrying input with\n*761│/// `ack = false` produces NO `KIND_APPLIED` frame, while one with `ack = true`\n 762│/// does. This is the broker leg of the fix: `dispatch_input` writes the applied\n 763│/// envelope ONLY when `req.ack`, so the fire-and-forward operator/rc flood never\n 764│/// fills the broker→brain return direction (the deadlock the whole REQ cures).\n    │...\n 775│/// first → `first_applied_op == NO_ACK_OP` → the assert below flips to RED.\n*776│// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n 777│fn ack_gate_test() {\n 778│    const NO_ACK_OP: u64 = 100;\n 779│    const PROBE_OP: u64 = 200;\n    │...\n 798│\n*799│        // (1) The fire-and-forward write: ack=false. The byte must still reach the\n 800│        //     PTY (delivery is unaffected), but NO applied frame may come back for it.\n 801│        send_input_op(&mut conn, sid, NO_ACK_OP, b\"NOACK-LINE\\r\", false);\n 802│        // (2) The known-acked probe: ack=true. Its applied frame is the\n    │...\n 808│        let mut first_applied_op: Option<u64> = None;\n*809│        let mut noack_echoed = false;\n 810│        let mut probe_echoed = false;\n 811│        for _ in 0..4000 {\n 812│            if first_applied_op == Some(PROBE_OP) && noack_echoed && probe_echoed {\n    │...\n 856│        Some(NO_ACK_OP),\n*857│        \"an ack=false input must NOT produce a KIND_APPLIED frame (the gate); a frame \\\n 858│         on the return direction for every flooded input is exactly the deadlock \\\n*859│         REQ-HAZARD-INPUT-ACK-BACKPRESSURE cures\"\n 860│    );\n 861│    // Delivery is unaffected by the ack flag — both lines reached the PTY.\n 862│    assert!(\n 863│        noack_echoed,\n*864│        \"the ack=false input must still be DELIVERED to the PTY (only the ack frame \\\n 865│         is suppressed)\"\n 866│    );\n 867│    assert!(probe_echoed, \"the ack=true input must also reach the PTY\");\n    │...\n 870│#[test]\n*871│fn ack_false_input_suppresses_applied_frame() {\n 872│    ack_gate_test();\n 873│}\n 874│\n*875│/// REQ-HAZARD-INPUT-ACK-BACKPRESSURE — EXACTLY-ONCE on the NO-ACK path. The fix\n 876│/// must not weaken exactly-once: dedup happens at the broker's applied-set\n 877│/// (`apply_once`) regardless of whether the ack frame is emitted. Replay the SAME\n*878│/// `(session, op_id)` with `ack = false` MORE THAN ONCE; the byte is written to\n 879│/// the PTY exactly once (a second apply_once for the key is deduped, the PtyWrite\n 880│/// effect is not re-run), and the broker-owned applied-set holds the key once.\n 881│///\n    │...\n 892│/// so the broker anchor is the load-bearing assertion, corroborated by receipt.\n*893│// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n*894│fn exactly_once_no_ack_test() {\n 895│    const OP: u64 = 77;\n 896│\n 897│    let name = unique_name();\n    │...\n 915│\n*916│        // The SAME (session, op_id) sent THREE times, all ack=false (the fire-and-\n 917│        // forward replay an rc re-drive after a hiccup would produce). The broker\n 918│        // dedups by the key; the PtyWrite runs only on the first apply_once.\n 919│        let line = b\"ONCE-ONLY-LINE\\r\";\n    │...\n 921│            send_input_op(&mut conn, sid, OP, line, false);\n*922│            // No applied frame comes back (ack=false), so don't wait for one — just\n 923│            // give the broker a beat to process each frame in order.\n 924│            thread::sleep(Duration::from_millis(40));\n 925│        }\n## conn_blackhole_lifecycle.rs\n 354│        // Wait for main's go, then fire the burst trigger (`\\r\\n` satisfies the\n*355│        // first Read-Host; ack=false = the rc/operator fire-and-forward path).\n 356│        let _ = go_rx.recv_timeout(Duration::from_secs(15));\n 357│        send(\n 358│            &mut r,\n    │...\n 364│                minter: None,\n*365│                ack: false,\n 366│            })\n 367│            .unwrap(),\n 368│        );\n## controller_lease.rs\n 580│        line.push(b'\\r');\n*581│        a.send_effect_no_ack(MintedOp::new(Minter::Rc, 100 + i), &line)\n 582│            .expect(\"flood input\");\n 583│    }\n 584│    // Let the echo output pile into A's unread conn until the writer wedges.\n## inject_control_wedge.rs\n  475│    let inject_acked = ack.is_some();\n *476│    let spooled = ack == Some(false);\n  477│\n  478│    // Broker process still alive on a fresh connection.\n  479│    let broker_alive = probe(\n     │...\n  488│         broker_alive={broker_alive} \\\n *489│         (acked promptly + delivered=false → the 4 MiB spooled, never reached write_input, \\\n  490│         never parked the dispatch thread) ===\"\n  491│    );\n  492│\n     │...\n 2967│    // as the CONTAINMENT asserts failing or MORE backpressure — never as fast-drain, so\n*2968│    // backpressured=false is directionally INCOMPATIBLE with the hazard it guards.\n 2969│    //\n 2970│    // So: HARD-assert the reproduction only when SPT_FORKPTY_BACKPRESSURE_HARD is set\n 2971│    // (the gravity-linux acceptance / manual run keeps the deterministic reproduction);\n## input_ack_deadlock.rs\n  *1│//! v0.13.0 P1b (REQ-HAZARD-INPUT-ACK-BACKPRESSURE) — the KEYSTONE, repro-first.\n   2│//!\n   3│//! ## The bug this gate locks down (the applied-ack full-duplex deadlock)\n   4│//!\n    │...\n  18│//! POST-FIX, the fire-and-forward operator path (`serve_attach`'s controller arm)\n *19│//! calls `Brain::send_effect_no_ack` (ack=false) → `dispatch_input` emits NO\n  20│//! applied frame → the broker's per-conn handler NEVER writes back while servicing\n  21│//! the flood → the return direction can't fill → the handler always drains → no\n  22│//! deadlock. Exactly-once is preserved (dedup at the broker's applied-set,\n    │...\n 267│\n*268│/// REQ-HAZARD-INPUT-ACK-BACKPRESSURE — the keystone gate. A FLOOD of N=64 operator\n 269│/// `Input` records is driven through the REAL `serve_attach` CONTROLLER path on ONE\n*270│/// conn (the rc → net-stream → serve_attach → send_effect_no_ack route), exceeding\n 271│/// the ~10-frame IPC pipe buffer. Post-fix the broker emits NO applied frame for\n 272│/// the fire-and-forward path, so its per-conn handler never wedges; a CONCURRENT\n 273│/// viewer attach still OPENS, receives a decodable repaint, and receives post-flood\n    │...\n 278│/// watchdog so a regression surfaces as a clean FAILURE, never a hang.\n*279│// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n 280│#[test]\n 281│fn input_flood_through_serve_attach_does_not_deadlock_broker() {\n 282│    let _home = init_home();\n    │...\n 309│    //    The target's serve_attach reads them in one decoder.push() batch and calls\n*310│    //    send_effect_no_ack per record (post-fix). The child is now silent: no\n 311│    //    concurrent output can confound the applied-ack deadlock substrate.\n 312│    //\n 313│    //    This whole leg runs on its own thread behind a result channel so a wedge\n    │...\n 361│        // CONTROLLER intent: this is the path the fix changed (the operator/rc\n*362│        // fire-and-forward route → send_effect_no_ack).\n 363│        let stream = match request_attach(\n 364│            &mut operator,\n 365│            dialed.conn_id,\n    │...\n 386│        // The target side serves the inbound attach (the REAL serve_attach pump —\n*387│        // its controller arm drives send_effect_no_ack for every flooded record).\n 388│        let mut target = connect_retry(&flood_name);\n 389│        let (stream_a, origin) = match wait_for_stream(&mut target) {\n 390│            Some(s) => s,\n    │...\n 673│         the immediate write error and operation; HelperDisconnected means the flood \\\n*674│         helper exited without reporting (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\"\n 675│    );\n 676│    // (2) The broker PROCESS stays alive on a fresh conn during/after the flood.\n 677│    assert!(\n    │...\n 692│         the broker's session table / dispatch must not be globally frozen \\\n*693│         (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\"\n 694│    );\n 695│    // (4) The post-flood VIEWER is serviced, receives a non-empty decodable cold\n 696│    //     repaint, then receives ACKDL_LIVE_OUTPUT as live fan-out after its\n## refresh_freeze.rs\n 36│// [int->REQ-DAEMON-REFRESH]\n*37│// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n 38│#[test]\n 39│fn refresh_resumes_bounded_input_ack_without_replacement_poison() {\n 40│    let binary = std::env::var_os(\"SPT_REFRESH_RIG_SPT\")\n## twohost_web.rs\n 637│    let mut sent = 0usize;\n*638│    let mut acked = false;\n 639│    while Instant::now() < deadline {\n 640│        match probe_socket.send_to(PROBE_MAGIC, target) {\n 641│            Ok(_) => sent += 1,","missingPaths":[".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input.rs",".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/effects.rs"]},"isError":false,"timestamp":1789528598648}}
{"type":"message","id":"6eba4076","parentId":"8eed32fa","timestamp":"2026-09-16T03:16:38.651Z","message":{"role":"toolResult","toolCallId":"call_IjiyRdl572OPgjtb6SjzdHiE|fc_0cf46cef591a107c016aaa0a16855c87d280c83b4eb9f04f5a","toolName":"glob","content":[{"type":"text","text":"# .worktrees/49-267-refresh-discriminator/\nV0.12.1-LIFECYCLE-JIT.md\nDAEMON-LIFECYCLE-VERIFICATION.md\nDAEMON-LIFECYCLE-PLAN.md\nDAEMON-LIFECYCLE-COMPLETION-PLAN.md\nCONTEXT.md\n## docs/\nNEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\nREGISTRY-LIFECYCLE-TRIAGE.md"}],"details":{"scopePath":".worktrees/49-267-refresh-discriminator/**/*LIFECYCLE*, .worktrees/49-267-refresh-discriminator/**/CONTEXT.md","fileCount":7,"files":[".worktrees/49-267-refresh-discriminator/docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md",".worktrees/49-267-refresh-discriminator/docs/REGISTRY-LIFECYCLE-TRIAGE.md",".worktrees/49-267-refresh-discriminator/V0.12.1-LIFECYCLE-JIT.md",".worktrees/49-267-refresh-discriminator/DAEMON-LIFECYCLE-VERIFICATION.md",".worktrees/49-267-refresh-discriminator/DAEMON-LIFECYCLE-PLAN.md",".worktrees/49-267-refresh-discriminator/DAEMON-LIFECYCLE-COMPLETION-PLAN.md",".worktrees/49-267-refresh-discriminator/CONTEXT.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1789528598650}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_F4Udpih4eHPuZlhx1RRFZHoP|fc_0cf46cef591a107c016aaa0a18667887d2a126b036de9558d9","toolName":"grep","startedAt":"2026-09-16T03:16:42.745Z","args":{"path":".worktrees/49-267-refresh-discriminator/CONTEXT.md"},"intent":"Locating canonical refresh section"},"id":"7df923c3","parentId":"6eba4076","timestamp":"2026-09-16T03:16:42.745Z"}
{"type":"message","id":"c3da1305","parentId":"7df923c3","timestamp":"2026-09-16T03:16:42.745Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_F4Udpih4eHPuZlhx1RRFZHoP|fc_0cf46cef591a107c016aaa0a18667887d2a126b036de9558d9","name":"grep","arguments":{"i":"Locating canonical refresh section","pattern":"REQ-DAEMON-REFRESH|daemon refresh|REQ-UPDATE-TRIAL-DRAIN-DRIVE|by:Some|by: Some","path":".worktrees/49-267-refresh-discriminator/CONTEXT.md","case":true,"gitignore":false,"skip":null},"intent":"Locating canonical refresh section"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":4215,"output":85,"cacheRead":84736,"cacheWrite":0,"totalTokens":89036,"cost":{"input":0.04215000000000001,"output":0.00425,"cacheRead":0.08473599999999999,"cacheWrite":0,"total":0.131136}},"stopReason":"toolUse","timestamp":1789528598687,"responseId":"resp_0cf46cef591a107c016aaa0a16f58c87d2a5c955d0a8b24923","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0cf46cef591a107c016aaa0a18667887d2a126b036de9558d9","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating canonical refresh section\",\"pattern\":\"REQ-DAEMON-REFRESH|daemon refresh|REQ-UPDATE-TRIAL-DRAIN-DRIVE|by:Some|by: Some\",\"path\":\".worktrees/49-267-refresh-discriminator/CONTEXT.md\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_F4Udpih4eHPuZlhx1RRFZHoP","internal_chat_message_metadata_passthrough":{"create_time":1789528599.197429,"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"grep","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}}]},"duration":4055.665300000459,"ttft":1760.5044999998063,"completedAt":1789528602744,"contextSnapshot":{"promptTokens":88951,"nonMessageTokens":10304,"compactionEpoch":0}}}
{"type":"message","id":"79ba82e3","parentId":"c3da1305","timestamp":"2026-09-16T03:16:42.770Z","message":{"role":"toolResult","toolCallId":"call_F4Udpih4eHPuZlhx1RRFZHoP|fc_0cf46cef591a107c016aaa0a18667887d2a126b036de9558d9","toolName":"grep","content":[{"type":"text","text":" 413|\n*414|**redispatch** (REDISPATCH-TRUTH, ADR-0038): the fresh brain's reconstruction of target-side attach workers over broker-held peer streams after a **brain cycle** (`spt daemon refresh` / `spt update` apply — the PTYs and QUIC streams survive; the brain-owned serve workers do not). Redispatch eligibility is **lifecycle-gated**: finished/terminal stream rows are retired from the dispatcher's enumeration (a fresh dispatcher never re-serves a terminal Attach — the KNOWN-HAZARDS 7.41 frozen-PTY steal/clear...\n 415|_Avoid_: conflating redispatch with msg dispatch or session-cursor resume; filtering replay by origin identity (the legitimate same-`by` successor re-take must keep working — lifecycle is the discriminator).\n 416|\n 417|<!-- [doc->REQ-RC-KEY-VT-TRANSLATE] -->\n...\n 689|\n*690|**daemon refresh (`spt daemon refresh`)** — restart the **brain** in place, no binary swap, broker and every held PTY untouched: the routine-update handoff path minus the swap. The recovery verb for wedged brain-held state (broken endpoint bringup, a downed hosted agent) that previously required a full daemon bounce. <!-- [doc->REQ-DAEMON-REFRESH] -->\n 691|\n 692|**project index** — a node's endpoint→project attribution is DERIVED state, held as a **persistent materialized index** (ADR-0037): `spt-store` owns the versioned format + read path (daemon-offline reads = last persisted snapshot); the **daemon is the sole single-flight writer** (load-at-start, ready-without-warm, background batched reconcile, atomic replace, coalesced event-driven invalidation keyed on branch-tip fingerprints, last-known-good on failure). Readers — list, picker, endpoint-info, hoo...\n 693|\n...\n 700|\n*701|**brain-trial promotion (readiness + drained)** — the broker supervises the swapped-in brain through a bounded readiness **trial** and *promotes* the new binary only when it both signals ready for its own generation **and** the OUTGOING generation's control plane has **drained** — the old brain's local (brain-owned) controller connection is closed or stall-evicted, never still holding blocked writes. A hard-killed prior generation leaves that connection **black-holed** (its hosted PTYs keep producing...\n 702|\n 703|**resume re-attach is view-only for non-driven sessions** — on respawn the new brain queries the broker for every hosted session and re-attaches to rebuild output-continuity cursors, but it re-attaches as a **viewer**, never a controller, for any session it does not itself drive (which is *all* of them today — the supervised daemon brain hosts no PTY sessions; spt-hosted PTYs are driven by the operator's attach or the endpoint's own loop). Re-attaching as a controller would seize the controller slot ...\n 704|\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":".worktrees/49-267-refresh-discriminator/CONTEXT.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\49-267-refresh-discriminator\\CONTEXT.md","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":3,"fileCount":1,"files":[".worktrees/49-267-refresh-discriminator/CONTEXT.md"],"fileMatches":[{"path":".worktrees/49-267-refresh-discriminator/CONTEXT.md","count":3}],"truncated":true,"displayContent":" 413│\n*414│**redispatch** (REDISPATCH-TRUTH, ADR-0038): the fresh brain's reconstruction of target-side attach workers over broker-held peer streams after a **brain cycle** (`spt daemon refresh` / `spt update` apply — the PTYs and QUIC streams survive; the brain-owned serve workers do not). Redispatch eligibility is **lifecycle-gated**: finished/terminal stream rows are retired from the dispatcher's enumeration (a fresh dispatcher never re-serves a terminal Attach — the KNOWN-HAZARDS 7.41 frozen-PTY steal/clear...\n 415│_Avoid_: conflating redispatch with msg dispatch or session-cursor resume; filtering replay by origin identity (the legitimate same-`by` successor re-take must keep working — lifecycle is the discriminator).\n 416│\n 417│<!-- [doc->REQ-RC-KEY-VT-TRANSLATE] -->\n    │...\n 689│\n*690│**daemon refresh (`spt daemon refresh`)** — restart the **brain** in place, no binary swap, broker and every held PTY untouched: the routine-update handoff path minus the swap. The recovery verb for wedged brain-held state (broken endpoint bringup, a downed hosted agent) that previously required a full daemon bounce. <!-- [doc->REQ-DAEMON-REFRESH] -->\n 691│\n 692│**project index** — a node's endpoint→project attribution is DERIVED state, held as a **persistent materialized index** (ADR-0037): `spt-store` owns the versioned format + read path (daemon-offline reads = last persisted snapshot); the **daemon is the sole single-flight writer** (load-at-start, ready-without-warm, background batched reconcile, atomic replace, coalesced event-driven invalidation keyed on branch-tip fingerprints, last-known-good on failure). Readers — list, picker, endpoint-info, hoo...\n 693│\n    │...\n 700│\n*701│**brain-trial promotion (readiness + drained)** — the broker supervises the swapped-in brain through a bounded readiness **trial** and *promotes* the new binary only when it both signals ready for its own generation **and** the OUTGOING generation's control plane has **drained** — the old brain's local (brain-owned) controller connection is closed or stall-evicted, never still holding blocked writes. A hard-killed prior generation leaves that connection **black-holed** (its hosted PTYs keep producing...\n 702│\n 703│**resume re-attach is view-only for non-driven sessions** — on respawn the new brain queries the broker for every hosted session and re-attaches to rebuild output-continuity cursors, but it re-attaches as a **viewer**, never a controller, for any session it does not itself drive (which is *all* of them today — the supervised daemon brain hosts no PTY sessions; spt-hosted PTYs are driven by the operator's attach or the endpoint's own loop). Re-attaching as a controller would seize the controller slot ...\n 704│","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1789528602769}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_KbtAa1zolTLuYjNN9j7IF6nR|fc_0cf46cef591a107c016aaa0a1e750487d2856c91efce9a2a9d","toolName":"yield","startedAt":"2026-09-16T03:18:28.896Z"},"id":"6903fcff","parentId":"79ba82e3","timestamp":"2026-09-16T03:18:28.896Z"}
{"type":"message","id":"749e9d78","parentId":"6903fcff","timestamp":"2026-09-16T03:18:28.898Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_KbtAa1zolTLuYjNN9j7IF6nR|fc_0cf46cef591a107c016aaa0a1e750487d2856c91efce9a2a9d","name":"yield","arguments":{"type":null,"data":{"summary":"Existing tests cover live-input flood containment, small manual restart replay, durable-opener recovery with an intentionally tiny retained ring, stale-stream fencing, and subscriber poison recovery separately. They do not establish sustained retained-input replay under real refresh. The gated refresh_freeze rig supplies that observable workload; pending-input replay versus outbound forwarding remains for parent measurement. Read-only inspection only; no validation commands or file changes.","files":[{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs:1-55,268-460,666-715","description":"64-record live-controller flood; explicit Unix-red/Windows-positive limitation; directly invokes serve_attach, no dispatcher restart."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs:541-670,864-1035,1264-1324","description":"Prepopulated session-output ring containment, two-input manual restart replay, and legitimate same-origin successor behavior."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs:110-136,209-219,351-366,494-504,571-685","description":"Production dispatch_fixture generations; finished-row retirement, finished request/reply exception, and active opener-eviction recovery."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs:320-415,478-535,537-607,677-788,808-809","description":"Subscriber isolation/cancellation, tail-only lease recovery, bounded fallback/breaker, and stale unfinished generation protection."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs:36-88,151-176","description":"Gated real-Windows refresh test and fixed timing/predicate configuration."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/support/refresh_freeze/rig.py:475-548,552-625","description":"One tagged input per second, independent child ledger, actual node refresh, recovery/poison/closure predicates."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/attach.rs:480-515,549-590,686-713,762-817","description":"Actual serve_attach lifecycle: subscribe before seeded request; unconfirmed inputs buffer; live confirmed inputs no-ack; pending flush uses acked send_effect. Parent instrumentation already present in this read."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs:2850-2862,13305-13342","description":"Explicit by:Some promotion exclusion and existing local_controller_wedged_scopes_to_the_brains_own_conn unit."},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0038-broker-stream-lifecycle-truth.md:26-70,73-135","description":"Lifecycle-only suppression, durable classification, legitimate successor retake, bounded subscriber/forwarding and ownership amendment."},{"path":".worktrees/49-267-refresh-discriminator/traceable-reqs.toml:1582-1586,2375-2398,2649-2652,2734-2752,2776-2794","description":"Exact active requirement stages and deferred clean-close seed."}],"architecture":"Broker retains PTY, effect journal, stream history and immutable opener across brain death. Production dispatch_fixture runs run_dispatch_loop and reconstructs serve_attach workers. serve_attach consumes stream replay and broker role/output events on brain, forwarding output over separate wire connection. Promotion only considers wedged by:None controllers; operator by:Some controller recovery is deliberately a separate contract.","report":"All paths below are relative to .worktrees/49-267-refresh-discriminator. Line numbers reflect the inspected live tree, including parent-owned temporary probes.\n\n## Existing coverage and precise gaps\n\n- `input_flood_through_serve_attach_does_not_deadlock_broker` — tests/input_ack_deadlock.rs:281-460,666-715. Real loopback broker/PTY, direct serve_attach, 64 inputs, pump-drained operator connection. Seeded child stays quiet during flood; assertions cover send completion, broker/session responsiveness and subsequent viewer repaint/live output. Header :38-54 explicitly states 64 inputs is deterministic pre-fix RED on Unix, NOT Windows; larger Windows N confounded the fixed path with loopback send-window throttling. No restart, retained history admission, or assertion that N inputs enter pending_input. [INFERENCE] Some cold-start racing input may enter pending_input, but this test neither measures nor forces a pressure-sized pending batch. Existing passing containment must not be described as falsified by the refresh incident.\n\n- `attach_survives_target_brain_restart_exactly_once` — tests/attach.rs:864-1035. Life 1 is manually stepped (:908-948), then snapshot/drop; only MARKER_ONE (op 10) and dead-window MARKER_TWO (op 11) are replayed. Life 2 directly calls serve_attach with no durable seed (:958-981), bypassing production rediscovery. Asserts resulting screen contains both markers and agrees with authoritative screen. It exercises small replay/idempotency, not a pressure-sized retained history or actual refresh trial. A screen oracle can also conceal repeated same-looking effects; a producer ledger is stronger for exact application count.\n\n- `loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock` — tests/attach.rs:541-670. Child emits TICK0..39; spawn connection drains then drops; direct serve_attach must forward through TICK39. This is retained SESSION OUTPUT, not retained operator-input history. It defends the two-connection replay-versus-forward split, not pending-input ACK traffic.\n\n- `finished_historical_attach_neither_takes_nor_clears_current_controller` — tests/redispatch.rs:209-350; `finished_unretired_attach_is_skipped_and_drained_not_replayed` — :351-492. Production dispatcher generations defend terminal exclusion including kill-mid-serve followed by FIN. `finished_pending_sync_request_still_serves_across_generation_start` — :494-569 guards the family exception: finished request/reply work must still answer. These are lifecycle eligibility boundaries, not a claim that every eligible active replay drains.\n\n- `opener_evicted_active_stream_resumes_after_dispatcher_restart` — tests/redispatch.rs:571-685. Best existing production seam: Generation (:110-136) launches actual run_dispatch_loop in dispatch_fixture; kill/restart reconstructs the SAME operator stream. Critically sets SPT_STREAM_RING_CHUNKS=8, sends 40 spaced inputs, proves floor_seq>0 and opener remains pinned, then verifies MARK-B after generation restart. This deliberately removes most history to exercise durable classification. Transport chunks are not equal to Input records, so do not claim exactly eight input records remain. It neither admits a retained-opener/full-history case nor measures replay pressure.\n\n- `transient_worker_start_failure_recovers_via_claim_retry` — tests/redispatch.rs:689 onward; fallback/breaker tests `opener_transient_is_bounded_requeue_never_a_peek_subscriber` and `seat_blocked_failure_trips_the_breaker_and_the_stream_recovers` — redispatch_stall.rs:546,607. Defend classified setup failure/recovery rather than an established serving worker's input-history drain.\n\n- `wedged_subscriber_never_stalls_the_producer_and_recovery_replays_gapless` — redispatch_stall.rs:320-404. Genuine broker subscribe/ring poison isolation, not attach-role input replay. `poisoned_subscriber_cancels_the_paired_forwarding_leg_then_recovery_serves` — :406-535 uses generic hello/tick traffic and acked net_stream_send. Its recovery explicitly subscribes at TAIL, with empty replay (:485-509), isolating lease renewal; it intentionally does not test recovery replay pressure. `stale_unfinished_attach_replay_neither_takes_nor_clears_the_replacement_controller` — :677-788 retains one S1-MARK input, kills gen1 without FIN, creates newer s2 and drives three markers. Defends stale ownership fencing, not large legitimate same-stream replay. `old_broker_unknown_verb_answer_falls_back_to_the_peek_and_serves_real_traffic` — :809 onward preserves N-1 fallback only.\n\n- `refresh_resumes_bounded_input_ack_without_replacement_poison` — refresh_freeze.rs:39-176 is the already-reproducing real Windows surface, not a result to reconfirm. Config :78-87: baseline 30s, post-refresh 25s, ACK bound 2s, recovery bound 15s. rig.py:531-548 sends one unique tag per second; :557-579 invokes real `node refresh` and verifies replacement brain identity. :600-625 requires bounded recovery, sustained timely ACK+state, accounting for every post tag, zero replacement write poison, detach-only controller closures, healthy reader. Independent producer ledger avoids PTY echo as child-input proof. Keep rig unchanged during discrimination.\n\n## Falsifiable source observations; not an RCA verdict\n\nsrc/attach.rs:480 subscribes to retained stream before seed processing (:549-590); role remains None until Subscribed. Input records accepted while role=None go into pending_input (:686-713). Confirmed live inputs call send_effect_no_ack (:704-707); confirmed-role pending drain calls send_effect (:789-800). src/brain.rs:921-935 maps these to ack=true versus false. Thus two semantically related input paths currently request different ACK behavior. This is a source fact, NOT proof the measured stall sits in that drain.\n\nParent probes distinguish: pressure-sized pending_flush with unmatched pending_send_effect boundary supports the pending replay hypothesis; pending flush completed/empty with an unmatched wire forward boundary supports outbound forwarding instead. Reader state and stream/connection attribution must establish the actually blocked pair. Promotion completion alone cannot select either hypothesis. No product remedy is proposed from this observation.\n\n## Coverage to defend a measured remedy\n\n1. Reuse redispatch.rs Generation + real broker/PTY/operator path. Admit an ACTIVE unfinished stream whose opener remains in the ring and whose retained input history is large enough to exercise the measured condition; restart only production dispatcher generation, retaining broker and same operator stream. Unlike D2, do not shrink/roll history away. Require post-restart unique child ACK/state progress within a bound, exact-once child effects for already-applied retained ops, and delivery of dead-window/new input. Exercise retained-opener and existing evicted-opener cases as distinct boundary conditions, not interchangeable tests.\n2. Force/observe the measured pre-role buffering condition at the real serving seam rather than merely injecting a batch and assuming scheduling. If measurement instead identifies wire-forward blocking, target that pairing and cancellation/replacement transition; do not rename an input-only test as its defense.\n3. Unit coverage should defend a real lifecycle transition introduced/changed by the measured remedy: unconfirmed input never applied before grant, Controller/TookControl permits it exactly once, Viewer/Busy cannot leak it, stale completion cannot clear replacement. Avoid call-count/wiring tests or a new abstraction just to unit-test a branchless call change. Existing protocol units (`msg.rs:2262,2294`) cover ACK compatibility, not pending-role lifecycle. Existing broker real-surface `ack_false_input_suppresses_applied_frame` (tests/broker.rs:759-872) covers ACK emission, and no-ack dedup helper (:875-924) covers repeated durable keys; neither invokes pending_input.\n4. Preserve real refresh_freeze acceptance unchanged after the eventual measured fix. Dispatcher-only tests cannot prove process supervision, promotion or Windows ConPTY/rc continuity.\n\n## Exact requirements/stages and documentation\n\nStages below are manifest requirements, NOT a claim that coverage has just been executed.\n\n- `REQ-DAEMON-REFRESH`: doc, impl, unit, int — traceable-reqs.toml:2649-2652. Canonical recovery definition CONTEXT.md:690; brain cycle without swap/broker/PTy loss. ADR-0018 Decision Q3/Q6/Q7 (:24-28) gives cycle, continuity/idempotency and readiness/rollback foundations.\n- `REQ-HAZARD-INPUT-ACK-BACKPRESSURE`: doc, impl, unit, int — manifest:1582-1586. KNOWN-HAZARDS §7.19 (:606-611) describes opt-in applied ACK, no-ack fire-and-forward path, retained exactly-once, and broker-resident/N-1 caveat. No separate pending_input-specific REQ was identified; this is the existing matching input-pressure contract.\n- `REQ-HAZARD-REDISPATCH-CONTROL-STEAL`: doc, int — :2734-2737; KNOWN-HAZARDS §7.41 (:789-793).\n- `REQ-REDISPATCH-FINISHED-RETIRE`: doc, impl, unit, int — :2739-2742. ADR-0038 Decision 1 (:31-40): retirement is enumeration exclusion, NOT transport teardown.\n- `REQ-STREAM-OPENER-DURABLE`: impl, unit, int — :2744-2747. ADR-0038 Decision 2 (:41-45); existing units named `opener_pins_first_line_across_full_ring_roll`, `opener_capture_gives_up_bounded_on_a_newline_less_stream` in manifest evidence.\n- `REQ-DISPATCH-CLAIM-RETRY`: impl, unit, int — :2749-2752. ADR-0038 Decision 3 (:46-49).\n- `REQ-HAZARD-REDISPATCH-STALL`: doc, int — :2776-2779; KNOWN-HAZARDS §7.43 (:808-812).\n- `REQ-DISPATCH-FALLBACK-CIRCUIT`, `REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE`, `REQ-SERVE-OWNERSHIP-GENERATION`: each doc, impl, unit, int — :2781-2794. ADR-0038 Amendment (:73-135), especially bounded/cancelable forwarding, no replacement until predecessor gone, and generation validation. Amendment explicitly says prior D1/D1b were field-correct on their own terms (:84-93), not failed containment.\n- `REQ-UPDATE-PROMOTE-DRAINED`: impl, unit, int — :2375-2378; CONTEXT.md:701, W3-WAVE-GATE-REPORT.md:34-56 and W3-GATE-VERDICT.md:40-43. by:Some exclusion is explicit and must remain. Source predicate src/broker.rs:2850-2862 only considers by.is_none(); unit `local_controller_wedged_scopes_to_the_brains_own_conn` (:13305-13342) checks wedged local=true, identically wedged remote=false, idle local=false. docs/49-267-REFRESH-OBSERVATION-PLAN.md:219-221 correctly warns exclusion does not rule out indirect viewport effects.\n- `REQ-UPDATE-TRIAL-DRAIN-DRIVE`: doc, impl, int — :2380-2383; heartbeat drives reap; unit deliberately omitted for branchless call. `REQ-BRAIN-RESUME-NO-CONTROL-STEAL`: doc, impl, int — :2395-2398; CONTEXT.md:703, viewer-only resume for non-driven sessions.\n- `REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE`: required_stages=[] — :2385-2388, explicitly deferred seed. Do not treat a clean-close implementation as currently mandated or infer it is the remedy.\n\nNo edits, tests, builds, formatters, linters, or reproduction runs performed."},"error":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":872,"output":3421,"cacheRead":88832,"cacheWrite":0,"totalTokens":93125,"cost":{"input":0.00872,"output":0.17105,"cacheRead":0.088832,"cacheWrite":0,"total":0.268602}},"stopReason":"toolUse","timestamp":1789528602817,"responseId":"resp_0cf46cef591a107c016aaa0a1b1d5887d29e978ed594b551ee","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0cf46cef591a107c016aaa0a1e750487d2856c91efce9a2a9d","type":"function_call","status":"completed","arguments":"{\"type\":null,\"data\":{\"summary\":\"Existing tests cover live-input flood containment, small manual restart replay, durable-opener recovery with an intentionally tiny retained ring, stale-stream fencing, and subscriber poison recovery separately. They do not establish sustained retained-input replay under real refresh. The gated refresh_freeze rig supplies that observable workload; pending-input replay versus outbound forwarding remains for parent measurement. Read-only inspection only; no validation commands or file changes.\",\"files\":[{\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs:1-55,268-460,666-715\",\"description\":\"64-record live-controller flood; explicit Unix-red/Windows-positive limitation; directly invokes serve_attach, no dispatcher restart.\"},{\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs:541-670,864-1035,1264-1324\",\"description\":\"Prepopulated session-output ring containment, two-input manual restart replay, and legitimate same-origin successor behavior.\"},{\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs:110-136,209-219,351-366,494-504,571-685\",\"description\":\"Production dispatch_fixture generations; finished-row retirement, finished request/reply exception, and active opener-eviction recovery.\"},{\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs:320-415,478-535,537-607,677-788,808-809\",\"description\":\"Subscriber isolation/cancellation, tail-only lease recovery, bounded fallback/breaker, and stale unfinished generation protection.\"},{\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs:36-88,151-176\",\"description\":\"Gated real-Windows refresh test and fixed timing/predicate configuration.\"},{\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/support/refresh_freeze/rig.py:475-548,552-625\",\"description\":\"One tagged input per second, independent child ledger, actual node refresh, recovery/poison/closure predicates.\"},{\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/attach.rs:480-515,549-590,686-713,762-817\",\"description\":\"Actual serve_attach lifecycle: subscribe before seeded request; unconfirmed inputs buffer; live confirmed inputs no-ack; pending flush uses acked send_effect. Parent instrumentation already present in this read.\"},{\"path\":\".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs:2850-2862,13305-13342\",\"description\":\"Explicit by:Some promotion exclusion and existing local_controller_wedged_scopes_to_the_brains_own_conn unit.\"},{\"path\":\".worktrees/49-267-refresh-discriminator/docs/adr/0038-broker-stream-lifecycle-truth.md:26-70,73-135\",\"description\":\"Lifecycle-only suppression, durable classification, legitimate successor retake, bounded subscriber/forwarding and ownership amendment.\"},{\"path\":\".worktrees/49-267-refresh-discriminator/traceable-reqs.toml:1582-1586,2375-2398,2649-2652,2734-2752,2776-2794\",\"description\":\"Exact active requirement stages and deferred clean-close seed.\"}],\"architecture\":\"Broker retains PTY, effect journal, stream history and immutable opener across brain death. Production dispatch_fixture runs run_dispatch_loop and reconstructs serve_attach workers. serve_attach consumes stream replay and broker role/output events on brain, forwarding output over separate wire connection. Promotion only considers wedged by:None controllers; operator by:Some controller recovery is deliberately a separate contract.\",\"report\":\"All paths below are relative to .worktrees/49-267-refresh-discriminator. Line numbers reflect the inspected live tree, including parent-owned temporary probes.\\n\\n## Existing coverage and precise gaps\\n\\n- `input_flood_through_serve_attach_does_not_deadlock_broker` — tests/input_ack_deadlock.rs:281-460,666-715. Real loopback broker/PTY, direct serve_attach, 64 inputs, pump-drained operator connection. Seeded child stays quiet during flood; assertions cover send completion, broker/session responsiveness and subsequent viewer repaint/live output. Header :38-54 explicitly states 64 inputs is deterministic pre-fix RED on Unix, NOT Windows; larger Windows N confounded the fixed path with loopback send-window throttling. No restart, retained history admission, or assertion that N inputs enter pending_input. [INFERENCE] Some cold-start racing input may enter pending_input, but this test neither measures nor forces a pressure-sized pending batch. Existing passing containment must not be described as falsified by the refresh incident.\\n\\n- `attach_survives_target_brain_restart_exactly_once` — tests/attach.rs:864-1035. Life 1 is manually stepped (:908-948), then snapshot/drop; only MARKER_ONE (op 10) and dead-window MARKER_TWO (op 11) are replayed. Life 2 directly calls serve_attach with no durable seed (:958-981), bypassing production rediscovery. Asserts resulting screen contains both markers and agrees with authoritative screen. It exercises small replay/idempotency, not a pressure-sized retained history or actual refresh trial. A screen oracle can also conceal repeated same-looking effects; a producer ledger is stronger for exact application count.\\n\\n- `loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock` — tests/attach.rs:541-670. Child emits TICK0..39; spawn connection drains then drops; direct serve_attach must forward through TICK39. This is retained SESSION OUTPUT, not retained operator-input history. It defends the two-connection replay-versus-forward split, not pending-input ACK traffic.\\n\\n- `finished_historical_attach_neither_takes_nor_clears_current_controller` — tests/redispatch.rs:209-350; `finished_unretired_attach_is_skipped_and_drained_not_replayed` — :351-492. Production dispatcher generations defend terminal exclusion including kill-mid-serve followed by FIN. `finished_pending_sync_request_still_serves_across_generation_start` — :494-569 guards the family exception: finished request/reply work must still answer. These are lifecycle eligibility boundaries, not a claim that every eligible active replay drains.\\n\\n- `opener_evicted_active_stream_resumes_after_dispatcher_restart` — tests/redispatch.rs:571-685. Best existing production seam: Generation (:110-136) launches actual run_dispatch_loop in dispatch_fixture; kill/restart reconstructs the SAME operator stream. Critically sets SPT_STREAM_RING_CHUNKS=8, sends 40 spaced inputs, proves floor_seq>0 and opener remains pinned, then verifies MARK-B after generation restart. This deliberately removes most history to exercise durable classification. Transport chunks are not equal to Input records, so do not claim exactly eight input records remain. It neither admits a retained-opener/full-history case nor measures replay pressure.\\n\\n- `transient_worker_start_failure_recovers_via_claim_retry` — tests/redispatch.rs:689 onward; fallback/breaker tests `opener_transient_is_bounded_requeue_never_a_peek_subscriber` and `seat_blocked_failure_trips_the_breaker_and_the_stream_recovers` — redispatch_stall.rs:546,607. Defend classified setup failure/recovery rather than an established serving worker's input-history drain.\\n\\n- `wedged_subscriber_never_stalls_the_producer_and_recovery_replays_gapless` — redispatch_stall.rs:320-404. Genuine broker subscribe/ring poison isolation, not attach-role input replay. `poisoned_subscriber_cancels_the_paired_forwarding_leg_then_recovery_serves` — :406-535 uses generic hello/tick traffic and acked net_stream_send. Its recovery explicitly subscribes at TAIL, with empty replay (:485-509), isolating lease renewal; it intentionally does not test recovery replay pressure. `stale_unfinished_attach_replay_neither_takes_nor_clears_the_replacement_controller` — :677-788 retains one S1-MARK input, kills gen1 without FIN, creates newer s2 and drives three markers. Defends stale ownership fencing, not large legitimate same-stream replay. `old_broker_unknown_verb_answer_falls_back_to_the_peek_and_serves_real_traffic` — :809 onward preserves N-1 fallback only.\\n\\n- `refresh_resumes_bounded_input_ack_without_replacement_poison` — refresh_freeze.rs:39-176 is the already-reproducing real Windows surface, not a result to reconfirm. Config :78-87: baseline 30s, post-refresh 25s, ACK bound 2s, recovery bound 15s. rig.py:531-548 sends one unique tag per second; :557-579 invokes real `node refresh` and verifies replacement brain identity. :600-625 requires bounded recovery, sustained timely ACK+state, accounting for every post tag, zero replacement write poison, detach-only controller closures, healthy reader. Independent producer ledger avoids PTY echo as child-input proof. Keep rig unchanged during discrimination.\\n\\n## Falsifiable source observations; not an RCA verdict\\n\\nsrc/attach.rs:480 subscribes to retained stream before seed processing (:549-590); role remains None until Subscribed. Input records accepted while role=None go into pending_input (:686-713). Confirmed live inputs call send_effect_no_ack (:704-707); confirmed-role pending drain calls send_effect (:789-800). src/brain.rs:921-935 maps these to ack=true versus false. Thus two semantically related input paths currently request different ACK behavior. This is a source fact, NOT proof the measured stall sits in that drain.\\n\\nParent probes distinguish: pressure-sized pending_flush with unmatched pending_send_effect boundary supports the pending replay hypothesis; pending flush completed/empty with an unmatched wire forward boundary supports outbound forwarding instead. Reader state and stream/connection attribution must establish the actually blocked pair. Promotion completion alone cannot select either hypothesis. No product remedy is proposed from this observation.\\n\\n## Coverage to defend a measured remedy\\n\\n1. Reuse redispatch.rs Generation + real broker/PTY/operator path. Admit an ACTIVE unfinished stream whose opener remains in the ring and whose retained input history is large enough to exercise the measured condition; restart only production dispatcher generation, retaining broker and same operator stream. Unlike D2, do not shrink/roll history away. Require post-restart unique child ACK/state progress within a bound, exact-once child effects for already-applied retained ops, and delivery of dead-window/new input. Exercise retained-opener and existing evicted-opener cases as distinct boundary conditions, not interchangeable tests.\\n2. Force/observe the measured pre-role buffering condition at the real serving seam rather than merely injecting a batch and assuming scheduling. If measurement instead identifies wire-forward blocking, target that pairing and cancellation/replacement transition; do not rename an input-only test as its defense.\\n3. Unit coverage should defend a real lifecycle transition introduced/changed by the measured remedy: unconfirmed input never applied before grant, Controller/TookControl permits it exactly once, Viewer/Busy cannot leak it, stale completion cannot clear replacement. Avoid call-count/wiring tests or a new abstraction just to unit-test a branchless call change. Existing protocol units (`msg.rs:2262,2294`) cover ACK compatibility, not pending-role lifecycle. Existing broker real-surface `ack_false_input_suppresses_applied_frame` (tests/broker.rs:759-872) covers ACK emission, and no-ack dedup helper (:875-924) covers repeated durable keys; neither invokes pending_input.\\n4. Preserve real refresh_freeze acceptance unchanged after the eventual measured fix. Dispatcher-only tests cannot prove process supervision, promotion or Windows ConPTY/rc continuity.\\n\\n## Exact requirements/stages and documentation\\n\\nStages below are manifest requirements, NOT a claim that coverage has just been executed.\\n\\n- `REQ-DAEMON-REFRESH`: doc, impl, unit, int — traceable-reqs.toml:2649-2652. Canonical recovery definition CONTEXT.md:690; brain cycle without swap/broker/PTy loss. ADR-0018 Decision Q3/Q6/Q7 (:24-28) gives cycle, continuity/idempotency and readiness/rollback foundations.\\n- `REQ-HAZARD-INPUT-ACK-BACKPRESSURE`: doc, impl, unit, int — manifest:1582-1586. KNOWN-HAZARDS §7.19 (:606-611) describes opt-in applied ACK, no-ack fire-and-forward path, retained exactly-once, and broker-resident/N-1 caveat. No separate pending_input-specific REQ was identified; this is the existing matching input-pressure contract.\\n- `REQ-HAZARD-REDISPATCH-CONTROL-STEAL`: doc, int — :2734-2737; KNOWN-HAZARDS §7.41 (:789-793).\\n- `REQ-REDISPATCH-FINISHED-RETIRE`: doc, impl, unit, int — :2739-2742. ADR-0038 Decision 1 (:31-40): retirement is enumeration exclusion, NOT transport teardown.\\n- `REQ-STREAM-OPENER-DURABLE`: impl, unit, int — :2744-2747. ADR-0038 Decision 2 (:41-45); existing units named `opener_pins_first_line_across_full_ring_roll`, `opener_capture_gives_up_bounded_on_a_newline_less_stream` in manifest evidence.\\n- `REQ-DISPATCH-CLAIM-RETRY`: impl, unit, int — :2749-2752. ADR-0038 Decision 3 (:46-49).\\n- `REQ-HAZARD-REDISPATCH-STALL`: doc, int — :2776-2779; KNOWN-HAZARDS §7.43 (:808-812).\\n- `REQ-DISPATCH-FALLBACK-CIRCUIT`, `REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE`, `REQ-SERVE-OWNERSHIP-GENERATION`: each doc, impl, unit, int — :2781-2794. ADR-0038 Amendment (:73-135), especially bounded/cancelable forwarding, no replacement until predecessor gone, and generation validation. Amendment explicitly says prior D1/D1b were field-correct on their own terms (:84-93), not failed containment.\\n- `REQ-UPDATE-PROMOTE-DRAINED`: impl, unit, int — :2375-2378; CONTEXT.md:701, W3-WAVE-GATE-REPORT.md:34-56 and W3-GATE-VERDICT.md:40-43. by:Some exclusion is explicit and must remain. Source predicate src/broker.rs:2850-2862 only considers by.is_none(); unit `local_controller_wedged_scopes_to_the_brains_own_conn` (:13305-13342) checks wedged local=true, identically wedged remote=false, idle local=false. docs/49-267-REFRESH-OBSERVATION-PLAN.md:219-221 correctly warns exclusion does not rule out indirect viewport effects.\\n- `REQ-UPDATE-TRIAL-DRAIN-DRIVE`: doc, impl, int — :2380-2383; heartbeat drives reap; unit deliberately omitted for branchless call. `REQ-BRAIN-RESUME-NO-CONTROL-STEAL`: doc, impl, int — :2395-2398; CONTEXT.md:703, viewer-only resume for non-driven sessions.\\n- `REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE`: required_stages=[] — :2385-2388, explicitly deferred seed. Do not treat a clean-close implementation as currently mandated or infer it is the remedy.\\n\\nNo edits, tests, builds, formatters, linters, or reproduction runs performed.\"},\"error\":null}","call_id":"call_KbtAa1zolTLuYjNN9j7IF6nR","internal_chat_message_metadata_passthrough":{"create_time":1789528603.30308,"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"},"name":"yield","metadata":{"turn_id":"9b38cee3-24b0-46f6-8b4c-be1f29972355"}}]},"duration":106070.7779000001,"ttft":3661.8153000008315,"completedAt":1789528708893,"contextSnapshot":{"promptTokens":89704,"nonMessageTokens":10304,"compactionEpoch":0}}}
{"type":"message","id":"657d1e01","parentId":"749e9d78","timestamp":"2026-09-16T03:18:28.903Z","message":{"role":"toolResult","toolCallId":"call_KbtAa1zolTLuYjNN9j7IF6nR|fc_0cf46cef591a107c016aaa0a1e750487d2856c91efce9a2a9d","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"Existing tests cover live-input flood containment, small manual restart replay, durable-opener recovery with an intentionally tiny retained ring, stale-stream fencing, and subscriber poison recovery separately. They do not establish sustained retained-input replay under real refresh. The gated refresh_freeze rig supplies that observable workload; pending-input replay versus outbound forwarding remains for parent measurement. Read-only inspection only; no validation commands or file changes.","files":[{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/input_ack_deadlock.rs:1-55,268-460,666-715","description":"64-record live-controller flood; explicit Unix-red/Windows-positive limitation; directly invokes serve_attach, no dispatcher restart."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/attach.rs:541-670,864-1035,1264-1324","description":"Prepopulated session-output ring containment, two-input manual restart replay, and legitimate same-origin successor behavior."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch.rs:110-136,209-219,351-366,494-504,571-685","description":"Production dispatch_fixture generations; finished-row retirement, finished request/reply exception, and active opener-eviction recovery."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/redispatch_stall.rs:320-415,478-535,537-607,677-788,808-809","description":"Subscriber isolation/cancellation, tail-only lease recovery, bounded fallback/breaker, and stale unfinished generation protection."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/refresh_freeze.rs:36-88,151-176","description":"Gated real-Windows refresh test and fixed timing/predicate configuration."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/tests/support/refresh_freeze/rig.py:475-548,552-625","description":"One tagged input per second, independent child ledger, actual node refresh, recovery/poison/closure predicates."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/attach.rs:480-515,549-590,686-713,762-817","description":"Actual serve_attach lifecycle: subscribe before seeded request; unconfirmed inputs buffer; live confirmed inputs no-ack; pending flush uses acked send_effect. Parent instrumentation already present in this read."},{"path":".worktrees/49-267-refresh-discriminator/crates/spt-daemon/src/broker.rs:2850-2862,13305-13342","description":"Explicit by:Some promotion exclusion and existing local_controller_wedged_scopes_to_the_brains_own_conn unit."},{"path":".worktrees/49-267-refresh-discriminator/docs/adr/0038-broker-stream-lifecycle-truth.md:26-70,73-135","description":"Lifecycle-only suppression, durable classification, legitimate successor retake, bounded subscriber/forwarding and ownership amendment."},{"path":".worktrees/49-267-refresh-discriminator/traceable-reqs.toml:1582-1586,2375-2398,2649-2652,2734-2752,2776-2794","description":"Exact active requirement stages and deferred clean-close seed."}],"architecture":"Broker retains PTY, effect journal, stream history and immutable opener across brain death. Production dispatch_fixture runs run_dispatch_loop and reconstructs serve_attach workers. serve_attach consumes stream replay and broker role/output events on brain, forwarding output over separate wire connection. Promotion only considers wedged by:None controllers; operator by:Some controller recovery is deliberately a separate contract.","report":"All paths below are relative to .worktrees/49-267-refresh-discriminator. Line numbers reflect the inspected live tree, including parent-owned temporary probes.\n\n## Existing coverage and precise gaps\n\n- `input_flood_through_serve_attach_does_not_deadlock_broker` — tests/input_ack_deadlock.rs:281-460,666-715. Real loopback broker/PTY, direct serve_attach, 64 inputs, pump-drained operator connection. Seeded child stays quiet during flood; assertions cover send completion, broker/session responsiveness and subsequent viewer repaint/live output. Header :38-54 explicitly states 64 inputs is deterministic pre-fix RED on Unix, NOT Windows; larger Windows N confounded the fixed path with loopback send-window throttling. No restart, retained history admission, or assertion that N inputs enter pending_input. [INFERENCE] Some cold-start racing input may enter pending_input, but this test neither measures nor forces a pressure-sized pending batch. Existing passing containment must not be described as falsified by the refresh incident.\n\n- `attach_survives_target_brain_restart_exactly_once` — tests/attach.rs:864-1035. Life 1 is manually stepped (:908-948), then snapshot/drop; only MARKER_ONE (op 10) and dead-window MARKER_TWO (op 11) are replayed. Life 2 directly calls serve_attach with no durable seed (:958-981), bypassing production rediscovery. Asserts resulting screen contains both markers and agrees with authoritative screen. It exercises small replay/idempotency, not a pressure-sized retained history or actual refresh trial. A screen oracle can also conceal repeated same-looking effects; a producer ledger is stronger for exact application count.\n\n- `loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock` — tests/attach.rs:541-670. Child emits TICK0..39; spawn connection drains then drops; direct serve_attach must forward through TICK39. This is retained SESSION OUTPUT, not retained operator-input history. It defends the two-connection replay-versus-forward split, not pending-input ACK traffic.\n\n- `finished_historical_attach_neither_takes_nor_clears_current_controller` — tests/redispatch.rs:209-350; `finished_unretired_attach_is_skipped_and_drained_not_replayed` — :351-492. Production dispatcher generations defend terminal exclusion including kill-mid-serve followed by FIN. `finished_pending_sync_request_still_serves_across_generation_start` — :494-569 guards the family exception: finished request/reply work must still answer. These are lifecycle eligibility boundaries, not a claim that every eligible active replay drains.\n\n- `opener_evicted_active_stream_resumes_after_dispatcher_restart` — tests/redispatch.rs:571-685. Best existing production seam: Generation (:110-136) launches actual run_dispatch_loop in dispatch_fixture; kill/restart reconstructs the SAME operator stream. Critically sets SPT_STREAM_RING_CHUNKS=8, sends 40 spaced inputs, proves floor_seq>0 and opener remains pinned, then verifies MARK-B after generation restart. This deliberately removes most history to exercise durable classification. Transport chunks are not equal to Input records, so do not claim exactly eight input records remain. It neither admits a retained-opener/full-history case nor measures replay pressure.\n\n- `transient_worker_start_failure_recovers_via_claim_retry` — tests/redispatch.rs:689 onward; fallback/breaker tests `opener_transient_is_bounded_requeue_never_a_peek_subscriber` and `seat_blocked_failure_trips_the_breaker_and_the_stream_recovers` — redispatch_stall.rs:546,607. Defend classified setup failure/recovery rather than an established serving worker's input-history drain.\n\n- `wedged_subscriber_never_stalls_the_producer_and_recovery_replays_gapless` — redispatch_stall.rs:320-404. Genuine broker subscribe/ring poison isolation, not attach-role input replay. `poisoned_subscriber_cancels_the_paired_forwarding_leg_then_recovery_serves` — :406-535 uses generic hello/tick traffic and acked net_stream_send. Its recovery explicitly subscribes at TAIL, with empty replay (:485-509), isolating lease renewal; it intentionally does not test recovery replay pressure. `stale_unfinished_attach_replay_neither_takes_nor_clears_the_replacement_controller` — :677-788 retains one S1-MARK input, kills gen1 without FIN, creates newer s2 and drives three markers. Defends stale ownership fencing, not large legitimate same-stream replay. `old_broker_unknown_verb_answer_falls_back_to_the_peek_and_serves_real_traffic` — :809 onward preserves N-1 fallback only.\n\n- `refresh_resumes_bounded_input_ack_without_replacement_poison` — refresh_freeze.rs:39-176 is the already-reproducing real Windows surface, not a result to reconfirm. Config :78-87: baseline 30s, post-refresh 25s, ACK bound 2s, recovery bound 15s. rig.py:531-548 sends one unique tag per second; :557-579 invokes real `node refresh` and verifies replacement brain identity. :600-625 requires bounded recovery, sustained timely ACK+state, accounting for every post tag, zero replacement write poison, detach-only controller closures, healthy reader. Independent producer ledger avoids PTY echo as child-input proof. Keep rig unchanged during discrimination.\n\n## Falsifiable source observations; not an RCA verdict\n\nsrc/attach.rs:480 subscribes to retained stream before seed processing (:549-590); role remains None until Subscribed. Input records accepted while role=None go into pending_input (:686-713). Confirmed live inputs call send_effect_no_ack (:704-707); confirmed-role pending drain calls send_effect (:789-800). src/brain.rs:921-935 maps these to ack=true versus false. Thus two semantically related input paths currently request different ACK behavior. This is a source fact, NOT proof the measured stall sits in that drain.\n\nParent probes distinguish: pressure-sized pending_flush with unmatched pending_send_effect boundary supports the pending replay hypothesis; pending flush completed/empty with an unmatched wire forward boundary supports outbound forwarding instead. Reader state and stream/connection attribution must establish the actually blocked pair. Promotion completion alone cannot select either hypothesis. No product remedy is proposed from this observation.\n\n## Coverage to defend a measured remedy\n\n1. Reuse redispatch.rs Generation + real broker/PTY/operator path. Admit an ACTIVE unfinished stream whose opener remains in the ring and whose retained input history is large enough to exercise the measured condition; restart only production dispatcher generation, retaining broker and same operator stream. Unlike D2, do not shrink/roll history away. Require post-restart unique child ACK/state progress within a bound, exact-once child effects for already-applied retained ops, and delivery of dead-window/new input. Exercise retained-opener and existing evicted-opener cases as distinct boundary conditions, not interchangeable tests.\n2. Force/observe the measured pre-role buffering condition at the real serving seam rather than merely injecting a batch and assuming scheduling. If measurement instead identifies wire-forward blocking, target that pairing and cancellation/replacement transition; do not rename an input-only test as its defense.\n3. Unit coverage should defend a real lifecycle transition introduced/changed by the measured remedy: unconfirmed input never applied before grant, Controller/TookControl permits it exactly once, Viewer/Busy cannot leak it, stale completion cannot clear replacement. Avoid call-count/wiring tests or a new abstraction just to unit-test a branchless call change. Existing protocol units (`msg.rs:2262,2294`) cover ACK compatibility, not pending-role lifecycle. Existing broker real-surface `ack_false_input_suppresses_applied_frame` (tests/broker.rs:759-872) covers ACK emission, and no-ack dedup helper (:875-924) covers repeated durable keys; neither invokes pending_input.\n4. Preserve real refresh_freeze acceptance unchanged after the eventual measured fix. Dispatcher-only tests cannot prove process supervision, promotion or Windows ConPTY/rc continuity.\n\n## Exact requirements/stages and documentation\n\nStages below are manifest requirements, NOT a claim that coverage has just been executed.\n\n- `REQ-DAEMON-REFRESH`: doc, impl, unit, int — traceable-reqs.toml:2649-2652. Canonical recovery definition CONTEXT.md:690; brain cycle without swap/broker/PTy loss. ADR-0018 Decision Q3/Q6/Q7 (:24-28) gives cycle, continuity/idempotency and readiness/rollback foundations.\n- `REQ-HAZARD-INPUT-ACK-BACKPRESSURE`: doc, impl, unit, int — manifest:1582-1586. KNOWN-HAZARDS §7.19 (:606-611) describes opt-in applied ACK, no-ack fire-and-forward path, retained exactly-once, and broker-resident/N-1 caveat. No separate pending_input-specific REQ was identified; this is the existing matching input-pressure contract.\n- `REQ-HAZARD-REDISPATCH-CONTROL-STEAL`: doc, int — :2734-2737; KNOWN-HAZARDS §7.41 (:789-793).\n- `REQ-REDISPATCH-FINISHED-RETIRE`: doc, impl, unit, int — :2739-2742. ADR-0038 Decision 1 (:31-40): retirement is enumeration exclusion, NOT transport teardown.\n- `REQ-STREAM-OPENER-DURABLE`: impl, unit, int — :2744-2747. ADR-0038 Decision 2 (:41-45); existing units named `opener_pins_first_line_across_full_ring_roll`, `opener_capture_gives_up_bounded_on_a_newline_less_stream` in manifest evidence.\n- `REQ-DISPATCH-CLAIM-RETRY`: impl, unit, int — :2749-2752. ADR-0038 Decision 3 (:46-49).\n- `REQ-HAZARD-REDISPATCH-STALL`: doc, int — :2776-2779; KNOWN-HAZARDS §7.43 (:808-812).\n- `REQ-DISPATCH-FALLBACK-CIRCUIT`, `REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE`, `REQ-SERVE-OWNERSHIP-GENERATION`: each doc, impl, unit, int — :2781-2794. ADR-0038 Amendment (:73-135), especially bounded/cancelable forwarding, no replacement until predecessor gone, and generation validation. Amendment explicitly says prior D1/D1b were field-correct on their own terms (:84-93), not failed containment.\n- `REQ-UPDATE-PROMOTE-DRAINED`: impl, unit, int — :2375-2378; CONTEXT.md:701, W3-WAVE-GATE-REPORT.md:34-56 and W3-GATE-VERDICT.md:40-43. by:Some exclusion is explicit and must remain. Source predicate src/broker.rs:2850-2862 only considers by.is_none(); unit `local_controller_wedged_scopes_to_the_brains_own_conn` (:13305-13342) checks wedged local=true, identically wedged remote=false, idle local=false. docs/49-267-REFRESH-OBSERVATION-PLAN.md:219-221 correctly warns exclusion does not rule out indirect viewport effects.\n- `REQ-UPDATE-TRIAL-DRAIN-DRIVE`: doc, impl, int — :2380-2383; heartbeat drives reap; unit deliberately omitted for branchless call. `REQ-BRAIN-RESUME-NO-CONTROL-STEAL`: doc, impl, int — :2395-2398; CONTEXT.md:703, viewer-only resume for non-driven sessions.\n- `REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE`: required_stages=[] — :2385-2388, explicitly deferred seed. Do not treat a clean-close implementation as currently mandated or infer it is the remedy.\n\nNo edits, tests, builds, formatters, linters, or reproduction runs performed."},"status":"success"},"isError":false,"timestamp":1789528708900}}
{"type":"custom","customType":"session_exit","data":{"reason":"dispose","kind":"normal","recordedAt":"2026-09-16T03:25:28.943Z"},"id":"8f4c7f07","parentId":"657d1e01","timestamp":"2026-09-16T03:25:28.943Z"}
