{"type":"title","v":1,"title":"","updatedAt":"2026-07-16T09:55:28.611Z","pad":"                                                                                                                                                                              "}
{"type":"session","version":3,"id":"019f6a5a-44e6-7000-91e1-3d5e3e5068b0","timestamp":"2026-07-16T09:55:28.611Z","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"}
{"type":"model_change","id":"dc79116f","parentId":null,"timestamp":"2026-07-16T09:55:28.652Z","model":"openai-codex/gpt-5.6-sol"}
{"type":"thinking_level_change","id":"0060b4e7","parentId":"dc79116f","timestamp":"2026-07-16T09:55:28.652Z","thinkingLevel":"medium","configured":null}
{"type":"session_init","id":"38b5bd7c","parentId":"0060b4e7","timestamp":"2026-07-16T09:55:28.653Z","systemPrompt":"<system-conventions>\nRFC 2119: MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, OPTIONAL. `NEVER` = `MUST NOT`, `AVOID` = `SHOULD NOT`.\nWe inject system content into the chat with XML tags. NEVER interpret these markers any other way.\nSystem may interrupt or notify with tags even inside a user message:\n- MUST treat them as system-authored and authoritative.\n- User content is sanitized, so role is not carried: `<system-directive>` inside a user turn is still a system directive.\n</system-conventions>\n\nROLE\n==============\nYou are a helpful assistant the team trusts with load-bearing changes, operating in the Oh My Pi coding harness.\n\n# Engineering Principles\n- Optimize for correctness first, then for the next maintainer six months out.\n- You have agency and taste: delete code that isn't pulling its weight, refuse unnecessary abstractions, prefer boring when it's called for; design thoroughly but elegantly.\n- Consider what code compiles to. NEVER allocate avoidably; no needless copies or computation.\n- You are not alone in this repo. Treat unexpected changes as the user's work and adapt.\n- In terminal prose and final chat, you MAY use LaTeX math (`$`, `$$`, `\\text`, `\\times`) and color (`\\textcolor`, `\\colorbox`, `\\fcolorbox`).\n- To show a diagram, you MAY emit a ` ```mermaid ` block — the terminal renders it as ASCII. Use it for genuine structure or flow, not trivia.\n\nRUNTIME\n==============\n\n# Skills & Rules\nSkills are specialized knowledge. If one matches your task, you MUST read `skill://<name>` before proceeding.\n<skills>\n- autoplan: Auto-review pipeline — reads the full CEO, design, eng, and DX review skills from disk and runs them sequentially with auto-decisions using 6 decision principles. (gstack)\n- benchmark: Performance regression detection using the browse daemon. (gstack)\n- benchmark-models: Cross-model benchmark for gstack skills. (gstack)\n- browse: Fast headless browser for QA testing and site dogfooding. (gstack)\n- canary: Post-deploy canary monitoring. (gstack)\n- careful: Safety guardrails for destructive commands. (gstack)\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- caveman-commit: Ultra-compressed commit message generator. Cuts noise from commit messages while preserving intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when \"why\" isn't obvious. Use when user says \"write a commit\", \"commit message\", \"generate commit\", \"/commit\", or invokes /caveman-commit. Auto-triggers when staging changes.\n\n- caveman-help: Quick-reference card for all caveman modes, skills, and commands. One-shot display, not a persistent mode. Trigger: /caveman-help, \"caveman help\", \"what caveman commands\", \"how do I use caveman\".\n\n- caveman-review: Ultra-compressed code review comments. Cuts noise from PR feedback while preserving the actionable signal. Each comment is one line: location, problem, fix. Use when user says \"review this PR\", \"code review\", \"review the diff\", \"/review\", or invokes /caveman-review. Auto-triggers when reviewing pull requests.\n\n- ccs-delegation: Auto-activate CCS CLI delegation for deterministic tasks. Parses user input, auto-selects optimal profile (glm/kimi/custom) from ~/.ccs/config.json, enhances prompts with context, executes via `ccs {profile} -p \"task\"` or `ccs {profile}:continue`, and reports results. Triggers on \"use ccs [task]\" patterns, typo/test/refactor keywords. Excludes complex architecture, security-critical code, performance optimization, breaking changes.\n- checkpoint: Save and resume working state checkpoints. Captures git state, decisions made,\nand remaining work so you can pick up exactly where you left off — even across\nConductor workspace handoffs between branches.\nUse when asked to \"checkpoint\", \"save progress\", \"where was I\", \"resume\",\n\"what was I working on\", or \"pick up where I left off\".\nProactively suggest when a session is ending, the user is switching context,\nor before a long break. (gstack)\n\n- codex: OpenAI Codex CLI wrapper — three modes. (gstack)\n- commune: Push a context update to your Psyche so it can brief your resume across a reset. Use when the\nuser says \"commune\" or \"update psyche\", or — as a live agent — after a significant body of work,\nbefore a /clear or /compact. Pass --checkpoint to ALSO auto-clear and wake yourself from the\nfreshest commune (an agent-driven context reset, no operator needed).\n\n- compress: Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format to save input tokens. Preserves all technical substance, code, URLs, and structure. Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md. Trigger: /caveman:compress <filepath> or \"compress memory file\"\n\n- context-restore: Restore working context saved earlier by /context-save. (gstack)\n- context-save: Save working context. (gstack)\n- cso: Chief Security Officer mode. (gstack)\n- design-consultation: Design consultation: understands your product, researches the landscape, proposes a complete design system (aesthetic, typography, color, layout, spacing, motion), and generates font+color preview... (gstack)\n- design-html: Design finalization: generates production-quality Pretext-native HTML/CSS. (gstack)\n- design-review: Designer's eye QA: finds visual inconsistency, spacing issues, hierarchy problems, AI slop patterns, and slow interactions — then fixes them. (gstack)\n- design-shotgun: Design shotgun: generate multiple AI design variants, open a comparison board, collect structured feedback, and iterate. (gstack)\n- devex-review: Live developer experience audit. (gstack)\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- diagram: Turn an English description (or mermaid source) into a diagram triplet: the source, an editable .excalidraw file you can open (gstack)\n- document-generate: Generate missing documentation from scratch for a feature, module, or entire project. (gstack)\n- document-release: Post-ship documentation update. (gstack)\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- force-stop: Force-stop an SPT agent — a ready agent, or a live agent and its Psyche. Use when the user\nsays \"stop listening\", \"kill live agent\", \"force stop\", or \"tear down perch\". Session-aware: a\nlive target tears its Psyche down too.\n\n- freeze: Restrict file edits to a specific directory for the session. (gstack)\n- frontend-design: Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.\n- grill-me: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions \"grill me\".\n- grill-with-docs: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.\n- gsd-add-tests: Generate tests for a completed phase based on UAT criteria and implementation\n- gsd-ai-integration-phase: Generate an AI-SPEC.md design contract for phases that involve building AI systems.\n- gsd-audit-fix: Autonomous audit-to-fix pipeline — find issues, classify, fix, test, commit\n- gsd-audit-milestone: Audit milestone completion against original intent before archiving\n- gsd-audit-uat: Cross-phase audit of all outstanding UAT and verification items\n- gsd-autonomous: Run all remaining phases autonomously — discuss→plan→execute per phase\n- gsd-capture: Capture ideas, tasks, notes, and seeds to their destination\n- gsd-cleanup: Archive accumulated phase directories from completed milestones\n- gsd-code-review: Review source files changed during a phase for bugs, security issues, and code quality problems\n- gsd-complete-milestone: Archive completed milestone and prepare for next version\n- gsd-config: Configure GSD settings — workflow toggles, advanced knobs, integrations, and model profile\n- gsd-debug: Systematic debugging with persistent state across context resets\n- gsd-discuss-phase: Gather phase context through adaptive questioning before planning.\n- gsd-docs-update: Generate or update project documentation verified against the codebase\n- gsd-eval-review: Audit an executed AI phase's evaluation coverage and produce an EVAL-REVIEW.md remediation plan.\n- gsd-execute-phase: Execute all plans in a phase with wave-based parallelization\n- gsd-explore: Socratic ideation and idea routing — think through ideas before committing to plans\n- gsd-extract-learnings: Extract decisions, lessons, patterns, and surprises from completed phase artifacts\n- gsd-fast: Execute a trivial task inline — no subagents, no planning overhead\n- gsd-forensics: Post-mortem investigation for failed GSD workflows — diagnoses what went wrong.\n- gsd-graphify: Build, query, and inspect the project knowledge graph in .planning/graphs/\n- gsd-health: Diagnose planning directory health and optionally repair issues\n- gsd-help: Show available GSD commands and usage guide\n- gsd-import: Ingest external plans with conflict detection against project decisions before writing anything.\n- gsd-inbox: Triage and review open GitHub issues and PRs against project templates and contribution guidelines.\n- gsd-ingest-docs: Bootstrap or merge a .planning/ setup from existing ADRs, PRDs, SPECs, and docs in a repo.\n- gsd-manager: Interactive command center for managing multiple phases from one terminal\n- gsd-map-codebase: Analyze codebase with parallel mapper agents to produce .planning/codebase/ documents\n- gsd-milestone-summary: Generate a comprehensive project summary from milestone artifacts for team onboarding and review\n- gsd-mvp-phase: Plan a phase as a vertical MVP slice — user story, SPIDR splitting, then plan-phase\n- gsd-new-milestone: Start a new milestone cycle — update PROJECT.md and route to requirements\n- gsd-new-project: Initialize a new project with deep context gathering and PROJECT.md\n- gsd-ns-context: codebase intelligence | map graphify docs learnings\n- gsd-ns-ideate: exploration capture | explore sketch spike spec capture\n- gsd-ns-manage: config workspace | workstreams thread update ship inbox\n- gsd-ns-project: project lifecycle | milestones audits summary\n- gsd-ns-review: quality gates | code review debug audit security eval ui\n- gsd-ns-workflow: workflow | discuss plan execute verify phase progress\n- gsd-pause-work: Create context handoff when pausing work mid-phase\n- gsd-phase: CRUD for phases in ROADMAP.md — add, insert, remove, or edit phases\n- gsd-plan-phase: Create detailed phase plan (PLAN.md) with verification loop\n- gsd-plan-review-convergence: Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain.\n- gsd-pr-branch: Create a clean PR branch by filtering out .planning/ commits — ready for code review\n- gsd-profile-user: Generate developer behavioral profile and create Claude-discoverable artifacts\n- gsd-progress: Check progress, advance workflow, or dispatch freeform intent — the unified GSD situational command\n- gsd-quick: Execute a quick task with GSD guarantees (atomic commits, state tracking) but skip optional agents\n- gsd-resume-work: Resume work from previous session with full context restoration\n- gsd-review: Request cross-AI peer review of phase plans from external AI CLIs\n- gsd-review-backlog: Review and promote backlog items to active milestone\n- gsd-secure-phase: Retroactively verify threat mitigations for a completed phase\n- gsd-settings: Configure GSD workflow toggles and model profile\n- gsd-ship: Create PR, run review, and prepare for merge after verification passes\n- gsd-sketch: Sketch UI/design ideas with throwaway HTML mockups, or propose what to sketch next (frontier mode)\n- gsd-spec-phase: Clarify WHAT a phase delivers with ambiguity scoring; produces a SPEC.md before discuss-phase.\n- gsd-spike: Spike an idea through experiential exploration, or propose what to spike next (frontier mode)\n- gsd-stats: Display project statistics — phases, plans, requirements, git metrics, and timeline\n- gsd-surface: Toggle which skills are surfaced — apply a profile, list, or disable a cluster without reinstall\n- gsd-thread: Manage persistent context threads for cross-session work\n- gsd-ui-phase: Generate UI design contract (UI-SPEC.md) for frontend phases\n- gsd-ui-review: Retroactive 6-pillar visual audit of implemented frontend code\n- gsd-ultraplan-phase: [BETA] Offload plan phase to Claude Code's ultraplan cloud; review in browser and import back.\n- gsd-undo: Safe git revert. Roll back phase or plan commits using the phase manifest with dependency checks.\n- gsd-update: Update GSD to latest version with changelog display\n- gsd-validate-phase: Retroactively audit and fill Nyquist validation gaps for a completed phase\n- gsd-verify-work: Validate built features through conversational UAT\n- gsd-workspace: Manage GSD workspaces — create, list, or remove isolated workspace environments\n- gsd-workstreams: Manage parallel workstreams — list, create, switch, status, progress, complete, and resume\n- gstack: Router for the gstack skill suite. (gstack)\n- gstack-upgrade: Upgrade gstack to the latest version.\n- guard: Full safety mode: destructive command warnings + directory-scoped edits. (gstack)\n- handoff: Compact the current conversation into a handoff document for another agent to pick up.\n- health: Code quality dashboard. (gstack)\n- improve-codebase-architecture: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable.\n- investigate: Systematic debugging with root cause investigation. (gstack)\n- ios-clean: Remove the DebugBridge SPM package and all #if DEBUG wiring from an iOS app. (gstack)\n- ios-design-review: Visual design audit for iOS apps on real hardware. (gstack)\n- ios-fix: Autonomous iOS bug fixer. (gstack)\n- ios-qa: Live-device iOS QA for SwiftUI apps. (gstack)\n- ios-sync: Regenerate the iOS debug bridge against the latest upstream gstack templates. (gstack)\n- land-and-deploy: Land and deploy workflow. (gstack)\n- landing-report: Read-only queue dashboard for workspace-aware ship. (gstack)\n- learn: Manage project learnings.\n- list-agents: List SPT agents and endpoints reachable from this node. Use when the user says \"list agents\",\n\"who's live\", \"who's listening\", or wants a roster of active perches.\n\n- live: Run a live agent session. For past sessions, restores a summarized context.\n\nEXPLICIT START phrases (route to /sptc:live <id>):\n- \"live as\"\n- \"start live\"\n- \"go live\"\n- \"start a live agent\"\n\nAUTO-RESUME phrases (route to /sptc:live --auto, resumes most-recently-active live agent):\n- continue live work\n- resume live work\n- continue live agent\n- resume live agent\n- live agent continue\n- live agent resume\n- live work continue\n- live work resume\n\nDoes NOT route here (too ambiguous — require BOTH \"live\" AND (\"agent\" or \"work\")):\n- \"keep going\"\n- \"resume work\"\n- \"continue\" (bare)\n\n- make-pdf: Turn any markdown file into a publication-quality PDF. (gstack)\n- mempalace: MemPalace — mine projects and conversations into a searchable memory palace. Use when asked about mempalace, memory palace, mining memories, searching memories, or palace setup.\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- office-hours: YC Office Hours — two modes. (gstack)\n- open-gstack-browser: Launch GStack Browser — AI-controlled Chromium with the sidebar extension baked in.\n- pair-agent: Pair a remote AI agent with your browser. (gstack)\n- plan-ceo-review: CEO/founder-mode plan review. (gstack)\n- plan-design-review: Designer's eye plan review — interactive, like CEO and Eng review. (gstack)\n- plan-devex-review: Interactive developer experience plan review. (gstack)\n- plan-eng-review: Eng manager-mode plan review. (gstack)\n- plan-tune: Self-tuning question sensitivity + developer psychographic for gstack (v1: observational). (gstack)\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- qa: Systematically QA test a web application and fix bugs found. (gstack)\n- qa-only: Report-only QA testing. (gstack)\n- ready: Make this Claude Code session reachable for inter-agent messages (register a perch and\nlisten). Use when the user says \"listen as\", \"ready as\", or wants to receive messages from\nother agents.\n\n- retro: Weekly engineering retrospective. (gstack)\n- review: Pre-landing PR review. (gstack)\n- role: Show or edit this endpoint's durable role — its statement of purpose in the mind. Use when the user\nsays \"show my role\", \"edit my role\", \"update my role\", or runs /sptc:role. Pass a directive to change\nit in words (e.g. /sptc:role make it senior); a bare /sptc:role opens an in-input-box editor\nround-trip on a live session; add --include-desc to also edit the service description.\n\n- scrape: Pull data from a web page. (gstack)\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- search: Instantly search files and folders on Windows via voidtools Everything 1.5a HTTP JSON API. Use when the user wants to find files by name, path, size, date, or extension across the whole system — much faster than Glob/Grep for filesystem-wide queries. Triggers on \"find file\", \"where is\", \"locate\", \"search my drive\", \"find all .ext files\", \"everything search\".\n- send: Send a message to another SPT agent. Use when the user says \"send to\", \"message\",\n\"tell <agent>\", or when you need to reach or reply to another agent yourself.\n\n- setup: Install or repair spt-core AND activate the claude-spt adapter for this Claude Code session\n(mid-session installer + post-install activation).\n\n- setup-browser-cookies: Import cookies from your real Chromium browser into the headless browse session. (gstack)\n- setup-deploy: Configure deployment settings for /land-and-deploy.\n- setup-gbrain: Set up gbrain for this coding agent: install the CLI, initialize a local PGLite or Supabase brain, register MCP, capture per-remote trust policy. (gstack)\n- ship: Ship workflow: detect + merge base branch, run tests, review diff, bump VERSION, update CHANGELOG, commit, push, create PR. (gstack)\n- signoff: Gracefully shut down your live session, saving a final context summary. Use when the user says\n\"sign off\" or \"graceful stop\", or when you (a live agent) are done and want to go offline cleanly.\n\n- skillify: Codify the most recent successful /scrape flow into a permanent browser-skill on disk. (gstack)\n- spec: Turn vague intent into a precise, executable spec in five phases. (gstack)\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- subnet: Manage subnet membership — create a subnet, show a pairing code to invite a machine, or join\nan existing one. Use when the user wants to pair machines, set up a subnet, or reach agents\nacross machines. Cross-machine /sptc:send and live agents depend on it.\n\n- sync-gbrain: Keep gbrain current with this repo's code and refresh agent search guidance in CLAUDE.md. Wraps the gstack-gbrain-sync orchestrator with state (gstack)\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 Claude Code 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- unfreeze: Clear the freeze boundary set by /freeze, allowing edits to all directories again. (gstack)\n- version: Report the spt-core-tracked adapter version (the version-of-truth, not the marketplace plugin\nversion). Use when the user asks the spt or adapter version, or runs /sptc:version.\n\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- 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</skills>\n# Internal URLs\nSpecial URLs for internal resources; with most FS/bash tools they auto-resolve to FS paths.\n- `skill://<name>`: skill instructions; `/<path>` = file within\n- `rule://<name>`: rule details\n- `agent://<id>`: agent output artifact; `/<path>` extracts a JSON field\n- `artifact://<id>`: artifact content\n- `local://<name>.md`: plan artifacts or shared content for subagents\n- `mcp://<uri>`: MCP resource\n- `issue://<N>` (or `issue://<owner>/<repo>/<N>`): GitHub issue, disk-cached. Bare lists recent issues; `?state=open|closed|all&limit=&author=&label=`.\n- `pr://<N>` (or `pr://<owner>/<repo>/<N>`): GitHub PR, same cache; `?comments=0` drops comments. Bare lists recent PRs; `?state=open|closed|merged|all&limit=&author=&label=`.\n- `omp://`: harness docs; AVOID unless the user asks about the harness itself.\n\n# Tool Inventory\n- Read: `read`\n- Grep: `grep`\n- Glob: `glob`\n- Web Search: `web_search`\n- Submit Result: `yield`\n- IRC: `irc`\n- mempalace:mempalace/mempalace_add_drawer: `mcp__mempalace_mempalace_mempalace_add_drawer`\n- mempalace:mempalace/mempalace_check_duplicate: `mcp__mempalace_mempalace_mempalace_check_duplicate`\n- mempalace:mempalace/mempalace_delete_drawer: `mcp__mempalace_mempalace_mempalace_delete_drawer`\n- mempalace:mempalace/mempalace_diary_read: `mcp__mempalace_mempalace_mempalace_diary_read`\n- mempalace:mempalace/mempalace_diary_write: `mcp__mempalace_mempalace_mempalace_diary_write`\n- mempalace:mempalace/mempalace_find_tunnels: `mcp__mempalace_mempalace_mempalace_find_tunnels`\n- mempalace:mempalace/mempalace_get_aaak_spec: `mcp__mempalace_mempalace_mempalace_get_aaak_spec`\n- mempalace:mempalace/mempalace_get_taxonomy: `mcp__mempalace_mempalace_mempalace_get_taxonomy`\n- mempalace:mempalace/mempalace_graph_stats: `mcp__mempalace_mempalace_mempalace_graph_stats`\n- mempalace:mempalace/mempalace_kg_add: `mcp__mempalace_mempalace_mempalace_kg_add`\n- mempalace:mempalace/mempalace_kg_invalidate: `mcp__mempalace_mempalace_mempalace_kg_invalidate`\n- mempalace:mempalace/mempalace_kg_query: `mcp__mempalace_mempalace_mempalace_kg_query`\n- mempalace:mempalace/mempalace_kg_stats: `mcp__mempalace_mempalace_mempalace_kg_stats`\n- mempalace:mempalace/mempalace_kg_timeline: `mcp__mempalace_mempalace_mempalace_kg_timeline`\n- mempalace:mempalace/mempalace_list_rooms: `mcp__mempalace_mempalace_mempalace_list_rooms`\n- mempalace:mempalace/mempalace_list_wings: `mcp__mempalace_mempalace_mempalace_list_wings`\n- mempalace:mempalace/mempalace_search: `mcp__mempalace_mempalace_mempalace_search`\n- mempalace:mempalace/mempalace_status: `mcp__mempalace_mempalace_mempalace_status`\n- mempalace:mempalace/mempalace_traverse: `mcp__mempalace_mempalace_mempalace_traverse`\n- GenerateImage: `generate_image`\n\nTOOL POLICY\n==============\n\n# General\nUse tools whenever they improve correctness, completeness, or grounding.\n- You MUST complete the task using available tools.\n- SHOULD resolve prerequisites before acting.\n- NEVER stop at the first plausible answer if another call would cut uncertainty.\n- Empty, partial, or suspiciously narrow lookup? Retry with a different strategy.\n- SHOULD parallelize independent calls.\n# Tool I/O\n- Prefer relative paths for `path`-like fields.\n- Most tools take `i`: a concise intent, present participle, 2–6 words, no period, capitalized.\n# Specialized Tools\nYou MUST use the specialized tool over its shell equivalent:\n- File or directory reads → `read` (a directory path lists entries).\n- Regex search → `grep`, not `grep`, `rg`, or `awk`.\n- Globbing → `glob`, not `ls **/*.ext` or `fd`.\n# Exploration\nYou NEVER open a file hoping. Hope is not a strategy.\n- You MUST load only what's necessary; AVOID reading files or sections you don't need.\n- Use `grep` to locate targets.\n- Use `glob` to map structure.\n- Use `read` with offset/limit instead of whole-file reads.\n# Delegation\n\nEXECUTION WORKFLOW\n==============\n\n# 1. Scope\n- Read relevant skills first.\n- For multi-file work, plan before touching files; research existing code and conventions first.\n\n# 2. Research Before Editing\n- Read sections, not snippets. You MUST reuse existing patterns; a second convention beside an existing one is PROHIBITED.\n\n- Re-read before acting if a tool fails or a file changed since you read it.\n\n# 3. Decompose\n- Update todos as you go; skip them for trivial requests. Marking a todo done is a transition: start the next in the same turn.\n- NEVER abandon phases under scope pressure—delegate, don't shrink.\n\n- Plan only what makes the request work. Cleanup—changelog, tests, docs—is NOT planned up front; it belongs to the final phase below.\n\n# 4. Implement\n- Fix problems at the source. Remove obsolete code—no leftover comments, aliases, or re-exports.\n- Prefer updating existing files over creating new ones.\n- Review changes from the user's perspective.\n- Grep instead of guessing.\n- Don't run destructive git commands or delete code you didn't write.\n\n# 5. Verify\n- NEVER yield non-trivial work without proof: tests, E2E, browsing, or QA.\n- Every test MUST defend an observable contract and fail on a plausible bug.\n- Test behavior, boundaries, invariants, transitions, precedence, and real errors—not plumbing, source text, or incidental defaults.\n- Match existing conventions; keep tests deterministic, isolated, and full-suite safe.\n- Run only touched tests; small/no-test changes still REQUIRE a focused behavioral smoke test.\n\n# 6. Cleanup\nChangelog, tests, docs, and removing scaffolding are the LAST phase—NEVER skipped, but gated on the request demonstrably working.\n\n- NEVER start, pre-plan, or pre-allocate todos for cleanup before you've made the request work and smoke-tested it. Until then, every edit serves correctness; housekeeping NEVER steers the design.\n- Once your smoke test confirms “it works,” do the cleanup in full before yielding.\n\nDELIVERY CONTRACT\n==============\n\n<contract>\nInviolable.\n- NEVER yield unless the deliverable is complete. A phase boundary, todo flip, or sub-step is NEVER a yield point—continue in the same turn.\n- NEVER fabricate outputs. Claims about code, tools, tests, docs, or sources MUST be grounded.\n- NEVER substitute an easier or more familiar problem:\n  - Don't infer extra scope—retries, validation, telemetry, abstraction “while you're at it”—because it changes the contract.\n  - Don't solve the symptom—suppress a warning or exception, special-case an input—unless asked. Do the real ask.\n- NEVER ask for what tools, repo context, or files can provide.\n- NEVER punt half-solved work back.\n- Default to clean cutover: migrate every caller; leave no shims, aliases, or deprecated paths.\n</contract>\n\n<completeness>\n- “Done” means the deliverable behaves as specified end to end—not that a scaffold compiles or a narrowed test passes.\n- A named plan, phase list, checklist, or spec MUST satisfy every acceptance criterion. A plausible subset is failure, not partial success.\n- NEVER silently shrink scope. Reduce scope only with explicit user approval in this conversation; otherwise do the full work—exhaust every tool and angle.\n- NEVER ship stubs, placeholders, mocks, no-ops, fake fallbacks, or `TODO: implement` as delivered work. If real implementation needs unavailable information, state the missing prerequisite and implement everything else.\n- NEVER relabel unfinished work—“scaffold,” “MVP,” “v1,” “foundation,” “follow-up”—to imply completion. Not done? Say so.\n</completeness>\n\n<evidence-and-output>\n- Output format MUST match the ask.\n- Every claim about code, tools, tests, docs, or sources MUST be grounded.\n- Mark any claim not directly observed or established as `[INFERENCE]`.\n- Verification claims MUST match what was exercised, preferably smoke tested.\n- No required tool lookup may be skipped when it would cut uncertainty.\n- Be brief in prose, not in evidence, verification, or blocking details.\n</evidence-and-output>\n\n<yielding>\nBefore yielding, verify:\n- All requested deliverables are complete; no partial implementation is presented as complete.\n- All affected artifacts—callsites, tests, docs—are updated or intentionally left unchanged.\n- The output and evidence requirements above are satisfied.\n\nBefore declaring blocked:\n- Be sure the information is unreachable through tools, context, or anything in reach. One failing check does not mean blocked—finish all remaining work first.\n- Still stuck? State exactly what's missing and what you tried.\n</yielding>\n<critical>\n- NEVER narrate or consider session limits, token or tool budgets, effort estimates, or how much you can finish. Not your concern—start as if unbounded; execute or delegate.\n- NEVER re-audit an applied edit; NEVER run git subcommands as routine validation. Tool results are THE verification.\n</critical>\n\nROLE\n===================================\n\nInvestigate the codebase rapidly. Return structured findings another agent can use without re-reading everything.\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\nYou are specializing as: **PTY Transport Investigator**. Bring exactly that expertise to the assignment — let it shape how you investigate, decide, and what you produce.\n\nCONTEXT\n===================================\n\n# Goal\nDiagnose why `spt daemon refresh` and `spt update` stall most but not all controlled session PTYs until detach plus `spt rc` reattach.\n# Constraints\nRead-only exploration. Do not run refresh/update, mutate endpoints, or edit files. Skip formatters, linters, and test suites. Use CONTEXT.md, relevant ADRs, docs/KNOWN-HAZARDS.md, and docs/DEBUG-ROLLOUT.md for domain rules.\n# Contract\nReturn exact files/symbols, causal candidates, existing tests, and a proposed safe deterministic feedback loop. Distinguish PTY data-plane ownership, controller/viewer connections, daemon brain refresh, and broker lifecycle.\nCOOP\n===================================\n\nYou are operating on a piece of work assigned to you by the main agent.\n# IRC Peers\nYou can reach other live agents via the `irc` tool. Your id is `PtyControl`. Currently visible peers:\n- `Main` — main (main, running)\n- `RefreshLifecycle` — Daemon Lifecycle Investigator (sub, running)\n- `ReproHarness` — Integration Test Investigator (sub, running)\n\nUse `irc` only for quick coordination, never long-form content. Address peers by id or use `\"all\"` to broadcast.\n- Discovery: the roster above shows each peer's role and what it is doing now; `irc` op:\"list\" refreshes it.\n- Coordination: before you edit a file or start work a sibling may already own, message that peer first — overlapping edits collide.\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\nCOMPLETION\n===================================\n\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 `result.data`.\n- Use non-empty `type: string[]` for incremental, non-terminal sections; calls accumulate by section.\n- Use `type: string` for a terminal result; if data is omitted, your last assistant turn becomes the raw final result.\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 `result.data`.\n\nYour terminal `yield` MUST use exactly this shape — the schema fields go inside `result.data`, NEVER at the top level and NEVER as a stringified summary:\n```ts\nresult: {\n  data: {\n    summary: string;\n    files: { path: string; description: string; }[];\n    architecture: string;\n  };\n}\n```\n\nGiving up is a last resort. If truly blocked, you MUST terminal-yield `result.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\n<workstation>\n- OS: win32 10.0.26200\n- Distro: Windows_NT\n- Kernel: Windows 11 Pro\n- Arch: x64\n- GPU: Parsec Virtual Display Adapter\n- Terminal: Windows Terminal\n- Model: openai-codex/gpt-5.6-sol\n</workstation>\n\n<context>\nYou MUST follow the context files below for all tasks:\n<file path=\"C:\\Users\\decid\\.claude\\CLAUDE.md\">\n## Reference Documents\n\nThe `~/.claude/reference_docs/` directory contains reference guides available across all projects.\n\n- **[claude-code-hooks.md](reference_docs/claude-code-hooks.md)** -- Complete Claude Code hooks reference: all 27 event types, input/output JSON schemas, matcher patterns, exit code behavior, environment variables, and caveats. Consult this when building or debugging hooks.\n</file>\n</context>\nThe context files above are loaded automatically. You NEVER `grep`/`glob` for `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, or similar agent/context files — the relevant ones are already in your context; any others are noise.\nToday is 2026-07-16, and the current working directory is 'C:/Users/decid/Documents/projects/spt-core'.\n\n<critical>\n- Each response MUST advance the task. There is no stopping condition other than completion.\n- You MUST default to informed action; do not ask for confirmation when tools or repo context can answer.\n- You MUST verify the effect of significant behavioral changes before yielding: run the specific test, command, or scenario that covers your change.\n</critical>","task":"Complete the assignment below, thoroughly:\n\n# Target\nBroker PTY control/attach/detach/reattach implementation and tests. Non-goal: daemon update mechanics except their interaction boundary.\n# Change\nTrace controller and viewer stream ownership from `spt rc` attach through broker session tasks. Explain what state produces a frozen terminal that recovers only after intentional detach and a new `spt rc`. Search for cancellation, half-open stream, connection epoch, displaced controller, and broker client reconnection handling.\n# Acceptance\nReturn exact symbols, invariants, existing hazards/tests, and 3-5 falsifiable hypotheses with a safe reproducible harness proposal. No writes or runtime mutation.","tools":["read","grep","glob","web_search","yield","irc","mcp__mempalace_mempalace_mempalace_add_drawer","mcp__mempalace_mempalace_mempalace_check_duplicate","mcp__mempalace_mempalace_mempalace_delete_drawer","mcp__mempalace_mempalace_mempalace_diary_read","mcp__mempalace_mempalace_mempalace_diary_write","mcp__mempalace_mempalace_mempalace_find_tunnels","mcp__mempalace_mempalace_mempalace_get_aaak_spec","mcp__mempalace_mempalace_mempalace_get_taxonomy","mcp__mempalace_mempalace_mempalace_graph_stats","mcp__mempalace_mempalace_mempalace_kg_add","mcp__mempalace_mempalace_mempalace_kg_invalidate","mcp__mempalace_mempalace_mempalace_kg_query","mcp__mempalace_mempalace_mempalace_kg_stats","mcp__mempalace_mempalace_mempalace_kg_timeline","mcp__mempalace_mempalace_mempalace_list_rooms","mcp__mempalace_mempalace_mempalace_list_wings","mcp__mempalace_mempalace_mempalace_search","mcp__mempalace_mempalace_mempalace_status","mcp__mempalace_mempalace_mempalace_traverse","generate_image"],"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"}}}}
{"type":"message","id":"eee06fec","parentId":"38b5bd7c","timestamp":"2026-07-16T09:55:28.657Z","message":{"role":"user","content":[{"type":"text","text":"Complete the assignment below, thoroughly:\n\n# Target\nBroker PTY control/attach/detach/reattach implementation and tests. Non-goal: daemon update mechanics except their interaction boundary.\n# Change\nTrace controller and viewer stream ownership from `spt rc` attach through broker session tasks. Explain what state produces a frozen terminal that recovers only after intentional detach and a new `spt rc`. Search for cancellation, half-open stream, connection epoch, displaced controller, and broker client reconnection handling.\n# Acceptance\nReturn exact symbols, invariants, existing hazards/tests, and 3-5 falsifiable hypotheses with a safe reproducible harness proposal. No writes or runtime mutation."}],"attribution":"agent","timestamp":1784195728654}}
{"type":"message","id":"13c973fe","parentId":"eee06fec","timestamp":"2026-07-16T09:55:40.751Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning skill diagnosis and context reading**"},{"type":"toolCall","id":"call_pmSaBrERJwEaZKP2GkE5S0D2|fc_008390f508d1d09b016a58aa9cb6f8819a93e48c0742f63049","name":"read","arguments":{"i":"Loading diagnosis workflow","path":"skill://diagnose","selector":null}},{"type":"toolCall","id":"call_hE92UFxA2QAxZpsIECVYx4IW|fc_008390f508d1d09b016a58aa9cb704819aa755d042346c062d","name":"glob","arguments":{"i":"Mapping broker PTY files","path":"**/*{broker,pty,session,attach,control}*","hidden":true,"gitignore":true,"limit":300}},{"type":"toolCall","id":"call_piYTXs66Njm2AHoGIh2MYXoH|fc_008390f508d1d09b016a58aa9cb70c819ab2558d7cdf85260e","name":"glob","arguments":{"i":"Locating required domain docs","path":"{CONTEXT.md,docs/KNOWN-HAZARDS.md,docs/DEBUG-ROLLOUT.md,docs/adr/**,docs/ADR/**}","hidden":true,"gitignore":true,"limit":200}},{"type":"toolCall","id":"call_AeayJf1T51MfAMyGJEp5EQdn|fc_008390f508d1d09b016a58aa9cb714819a98fc36317c529055","name":"grep","arguments":{"i":"Finding attach control symbols","pattern":"\\b(attach|detach|reattach|controller|viewer|displaced|epoch|half[-_ ]open|cancel|CancellationToken|reconnect)\\b","path":"src;crates;packages;tests;test","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":14545,"output":292,"cacheRead":0,"cacheWrite":0,"totalTokens":14837,"reasoningTokens":46,"cost":{"input":0.07272500000000001,"output":0.00876,"cacheRead":0,"cacheWrite":0,"total":0.08148500000000002}},"stopReason":"toolUse","timestamp":1784195728714,"responseId":"resp_008390f508d1d09b016a58aa912a9c819ab85940a2b8da7ff4","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_008390f508d1d09b016a58aa977478819a934b8941b0dd4536","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKqcWujIPKZwQirpmGO1TR7b-x09qV4QSf7ZE5Kj4Wl8IhqJ8KqgUSGJ4Jzx2vrq9qRMY46MuZMH0Y81KBLKqSFzRabzFh1Cbx5xEYJkusojt607qg4Hxokibx6v8IpENW2AQ3Yl8y0DGg6jxYZgNEUv0ahrZqEJwWB1mp3dE7PA3js2UxduOfdvV_WZXch92K2mqjRjx4TbvbF7ylgYVcuFoUgkNpkI65tfURfnLXn0L5qWFn3cxb7GNkknKxfYpSjOIatQRGb9T1vrI015BTCM3Y8qNCY60QdOWdWP0NA-Olz3KJeamX7WGeRIDq3HshAz2X_BYYahCCvyTvbaSh01xRKkcSVc5-kS3eQ_bNYK-Jf3IFPUFQein9B5k51_BcXv1oNmvoyDqBfLZHqXOm7B_QdpSnN6DFZ34uFbmh3Ms05wT7sF5GZoeAZACdgJvMNsKk7XOVGZ8K66S3UIUbhZAsrnQRuIvDSJIn9GvfRyk1i0YxUTdesyxXITe-O3SuuNV6YBYUeCsNs3omHRyLH6waturJuOg6VbR94lCG0QEs22eTh6UC9po7eUeL_swSByN5md2fULc6nUY2Wpiv9b95Es1UPk3B838VmsECznd65GMO9l82iS3wEsdRWYsNbhYMZwnWEqfYBqPFxNOwT0HpW0ueKPrq2WJ0q3xJ4ZZcNlA5Y1_VDsf5zD0M_7FfHpUq687lB_2ExZZrd9ZeOE-kaTChhWBxyLqQ0y_WMLh67zUjSvmyGcotD0mGjaXbapso55YRtO27iAZbYA-AIkM9fA25tbtQNf_D5eMOWRvqu5SWBrw48KVFVJBVIerYVfT3_73iePLiAcfwujbaoAe_KKTFI9xKaBmi8UUPQ1EOobUG1FYQM9ndY7d0dbZJpTLuZRp6dr27fBl4HrYEkeWnkuH9_ObsuNai6do_M_tHQ1YYRL2oUsES1KqlrGOcw1yskTaIX4Eyh4v3CgvC5MQbkLMi55G08iboFW7ht4cEFXVh5bf0tm2uxNi3bCp5lGW07v5MeJwfjEpCboHefvUKzPktpzAPT2g5mxLRtWZ7NCIiTbqsb9awygu3HXQCgN88-p-js9N5bVPETjA7qGCBr3Hi1xBbhUp6WkssSXvrWq9jXXkAhf7dw8HD09PF12Tb9xHxtKLENo1OlxcNCrRqslvI-QHVQXJXJnQqpd2ZuQ7er0rsZoClwXxKFpprruAT8jJReKgDBOPmNveBG7UA==","summary":[{"type":"summary_text","text":"**Planning skill diagnosis and context reading**"}]},{"id":"fc_008390f508d1d09b016a58aa9cb6f8819a93e48c0742f63049","type":"function_call","status":"completed","arguments":"{\"i\":\"Loading diagnosis workflow\",\"path\":\"skill://diagnose\",\"selector\":null}","call_id":"call_pmSaBrERJwEaZKP2GkE5S0D2","name":"read"},{"id":"fc_008390f508d1d09b016a58aa9cb704819aa755d042346c062d","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping broker PTY files\",\"path\":\"**/*{broker,pty,session,attach,control}*\",\"hidden\":true,\"gitignore\":true,\"limit\":300}","call_id":"call_hE92UFxA2QAxZpsIECVYx4IW","name":"glob"},{"id":"fc_008390f508d1d09b016a58aa9cb70c819ab2558d7cdf85260e","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating required domain docs\",\"path\":\"{CONTEXT.md,docs/KNOWN-HAZARDS.md,docs/DEBUG-ROLLOUT.md,docs/adr/**,docs/ADR/**}\",\"hidden\":true,\"gitignore\":true,\"limit\":200}","call_id":"call_piYTXs66Njm2AHoGIh2MYXoH","name":"glob"},{"id":"fc_008390f508d1d09b016a58aa9cb714819a98fc36317c529055","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding attach control symbols\",\"pattern\":\"\\\\b(attach|detach|reattach|controller|viewer|displaced|epoch|half[-_ ]open|cancel|CancellationToken|reconnect)\\\\b\",\"path\":\"src;crates;packages;tests;test\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_AeayJf1T51MfAMyGJEp5EQdn","name":"grep"}]},"duration":12036.800200000405,"ttft":6718.526200000197,"contextSnapshot":{"promptTokens":14545,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_pmSaBrERJwEaZKP2GkE5S0D2|fc_008390f508d1d09b016a58aa9cb6f8819a93e48c0742f63049","toolName":"read","startedAt":"2026-07-16T09:55:40.751Z","args":{"path":"skill://diagnose"},"intent":"Loading diagnosis workflow"},"id":"24099c44","parentId":"13c973fe","timestamp":"2026-07-16T09:55:40.751Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_hE92UFxA2QAxZpsIECVYx4IW|fc_008390f508d1d09b016a58aa9cb704819aa755d042346c062d","toolName":"glob","startedAt":"2026-07-16T09:55:40.751Z","args":{"path":"**/*{broker,pty,session,attach,control}*"},"intent":"Mapping broker PTY files"},"id":"362e73d8","parentId":"24099c44","timestamp":"2026-07-16T09:55:40.751Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_piYTXs66Njm2AHoGIh2MYXoH|fc_008390f508d1d09b016a58aa9cb70c819ab2558d7cdf85260e","toolName":"glob","startedAt":"2026-07-16T09:55:40.752Z","args":{"path":"{CONTEXT.md,docs/KNOWN-HAZARDS.md,docs/DEBUG-ROLLOUT.md,docs/adr/**,docs/ADR/**}"},"intent":"Locating required domain docs"},"id":"84cd33c5","parentId":"362e73d8","timestamp":"2026-07-16T09:55:40.752Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_AeayJf1T51MfAMyGJEp5EQdn|fc_008390f508d1d09b016a58aa9cb714819a98fc36317c529055","toolName":"grep","startedAt":"2026-07-16T09:55:40.752Z","args":{"path":"src;crates;packages;tests;test"},"intent":"Finding attach control symbols"},"id":"272a41ab","parentId":"84cd33c5","timestamp":"2026-07-16T09:55:40.752Z"}
{"type":"message","id":"e7aa4054","parentId":"272a41ab","timestamp":"2026-07-16T09:55:40.752Z","message":{"role":"toolResult","toolCallId":"call_pmSaBrERJwEaZKP2GkE5S0D2|fc_008390f508d1d09b016a58aa9cb6f8819a93e48c0742f63049","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.\n"}],"details":{"resolvedPath":"C:\\Users\\decid\\.claude\\skills\\diagnose\\SKILL.md","contentType":"text/markdown","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\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,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]},"meta":{"source":{"type":"internal","value":"skill://diagnose"}}},"isError":false,"timestamp":1784195740752}}
{"type":"message","id":"9bd04b52","parentId":"e7aa4054","timestamp":"2026-07-16T09:55:40.768Z","message":{"role":"toolResult","toolCallId":"call_piYTXs66Njm2AHoGIh2MYXoH|fc_008390f508d1d09b016a58aa9cb70c819ab2558d7cdf85260e","toolName":"glob","content":[{"type":"text","text":"CONTEXT.md\n# docs/\nKNOWN-HAZARDS.md\nDEBUG-ROLLOUT.md\n## adr/\n0037-materialized-project-index.md\n0036-private-fork-distribution.md\n0005-totp-seeded-spake2-node-pairing.md\n0031-server-side-screen-grid-render-repaint.md\n0034-per-minter-op-id-namespacing.md\n0033-wake-resume-via-reconcile-intent.md\n0025-live-daemon-coordinated-adapter-update.md\n0022-spt-hosted-idle-delivery-translation-binary.md\n0032-boundary-rotation-credential.md\n0030-robust-wan-subnet-join.md\n0029-adapter-update-arc-resolve-not-execute.md\n0028-activity-gated-delivery-and-send-modifier-axes.md\n0027-unbound-endpoint-state-and-attach-on-session.md\n0026-multi-subnet-home-at-endpoint-run-creation.md\n0024-multi-platform-adapter-spt-packaging.md\n0023-always-on-endpoints-resident-supervised-binary-sigil-addressing.md\n0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md\n0020-event-envelope-sole-arriving-format-reply-to-removed.md\n0012-psyche-trust-boundary-stdout-event-outbound.md\n0009-endpoint-access-control-model.md\n0008-live-activity-buffer-pty-digest.md\n0019-digest-adapter-declared-extractor-session-spanning.md\n0018-broker-brain-process-isolation-restoration.md\n0016-platform-targeted-update-sets.md\n0017-subnet-mesh-membership-seed-proof.md\n0014-publish-target-repo-and-lapse-proof-canonical-url.md\n0015-release-signing-two-key-trust-anchor.md\n0013-psyche-reconciled-context-merge.md\n0010-immutable-home-subnet-fork-not-rehome.md\n0011-branchstore-git-kv-state-substrate.md\n0006-multi-subnet-membership-model.md\n0007-subnet-notification-primitive.md\n0003-multi-instance-node-anchored-identity-model.md\n0004-single-daemon-broker-brain-split-and-self-update.md\n0002-bake-networking-into-core-collapse-spt-node.md\n0001-clean-room-fork-from-claude-skill-owl.md"}],"details":{"scopePath":".","fileCount":39,"files":["CONTEXT.md","docs/adr/0037-materialized-project-index.md","docs/adr/0036-private-fork-distribution.md","docs/KNOWN-HAZARDS.md","docs/adr/0005-totp-seeded-spake2-node-pairing.md","docs/adr/0031-server-side-screen-grid-render-repaint.md","docs/adr/0034-per-minter-op-id-namespacing.md","docs/adr/0033-wake-resume-via-reconcile-intent.md","docs/adr/0025-live-daemon-coordinated-adapter-update.md","docs/adr/0022-spt-hosted-idle-delivery-translation-binary.md","docs/adr/0032-boundary-rotation-credential.md","docs/adr/0030-robust-wan-subnet-join.md","docs/adr/0029-adapter-update-arc-resolve-not-execute.md","docs/adr/0028-activity-gated-delivery-and-send-modifier-axes.md","docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md","docs/adr/0026-multi-subnet-home-at-endpoint-run-creation.md","docs/adr/0024-multi-platform-adapter-spt-packaging.md","docs/adr/0023-always-on-endpoints-resident-supervised-binary-sigil-addressing.md","docs/adr/0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md","docs/adr/0020-event-envelope-sole-arriving-format-reply-to-removed.md","docs/adr/0012-psyche-trust-boundary-stdout-event-outbound.md","docs/adr/0009-endpoint-access-control-model.md","docs/adr/0008-live-activity-buffer-pty-digest.md","docs/adr/0019-digest-adapter-declared-extractor-session-spanning.md","docs/adr/0018-broker-brain-process-isolation-restoration.md","docs/adr/0016-platform-targeted-update-sets.md","docs/adr/0017-subnet-mesh-membership-seed-proof.md","docs/adr/0014-publish-target-repo-and-lapse-proof-canonical-url.md","docs/adr/0015-release-signing-two-key-trust-anchor.md","docs/adr/0013-psyche-reconciled-context-merge.md","docs/adr/0010-immutable-home-subnet-fork-not-rehome.md","docs/adr/0011-branchstore-git-kv-state-substrate.md","docs/adr/0006-multi-subnet-membership-model.md","docs/adr/0007-subnet-notification-primitive.md","docs/adr/0003-multi-instance-node-anchored-identity-model.md","docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","docs/adr/0002-bake-networking-into-core-collapse-spt-node.md","docs/DEBUG-ROLLOUT.md","docs/adr/0001-clean-room-fork-from-claude-skill-owl.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1784195740768}}
{"type":"message","id":"ae385620","parentId":"9bd04b52","timestamp":"2026-07-16T09:55:40.769Z","message":{"role":"toolResult","toolCallId":"call_hE92UFxA2QAxZpsIECVYx4IW|fc_008390f508d1d09b016a58aa9cb704819aa755d042346c062d","toolName":"glob","content":[{"type":"text","text":"# crates/\n## spt/tests/\nresume_no_control_steal_e2e.rs\nattach_wedge_e2e.rs\nrun_no_dup_session_e2e.rs\n## spt-daemon/\n### src/\nbroker.rs\nattach.rs\n### tests/\ninject_control_wedge.rs\nnetbroker.rs\nbroker.rs\nattach.rs\ncontrol_stamp_lifetime.rs\n## spt-store/src/\nsessions.rs\nattachment.rs\n## spt-term/src/\npty.rs\n## spt-net/src/net/\nattach.rs\n# docs/\n## adr/\n0027-unbound-endpoint-state-and-attach-on-session.md\n0009-endpoint-access-control-model.md\n0008-live-activity-buffer-pty-digest.md\n0019-digest-adapter-declared-extractor-session-spanning.md\n0018-broker-brain-process-isolation-restoration.md\n0004-single-daemon-broker-brain-split-and-self-update.md\n## spikes/\nSPIKE-04-forkpty-parity.md\nSPIKE-01-broker-handoff.md"}],"details":{"scopePath":".","fileCount":22,"files":["crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/tests/inject_control_wedge.rs","crates/spt/tests/attach_wedge_e2e.rs","crates/spt-store/src/sessions.rs","crates/spt-daemon/tests/netbroker.rs","crates/spt-daemon/tests/broker.rs","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/src/attach.rs","crates/spt-term/src/pty.rs","crates/spt-store/src/attachment.rs","crates/spt-net/src/net/attach.rs","crates/spt-daemon/tests/control_stamp_lifetime.rs","crates/spt/tests/run_no_dup_session_e2e.rs","docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md","docs/adr/0009-endpoint-access-control-model.md","docs/adr/0008-live-activity-buffer-pty-digest.md","docs/adr/0019-digest-adapter-declared-extractor-session-spanning.md","docs/adr/0018-broker-brain-process-isolation-restoration.md","docs/spikes/SPIKE-04-forkpty-parity.md","docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","docs/spikes/SPIKE-01-broker-handoff.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1784195740769}}
{"type":"message","id":"e915002d","parentId":"ae385620","timestamp":"2026-07-16T09:55:40.876Z","message":{"role":"toolResult","toolCallId":"call_AeayJf1T51MfAMyGJEp5EQdn|fc_008390f508d1d09b016a58aa9cb714819a98fc36317c529055","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## access.rs\n 41|//! message traffic.\n*42|//! Control surfaces (terminal attach, file transfer) are never replies; their\n 43|//! serves always gate as [`InboundClass::Unsolicited`].\n 44|\n 45|use spt_store::access::{AccessStore, RecentOutbound, REPLY_WINDOW_MS};\n## applyhost.rs\n 320|    // brain-restart trigger rides after the swap (ADR-0018 D3-3); it does not\n*321|    // attach to any session (the broker holds every hosted endpoint across the\n 322|    // brain cycle — that is the whole point).\n 323|    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n 324|\n...\n 451|/// the planned brain-restart trigger (ADR-0018 D3-3) rides after the binary\n*452|/// swap. It does NOT attach to a session: the broker holds every hosted endpoint\n 453|/// across the brain cycle, so apply never touches one. `Err` means the broker\n 454|/// never answered (a dead broker fails the apply with the seat untouched).\n 455|fn connect_retry(broker_name: &str) -> io::Result<Brain> {\n## attach.rs\n*1|//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport\n 2|//! onto a local session for an operator on another node.\n 3|//!\n 4|//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\n...\n 20|//! eats the operator's `Input` records (both sides starve; found the hard way\n*21|//! in `tests/attach.rs`). Idempotency holds without it because each direction\n 22|//! dedups at its **effect**: the PTY write is journaled by the operator-minted\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...\n 46|\n*47|use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\n 48|use spt_net::net::ndjson;\n 49|\n 50|use crate::access::{access_check, AccessDecision, InboundClass};\n...\n 54|\n*55|/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a\n 56|/// served viewport is a **driver** — attaching wakes the endpoint, the\n 57|/// viewport ending detaches it (active → dormant; the transition echo is\n 58|/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\n...\n 68|pub enum AttachServeOutcome {\n*69|    /// The access gate refused the requesting origin, OR a `Control` attach hit\n 70|    /// an endpoint a DIFFERENT operator already controls (busy-refuse,\n 71|    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the\n 72|    /// busy guidance client-side from the endpoint's `driven_by`.\n...\n 79|    Detached,\n*80|    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n*81|    /// (REQ-KICK-1): the loud `Displaced` notice was sent and the stream\n 82|    /// finished. The endpoint stays driven (by the taker) — no dormant edge.\n*83|    Displaced,\n 84|}\n 85|\n 86|/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\n*87|/// arrives — gates wake (controller-only) and PTY input.\n 88|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 89|enum ServeRole {\n*90|    /// The interactive controller (input + resize; wakes the endpoint).\n*91|    Controller,\n*92|    /// A read-only viewer (output only; wake-neutral).\n*93|    Viewer,\n 94|}\n 95|\n*96|/// Minimum spacing between skip-to-live re-subscribes after a viewer eviction\n*97|/// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). HARD constraint: an evicted viewer that is\n 98|/// hopelessly behind under a sustained flood will be re-evicted immediately after\n 99|/// each re-subscribe; without spacing that is an evict→resubscribe CPU spin. With\n 100|/// it, the operator sees an intermittent LIVE burst each interval (forward\n...\n 103|\n*104|/// The CONTROLLER fell behind the live ring further than the ring holds — the\n 105|/// dropped frames are GONE (`tail - delivered_through > cap_chunks`), so an\n 106|/// exactly-once re-fetch is IMPOSSIBLE. Distinguishes the irrecoverable edge from\n 107|/// an ordinary fatal so the serve loop can surface it (a marked truncation),\n 108|/// rather than silent-skipping (a B2 lie) or spinning. Full graceful handling is\n*109|/// DEFERRED to `REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND`.\n*110|// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n 111|#[derive(Debug)]\n 112|pub struct ControllerIrrecoverablyBehind {\n 113|    /// The frozen `delivered_through` floor the ring has already rolled past.\n...\n 120|            f,\n*121|            \"controller fell irrecoverably behind the live ring at seq {} (dropped frames rolled out of the ring)\",\n 122|            self.floor\n 123|        )\n 124|    }\n...\n 129|/// Is `e` the brain's strict reject-gap fatal (`output gap: got seq N want M`)?\n*130|/// That is the only error the controller gap-resume recovers from; every other\n 131|/// error propagates unchanged.\n 132|fn is_output_gap(e: &io::Error) -> bool {\n 133|    e.kind() == io::ErrorKind::InvalidData && e.to_string().contains(\"output gap\")\n## autostart.rs\n 6|//! operator marks an endpoint a **startup default** with `spt endpoint run\n*7|//! --save` (the endpoint analog of the shipped `subnet attach --save`\n 8|//! precedent — doyle-ruled shape (a)); the daemon replays the saved runs here\n 9|//! at start.\n 10|//!\n## brain.rs\n 56|};\n*57|use spt_net::net::attach::AttachIntent;\n 58|use crate::transport::{send_hello, LocalSocketTransport};\n 59|\n*60|/// Wall-clock now in epoch milliseconds — the source for `gen_start` (matches\n*61|/// the epoch-ms stamping used elsewhere in the tree; no date dependency).\n 62|pub fn now_ms() -> u64 {\n 63|    SystemTime::now()\n 64|        .duration_since(UNIX_EPOCH)\n...\n 68|\n*69|/// The brain's durable state across a handoff: enough to re-attach gaplessly and\n 70|/// advance the generation. `gen_start_ms` is recorded for observability but is\n 71|/// **deliberately not authoritative on restart** — [`Brain::handoff`] sets a\n 72|/// fresh `gen_start` from `now()` (2.4).\n...\n 107|    Exit { session_id: u64, code: Option<i32> },\n*108|    /// The resolved controller/viewer role of a [`Brain::attach_as`] subscribe\n*109|    /// (REQ-RCVIEW-1 / REQ-KICK-1). The serving brain wakes (controller-only),\n*110|    /// stays wake-neutral (viewer), or refuses the operator (busy).\n 111|    Subscribed {\n 112|        session_id: u64,\n 113|        outcome: SubscribeOutcome,\n 114|    },\n*115|    /// The current PTY size of a session, pushed to a VIEWER (the letterbox feed,\n 116|    /// REQ-RCVIEW-1).\n 117|    Size {\n 118|        session_id: u64,\n...\n 121|    },\n*122|    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n 123|    /// (REQ-KICK-1). The serving brain forwards a loud notice and detaches.\n*124|    Displaced { session_id: u64, by: String },\n*125|    /// This VIEWER's bounded subscription queue overflowed and it was evicted\n*126|    /// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). DISTINCT from a session-exit EOF: the\n 127|    /// serving brain re-subscribes from the current ring floor (skip-to-live)\n*128|    /// rather than tearing the attach down.\n 129|    ViewerEvicted { session_id: u64 },\n 130|    /// An idempotent effect is durably applied (or was deduped — `applied_now`\n 131|    /// distinguishes). The exactly-once ack for a [`Brain::send_effect`] (B5).\n...\n 301|    io_timeout: Option<Duration>,\n*302|    /// #6 (ADR-0031): a COLD attach (`from_seq == 0`, set by [`Brain::attach`] /\n 303|    /// [`Brain::attach_as`]) receives a synthesized clean-screen REPAINT as its\n 304|    /// first `KIND_OUTPUT` frame, at pseudo-seq `broker.next_seq - 1` — NOT seq 0.\n 305|    /// When `true`, the strict legacy `next_seq` path adopts that first frame's seq\n...\n 369|    /// caller (the brain-only update engine, `idempotent`/`daemon_e2e`/`handoff`\n*370|    /// integration tests) replays output through it with no later `attach`. So it\n*371|    /// stays. The P1c controller-writer reorder (a `serve_attach` re-serve that\n 372|    /// ALSO subscribes from 0 over this same socket → two racing\n 373|    /// `controller_writer`s) is fixed in the BROKER, not by dropping this\n*374|    /// subscribe: the epoch gate ([`OutputLog::become_controller`]) guarantees a\n 375|    /// single live writer per connection, and the seeded `session_cursors` below\n 376|    /// routes the consumer through the dedup-below + snap-above path so any\n 377|    /// at-least-once boundary re-send is tolerated, never a fatal reject-gap\n*378|    /// ([`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]).\n 379|    ///\n 380|    /// **Test-only (restoration D4-3).** The production resume path is\n 381|    /// [`Brain::cold_start`] + [`Brain::resume_sessions`] (broker cursor-of-record),\n...\n 383|    /// tests. See [`BrainState`].\n*384|    // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n 385|    pub fn handoff(name: &str, prior: BrainState, now_ms: u64) -> io::Result<Self> {\n 386|        let mut conn = LocalSocketTransport::connect(name)?;\n 387|        send_hello(&mut conn, Role::Brain)?;\n...\n 409|\n*410|    /// The wall-clock generation start (epoch ms) — always this generation's own\n 411|    /// start time, never a rehydrated value.\n 412|    pub fn gen_start_ms(&self) -> u64 {\n 413|        self.gen_start_ms\n## brainproc.rs\n 181|    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\n*182|    // continuity from the broker — re-attach EVERY hosted session in resume mode\n 183|    // from its delivered cursor, the production replacement for the retired\n 184|    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n 185|    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n...\n 278|        }\n*279|        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n 280|        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n 281|        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n*282|        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n 283|        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n 284|        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\n 285|        // window, so without this the wedge never drains: the promotion DRAINED gate\n...\n 473|    /// Whether the OLD brain generation's broker subscriber connection is DRAINED —\n*474|    /// its local controller conn CLOSED or stall-evicted, no blocked writes still\n 475|    /// pending on it (REQ-UPDATE-PROMOTE-DRAINED). Reads BROKER TRUTH in-process (no\n 476|    /// brain round-trip): the supervisor promotes a ready candidate only once this is\n 477|    /// `true`, so `brain.ready` arriving inside the W2 `BRAIN_WRITE_DEADLINE` window\n...\n 572|    fn old_gen_drained(&self) -> bool {\n*573|        // Drained ⟺ no hosted session has a wedged LOCAL (brain-own) controller conn.\n 574|        // No broker (net-less / bind-failed) → no control plane → drained. A broker\n*575|        // with no wedged local controller (the common case, incl. no daemon-hosted\n 576|        // sessions) reads drained at once → the normal fast promote on `brain.ready`.\n 577|        self.broker\n 578|            .as_ref()\n...\n 592|/// = the A11 kill+wait precedes `rollback`), so no brain-side pump can be\n*593|/// advertising and the [`spt_store::epoch::EpochSource`] single-writer assumption\n*594|/// (epoch.rs) holds at this `EpochSource::load`. **Do NOT fire broker-side notifs\n 595|/// while a brain is live:** concurrent `EpochSource` read-modify-write across the\n 596|/// two processes mints duplicate epochs (KH 4.8 lost-update). The forward fix for\n 597|/// any future broker-side live-notif is cross-process serialization (a file\n...\n 603|    };\n*604|    let mut epochs = spt_store::epoch::EpochSource::load();\n 605|    let policy = crate::notif::NotifSurfacePolicy::load();\n 606|    let node_hex = crate::presence::local_node_hex();\n 607|    let owlery = spt_store::perch::owlery_dir();\n...\n 668|/// `BRAIN_WRITE_DEADLINE`, so a new brain can signal ready while the OLD generation's\n*669|/// local controller conn is still wedged (the 22:47 apply \"promoted\" onto a frozen\n 670|/// control plane). So readiness is LATCHED (`ready_seen` — a candidate that stamped\n 671|/// ready then exited is not demoted, preserving the ready-then-exit-still-promotes\n 672|/// semantic) and promotion waits for `env.old_gen_drained()` too. While ready-seen\n## broker.rs\n 13|//! crash). The broker — and every hosted child — must survive that untouched,\n*14|//! and the new brain must re-attach to the output stream **gaplessly and\n 15|//! exactly-once** (Spike #1 made real). Two mechanisms:\n 16|//!\n 17|//! - **Sequenced, buffered output.** Each session's output is appended to an\n...\n 44|\n*45|use spt_net::net::attach::AttachIntent;\n 46|use spt_net::net::endpoint::EndpointAddr;\n 47|use spt_store::perch::{resolve_perch_path, ParentHint};\n 48|\n...\n 89|\n*90|/// The viewer-channel depth (frames). A bounded SPSC queue per viewer; when it\n*91|/// OVERFLOWS the viewer is evicted — the drain `try_send`s and never blocks\n*92|/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render\n*93|/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n*94|/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n*95|/// owns the replay batch), so a viewer attaching to a busy session is not\n 96|/// spuriously evicted.\n 97|const VIEWER_CHANNEL_DEPTH: usize = 256;\n 98|\n*99|/// Soft cap on concurrent viewers per session (REQ-HAZARD-VIEWER-ISOLATION:\n*100|/// bounds the writer-thread count). A `Viewer` attach beyond the cap is refused.\n 101|const MAX_VIEWERS: usize = 32;\n 102|\n*103|/// Whether a viewer's bounded-queue `try_send` result demands EVICTION\n*104|/// (REQ-HAZARD-VIEWER-ISOLATION): a `Full` queue (the viewer can't keep up with\n 105|/// the live stream) or a `Disconnected` one (its writer thread ended after a\n*106|/// dead socket). The drain evicts on either and NEVER blocks on a viewer — this\n 107|/// is the pure decision the fan-out applies under the log lock.\n*108|// [impl->REQ-HAZARD-VIEWER-ISOLATION]\n 109|fn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {\n 110|    r.is_err()\n 111|}\n 112|\n*113|/// One read-only viewer's isolated sink (REQ-HAZARD-VIEWER-ISOLATION). The drain\n 114|/// thread `try_send`s output frames into `tx` (a bounded queue); a dedicated\n*115|/// `writer` thread does the BLOCKING socket write, so a wedged viewer backs up\n 116|/// only its own queue — never the drain. When the queue overflows the sink is\n 117|/// dropped (the `tx` closes → the writer ends) and removed from the map.\n 118|struct ViewerSink {\n*119|    /// The bounded live-frame queue to this viewer's writer thread.\n 120|    tx: SyncSender<Envelope>,\n*121|    /// The viewer's IPC send half — kept ONLY for identity matching on detach\n*122|    /// (a dropped brain connection removes its viewer rows); never written here.\n 123|    send: SharedSend,\n*124|    /// Set TRUE when this viewer is EVICTED for overflow (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT),\n 125|    /// distinguishing an eviction from a normal teardown (session end / conn drop).\n 126|    /// `append` stores `true` BEFORE dropping the sink (and thus `tx`); the writer,\n 127|    /// on its `rx.recv()` Err, reads this and — when true — emits a\n## codec.rs\n 64|    fn frame_round_trips_through_buffer() {\n*65|        let env = Envelope::new(\"attach\", json!({ \"session\": \"s1\", \"n\": 7 }));\n 66|        let mut buf: Vec<u8> = Vec::new();\n 67|        write_frame(&mut buf, &env).unwrap();\n 68|\n...\n 71|        assert_eq!(got.protocol_version, IPC_PROTOCOL_VERSION);\n*72|        assert_eq!(got.kind, \"attach\");\n 73|        assert_eq!(got.payload, json!({ \"session\": \"s1\", \"n\": 7 }));\n 74|    }\n 75|\n## config.rs\n 23|\n*24|/// Default drop-file precedence protection window (epoch-ms), mirroring the\n 25|/// interim `spt_live` guard. Surfaced here so the one config source owns it.\n 26|pub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 2_000;\n 27|\n...\n 67|/// REQ-ENDPOINT-AUTOSTART): the daemon REPLAYS these at start — the endpoint\n*68|/// analog of the shipped `subnet attach/detach --save` precedent. Minimal by\n 69|/// design: the adapter option is re-resolved fresh at each replay (an adapter\n 70|/// update between restarts is honored, an uninstalled one loud-skips), and the\n 71|/// endpoint's home subnet is NOT recorded (home is durable on the perch —\n...\n 134|    pub pulse_period: Duration,\n*135|    /// The drop-file precedence protection window in epoch-ms (6.5).\n 136|    pub protection_window_ms: u64,\n 137|    /// The orphan-watch grace window (B7): the recovery wait before a\n 138|    /// supervised-crash teardown rechecks liveness and signs off (1.1).\n...\n 180|    /// REQ-SUBNET-5): the daemon's boot reset seeds the live\n*181|    /// `attachment.json` from this list — `spt subnet detach/attach --save`\n 182|    /// writes it. Default empty = every held subnet starts attached.\n 183|    pub detached_subnets: Vec<String>,\n 184|    /// The worker-reap TTL floor (seconds since a worker's `started`), W-3\n## conn.rs\n 11|//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n*12|//! logical stall-evict (`stall_evict_controller`) releases only the controller\n 13|//! ROLE — it never cancels the in-flight pipe write or closes the physical\n 14|//! connection, so the writer (and every write queued behind the same gate)\n 15|//! stays wedged until the peer process dies (~128 s in the field capture, on a\n...\n 53|//! data), followed by one more `CancelIoEx` covering a read re-issued in the\n*54|//! cancel→disconnect window.\n 55|//!\n 56|//! No new output queue (the bounded per-sink queues + isolated writer threads\n 57|//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\n...\n 95|/// Wall-clock + monotonic stamp for daemon-stderr correlation records\n*96|/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\n 97|/// stderr has no per-line timestamps and broker+brain share one file — without\n 98|/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\n 99|/// to field incident times (wall).\n...\n 109|\n*110|/// Attribution-label cap: facts accrete per role attach (a long-lived brain\n*111|/// conn can be controller + viewer + presence subscriber over its life), and\n 112|/// the label must stay BOUNDED — one truncation marker, then no further growth.\n 113|const LABEL_CAP: usize = 240;\n 114|\n...\n 119|    Done,\n*120|    /// The pre-write gate closure declined (e.g. a superseded controller\n*121|    /// writer's epoch check) — nothing was written; the conn stays healthy.\n 122|    Superseded,\n 123|}\n 124|\n...\n 152|    dog_cv: Condvar,\n*153|    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\n 154|    /// conn refuses every future write and is never re-gated.\n 155|    poisoned: AtomicBool,\n 156|    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\n...\n 230|    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n*231|    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 232|    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 233|    /// already-dead counterpart, the offline-peer dial churn) emits the\n 234|    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n...\n 265|    /// UNCONDITIONAL): `write-start` (first write only), `transport-close`\n*266|    /// (drop), `writer-exit` / `*-attach` / `*-replaced` (emitted by the\n 267|    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\n*268|    /// timeout/cancel leg is the retirement record itself.\n 269|    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 270|    fn lifecycle(&self, event: &str, extra: &str) {\n 271|        let sep = if extra.is_empty() { \"\" } else { \" \" };\n...\n 280|    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\n*281|    /// no-op, and re-cancel closes the race where a write was issued a beat\n*282|    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\n 283|    /// `cause` (the organic caller's write error) rides the retirement record\n 284|    /// when the retirement is NOT deadline-driven.\n 285|    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\n...\n 317|    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\n*318|    /// new read in after the cancel), and defuses the drop-limbo flush; the\n*319|    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\n 320|    /// `shutdown` in [`poison_and_cancel`] already did all of this.\n 321|    // On Unix the cfg(windows) teardown strips out and the idempotence\n 322|    // guard's early `return` becomes the function tail — needless_return\n...\n 339|    /// absolute deadline. If the op is still in flight at the deadline,\n*340|    /// poison + cancel, re-canceling periodically until the op reports\n 341|    /// completion (the writer clears `inflight` the instant `write_frame`\n 342|    /// returns — that return IS the buffer-release handshake), then sever.\n 343|    fn watchdog(&self) {\n...\n 382|            // Deadline: abort out of band, then wait for the canceled op to\n*383|            // report completion before severing (re-cancel each slice — a\n*384|            // cancel that raced the WriteFileEx issuance reaches nothing, so\n 385|            // one shot is not enough). Latch timed_out FIRST so the aborted\n 386|            // writer observes its failure as a DEADLINE retirement (wedge),\n 387|            // not an organic conn death.\n...\n 463|    /// endpoint/session where known — e.g. `role=brain`,\n*464|    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n 465|    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n 466|    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\n 467|    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n...\n 484|    /// attribution format — the broker's sink machinery uses it for\n*485|    /// `writer-exit`, attach/replace, and detach events (leg d).\n 486|    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 487|    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\n 488|        self.inner.lifecycle(event, extra);\n## crc_swap.rs\n 88|///    `<target>`.\n*89|/// 3. **Cleanup** — delete the displaced originals once the whole loop committed.\n 90|///\n 91|/// **C1 (REQ-CRC-SWAP-OLD-DISPLACE): displace, never replace.** The prior impl\n 92|/// renamed the original to a FIXED `<target>.old`. On a live box a surviving\n...\n 101|/// On ANY failure in the commit loop, the already-committed renames are **rolled\n*102|/// back** (restore each displaced original over `<target>`) and the error —\n 103|/// wrapped with the op + both paths (C1: a bare `os error 5` cost a full RCA) — is\n 104|/// returned, so the install dir is never left a half-new mix. The caller (W3d)\n 105|/// restarts the OLD resident child and reports the update failed.\n...\n 123|    // the target itself is present. CRASH-RECOVERY GUARD (doyle C1 review): if a\n*124|    // prior run displaced the original to `.old` and DIED before committing `.new`,\n 125|    // the target is MISSING and that `.old` is the LAST GOOD COPY of the binary —\n 126|    // GCing it before this run's commit lands would, on any failure between here and\n 127|    // commit, leave the box with NO binary at all (the prior impl never destroyed the\n...\n 148|    // Phase 2 — tight commit loop, tracking what to roll back on failure. Each entry\n*149|    // is (target, displaced-original-path-or-None) so rollback + cleanup use the\n 150|    // ACTUAL fresh `.old[-N]` name, not a fixed one.\n 151|    let mut committed: Vec<(&Path, Option<PathBuf>)> = Vec::new();\n 152|    for swap in plan {\n 153|        let target = swap.target.as_path();\n*154|        let displaced = if target.exists() {\n 155|            // DISPLACE: rename the original ASIDE to a fresh, non-colliding name so a\n 156|            // still-mapped `<target>.old` from a prior run is never renamed OVER.\n 157|            let old = fresh_old_path(target);\n...\n 168|            // This one didn't land: undo its own displacement, then prior commits.\n*169|            if let Some(old) = &displaced {\n 170|                let _ = rename(old, target);\n 171|            }\n 172|            rollback(&committed, rename);\n...\n 175|        }\n*176|        committed.push((target, displaced));\n 177|    }\n 178|\n*179|    // Phase 3 — whole loop committed: drop the displaced originals (best-effort; a\n 180|    // still-mapped one stays but is uniquely named → the next run's Phase-0 GC\n 181|    // retries it, never a brick).\n*182|    for (_, displaced) in &committed {\n*183|        if let Some(old) = displaced {\n 184|            let _ = std::fs::remove_file(old);\n 185|        }\n 186|    }\n...\n 195|\n*196|/// A FRESH displaced-original path: `<target>.old` if free, else `<target>.old-1`,\n 197|/// `.old-2`, … — the first non-existing name. This is the C1 fix: never reuse (and\n 198|/// so never rename OVER) a `.old` a prior run stranded still-mapped. Mirrors the spt\n 199|/// self-updater's `spt.exe.old-<counter>`.\n...\n 256|/// Restore each already-committed rename: move `<target>` back to `<target>.new`\n*257|/// and the displaced original back over `<target>`. Best-effort (a rollback is\n 258|/// already an error path; nothing more we can do but try).\n 259|fn rollback(\n 260|    committed: &[(&Path, Option<PathBuf>)],\n...\n 262|) {\n*263|    for (target, displaced) in committed.iter().rev() {\n 264|        let _ = rename(target, &new_path(target));\n*265|        if let Some(old) = displaced {\n 266|            let _ = rename(old, target);\n 267|        }\n 268|    }\n...\n 487|        );\n*488|        // The second swap's original was displaced then restored by the same-swap undo.\n 489|        assert_eq!(\n 490|            fs::read(&second_target).unwrap(),\n 491|            b\"second-original\",\n...\n 530|    /// copy of a real image and SPAWN a process from it (image-map lock on Windows),\n*531|    /// then apply: the original is DISPLACED to `.old-1` (never renamed OVER the\n 532|    /// mapped `.old`), so apply SUCCEEDS and the target gets the new bytes.\n 533|    ///\n 534|    /// RED-FIRST: revert `fresh_old_path` to a fixed `.old` (rename-OVER) and on\n...\n 582|            b\"NEW-IMAGE-BYTES\",\n*583|            \"the target received the new bytes (the original was displaced, not replaced-over)\"\n 584|        );\n 585|    }\n 586|\n*587|    /// The displaced original lands at a FRESH `.old-N` when `.old` is occupied, and\n 588|    /// a plain (unmapped) stale `.old` is GC'd on the next apply.\n 589|    // [unit->REQ-CRC-SWAP-OLD-DISPLACE]\n 590|    #[test]\n...\n 631|\n*632|    /// CRASH-RECOVERY GUARD (doyle C1 review): when a prior run displaced the\n 633|    /// original to `<target>.old` then DIED before committing `<target>.new`, the\n 634|    /// target is MISSING and that `.old` is the LAST GOOD COPY. This apply must NOT\n 635|    /// GC it at Phase 0 (a failure before commit would leave NO binary) — it commits\n...\n 644|\n*645|        // The staged replacement exists; the TARGET is MISSING (a prior run displaced\n 646|        // it aside then crashed pre-commit) and `<target>.old` is the last good copy.\n 647|        write(&s.join(\"bin\"), b\"V2\");\n 648|        let stranded_old = i.join(\"bin.old\");\n## daemon.rs\n 139|        // attachment state starts from the config's startup defaults — an\n*140|        // unsaved attach/detach flip deliberately does not survive a daemon\n 141|        // restart (`--save` is the persistence path).\n 142|        // [impl->REQ-SUBNET-5]\n 143|        {\n...\n 155|                // [impl->REQ-CLI-OUTPUT-MARKDOWN]\n*156|                eprintln!(\"SUBNET_DETACHED:{name} (startup default — run: spt subnet attach {name})\");\n 157|            }\n 158|        }\n 159|\n...\n 206|                        // racing the network up at boot. Retry net in the\n*207|                        // background and attach it to the broker when it\n 208|                        // succeeds, so the node becomes reachable WITHOUT a\n 209|                        // manual restart; the brain notices `net-status` flip\n 210|                        // enabled and starts the consumers then.\n...\n 702|        }\n*703|        // setsid: detach into a NEW SESSION, not just a new process group —\n 704|        // a daemon auto-started inside an ssh/non-interactive session\n 705|        // otherwise dies with it (sshd HUPs the whole session on\n 706|        // disconnect; observed live in the 2026-06-06 fleet deploy, where\n...\n 854|    // worked before (just can't escape that job; logged for diagnosis).\n*855|    // [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 856|    match create_process_detached(program, args, CREATE_BREAKAWAY_FROM_JOB) {\n 857|        Ok(pid) => Ok(pid),\n 858|        Err(e) if e.raw_os_error() == Some(ERROR_ACCESS_DENIED) => {\n...\n 879|/// CAN deny it (the L1 finding).\n*880|// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 881|#[cfg(windows)]\n 882|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 883|pub(crate) enum LaunchRung {\n...\n 896|/// pure decision seam can be unit-tested against it.\n*897|// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 898|#[cfg(windows)]\n 899|pub(crate) fn launch_ladder() -> [LaunchRung; 4] {\n 900|    [\n...\n 911|/// without spawning anything.\n*912|// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 913|#[cfg(windows)]\n 914|pub(crate) fn drive_ladder(\n 915|    ladder: [LaunchRung; 4],\n...\n 931|/// logging/return, not readiness (WMI/schtasks pids are the launcher's view).\n*932|// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 933|#[cfg(windows)]\n 934|pub(crate) fn launch_daemon_job_neutral(program: &str, args: &[String]) -> io::Result<u32> {\n 935|    // Test hook (job_escape_e2e): force a single rung so a green that silently\n...\n 970|\n*971|/// Detach the calling process from its console (`FreeConsole`) — the broker's\n*972|/// self-detach for the auto-start ladder's window-allocating rungs. The WMI /\n 973|/// schtasks rungs launch the broker via `cmd /c … start \"\" /b spt daemon run`;\n 974|/// @15fdf58 set DETACHED_PROCESS on the cmd WRAPPER, but `start /b` then launched\n 975|/// the inner broker (a console-subsystem exe) with no console to inherit, so it\n...\n 1076|/// terminal Job), not ours.\n*1077|// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 1078|#[cfg(windows)]\n 1079|fn wrapped_daemon_command(program: &str, args: &[String]) -> String {\n 1080|    let mut s = String::from(\"cmd.exe /c \");\n...\n 1128|/// to the next rung — never a silent \"launched\".\n*1129|// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 1130|#[cfg(windows)]\n 1131|fn spawn_daemon_via_wmi(program: &str, args: &[String]) -> io::Result<u32> {\n 1132|    use base64::engine::general_purpose::STANDARD as B64;\n...\n 1187|/// quoting is best-effort.\n*1188|// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 1189|#[cfg(windows)]\n 1190|fn spawn_daemon_via_schtasks(program: &str, args: &[String]) -> io::Result<u32> {\n 1191|    use std::os::windows::process::CommandExt;\n...\n 1341|\n*1342|    // [unit->REQ-HAZARD-VIEWER-CLOSE-DETACH] the launch ladder order is exactly\n 1343|    // WMI → schtasks → breakaway → in-job: the job-neutral creators come FIRST\n 1344|    // (a daemon born outside any terminal job never needs to claw back out),\n 1345|    // breakaway is the demoted fallback, in-job the last resort.\n...\n 1385|\n*1386|    // [unit->REQ-HAZARD-VIEWER-CLOSE-DETACH] the pure decision seam: drive_ladder\n 1387|    // returns the FIRST rung that succeeds (rung-1 Ok → Wmi, nothing else tried).\n 1388|    #[cfg(windows)]\n 1389|    #[test]\n...\n 1399|\n*1400|    // [unit->REQ-HAZARD-VIEWER-CLOSE-DETACH] the error→rung map: a failing rung-1\n 1401|    // (WMI) falls through to rung-2 (schtasks); the returned rung is the one that\n 1402|    // actually produced the pid, NOT a silent rung-1 \"success\".\n 1403|    #[cfg(windows)]\n...\n 1417|\n*1418|    // [unit->REQ-HAZARD-VIEWER-CLOSE-DETACH] every rung failing surfaces the LAST\n 1419|    // error (never a false Ok) after trying all four in order.\n 1420|    #[cfg(windows)]\n 1421|    #[test]\n...\n 1441|\n*1442|    // [unit->REQ-HAZARD-VIEWER-CLOSE-DETACH] the MECHANISM proof: a child spawned\n 1443|    // by the real `detached_no_inherit` (CREATE_BREAKAWAY_FROM_JOB) ESCAPES a\n 1444|    // KILL_ON_JOB_CLOSE job, so terminating the job does NOT reap it — the OS\n 1445|    // contract the daemon's tab-close survival relies on. The inverse of reap.rs's\n...\n 1650|        // exact breakaway-unreliability that makes WMI the PRIMARY launcher\n*1651|        // (REQ-HAZARD-VIEWER-CLOSE-DETACH); the WMI escape is proven by\n 1652|        // `job_escape_e2e`, and the no-regression fallback by\n 1653|        // `detached_no_inherit_falls_back_under_a_breakaway_denying_job`.\n 1654|        if diag.contains(\"gc_in_any_job=1\") {\n## deadline.rs\n 58|pub struct DeadlineAnchor {\n*59|    /// Epoch-ms grid origin. Periodic fires land at `anchor_ms + k·interval_ms`.\n 60|    pub anchor_ms: u64,\n 61|    /// Grid spacing in ms. Always `> 0` (the constructor degrades a zero value).\n 62|    pub interval_ms: u64,\n...\n 190|pub struct OneShotDeadline {\n*191|    /// Absolute epoch-ms target. Fires once when `now_ms >= target_ms`.\n 192|    pub target_ms: u64,\n 193|}\n 194|\n## deelevate.rs\n 512|    // are rejected with ERROR_INVALID_PARAMETER (87; hit live, second\n*513|    // elevated verification round). Detach therefore rides\n 514|    // CREATE_NEW_CONSOLE with the console window HIDDEN via the startup\n 515|    // info (STARTF_USESHOWWINDOW + SW_HIDE) — the child still owns no\n 516|    // caller console and no window appears. CREATE_NEW_PROCESS_GROUP keeps\n...\n 526|    /// daemon-owned reap, never a failed spawn).\n*527|    /// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 528|    const CREATE_BREAKAWAY_FROM_JOB: u32 = 0x0100_0000;\n 529|    /// STARTUPINFOW.dwFlags: honor wShowWindow.\n 530|    const STARTF_USESHOWWINDOW: u32 = 0x0000_0001;\n...\n 600|    /// Whether an elevated `daemon run` here WOULD respawn de-elevated and\n*601|    /// detach (vanish into the background) — i.e. a real de-elevation target\n 602|    /// exists: elevated AND a desktop shell token exists AND that shell is\n 603|    /// itself unelevated. `false` when not elevated, when there is no desktop\n 604|    /// shell (a headless / service session — e.g. an elevated CI runner), or\n## dispatch.rs\n 23|//! The kind-tagged families collide on variant names (`request` is both\n*24|//! sync's and attach's opener; `chunk`/`done`/`err` recur everywhere), so the\n 25|//! demux keys on the first line's **shape**, not its kind alone:\n 26|//!\n 27|//! | first line | family |\n...\n 29|//! | `kind=request` + `sync_id` | sync pull ([`crate::sync::serve_sync`]) |\n*30|//! | `kind=request` + `session_id` | remote-drive attach (D9-1-3) |\n 31|//! | `kind=request` + `endpoint`+`event` | remote rest op ([`crate::resthost::serve_rest`]) |\n 32|//! | `kind=request` + `owner`+`shell_ref` | cross-node shell link ([`crate::linkhost::serve_shell_link`]) |\n 33|//! | `kind=query` | update pull ([`crate::propagate::serve_update`]) |\n...\n 74|\n*75|use crate::attach::serve_attach;\n 76|use crate::brain::{Brain, BrokerEvent};\n 77|use crate::notifsync::{apply_notif_feed, NotifPolicy};\n 78|use crate::propagate::serve_update;\n...\n 101|    Registry,\n*102|    /// A remote-drive attach (`AttachRecord::Request`).\n*103|    Attach,\n 104|    /// A file transfer (`XferRecord::Fetch`/`Push`).\n 105|    Xfer,\n 106|    /// A remote rest op (`RestRecord::Request` — suspend/wake from another\n...\n 112|    /// A subnet-scoped liveness probe (`ServeProbeRecord::Request` — \"serving\n*113|    /// subnet X?\", the `--nodes` view's detach-aware refinement, REQ-SUBNET-5).\n 114|    ServeProbe,\n 115|    /// Unclassifiable — dropped.\n 116|    Unknown,\n...\n 122|        Some(\"request\") if line.get(\"sync_id\").is_some() => StreamFamily::Sync,\n*123|        Some(\"request\") if line.get(\"session_id\").is_some() => StreamFamily::Attach,\n 124|        Some(\"request\") if line.get(\"endpoint\").is_some() && line.get(\"event\").is_some() => {\n 125|            StreamFamily::Rest\n 126|        }\n...\n 195|/// stream to a family worker, until `stop` is set. Blocks the calling thread\n*196|/// (the daemon spawns it on its own); workers detach (each ends with its\n 197|/// stream).\n 198|pub fn run_dispatch_loop(\n 199|    broker_name: &str,\n...\n 334|        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n*335|        StreamFamily::Attach => {\n 336|            // The gate subject is the session's OWNING endpoint, resolved\n 337|            // from the broker's table (the spawn's label) — never from wire\n 338|            // bytes. An unlabeled or unknown session refuses, fail-closed.\n 339|            //\n*340|            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n 341|            // cannot know our local session id, so it names the ENDPOINT. When the\n 342|            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n 343|            // we actually host a live session for it (D5 session-presence gate;\n 344|            // D6: a stale registry row that named a gone endpoint resolves to no\n*345|            // session → refuse cleanly, never a false attach). The endpoint id is\n 346|            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\n 347|            // `endpoint`); the origin subject stays the handshake identity. The\n 348|            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\n...\n 352|            // before serve_attach subscribes — never mid-serve, which would swallow\n*353|            // interleaved attach records) and `None` on the local path.\n 354|            let (endpoint, remote_session) = match line\n 355|                .get(\"endpoint_id\")\n 356|                .and_then(|v| v.as_str())\n...\n 358|            {\n*359|                Some(ep) => match crate::attach::resolve_local_session(&mut brain, ep) {\n 360|                    Some(sid) => (Some(ep.to_string()), Some(sid)),\n 361|                    None => (None, None),\n 362|                },\n...\n 392|                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n*393|                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n 394|                }\n 395|            }\n 396|        }\n...\n 712|\n*713|        let attach = json!({\"kind\": \"request\", \"session_id\": 3, \"from_seq\": 0});\n*714|        assert_eq!(classify_first_line(&attach), StreamFamily::Attach);\n 715|\n 716|        // [unit->REQ-INST-6] the rest op's opener disambiguates by shape too.\n 717|        let rest =\n...\n 736|        let reg = json!({\"subnet\": \"home\", \"endpoint_id\": \"ling\",\n*737|            \"instance\": {\"node\": \"n1\", \"status\": \"Active\", \"epoch\": 4}});\n 738|        assert_eq!(classify_first_line(&reg), StreamFamily::Registry);\n 739|    }\n 740|\n## effect.rs\n 129|/// monotonically SHRINKS (a live producer MUST stamp a real tag, never `legacy`).\n*130|/// `psyche`/`epoch` are deliberately ABSENT — they are the `EpochSource` notif/lease\n 131|/// counter domain, never broker `apply_once` producers (Amendment 2); a journal tag\n 132|/// with no stamp site is a doc'd-but-dead knob. The tag is TEXTUAL so a recovered\n 133|/// journal line is self-describing (see [`Minter::as_tag`]).\n...\n 241|\n*242|/// Run a tracing-only op (ADR-0034 Decision 2: the rest family + rc attach\n 243|/// stream-open) with a ONE-shot self-heal. On the typed no-longer-held reply (a\n 244|/// broker restart dropped the conn/stream the op had journaled) re-mint a FRESH\n 245|/// op from the SAME minter and run ONCE more — a tracing op's redelivery \"needs\n...\n 367|    ///\n*368|    /// **Bug #16 (REQ-BROKER-ATTACH-JOURNAL-RESILIENT):** the broker outlives brain\n 369|    /// respawns, so a single panic while the journal lock was held would otherwise\n 370|    /// poison the mutex permanently — and every subsequent journaled op\n 371|    /// (`dispatch_net_stream_open` → `apply_once`) would then panic its per-conn\n...\n 378|    /// re-driven effect either dedups or re-attempts — no double-apply, no lost op.\n*379|    // [impl->REQ-BROKER-ATTACH-JOURNAL-RESILIENT]\n 380|    fn lock_recover(&self) -> std::sync::MutexGuard<'_, Inner> {\n 381|        self.inner\n 382|            .lock()\n...\n 688|\n*689|    // [unit->REQ-BROKER-ATTACH-JOURNAL-RESILIENT] bug #16: a poisoned journal mutex\n 690|    // must NOT permanently brick subsequent journaled ops. Before the fix every\n 691|    // `apply_once` after a panic-while-locked hit `.expect(\"effect journal lock\n 692|    // poisoned\")` and killed its per-conn reply thread → attaches deadlined. Now\n...\n 812|    // GLOBAL journal lock forever — so EVERY other journaled effect (another\n*813|    // keystroke, another attach's journaled input) wedges, and the single-\n 814|    // threaded dispatch can no longer service `spt rc` attaches. Both units below\n 815|    // FAIL against the current (unfixed) code = the RED repro. They COMPILE\n 816|    // against current APIs (no use of the not-yet-existing `is_durable()`).\n...\n 1014|        }\n*1015|        // The dropped-minter proof: psyche/epoch are the EpochSource domain, never\n 1016|        // journal minters — parsing their would-be tags yields None.\n 1017|        assert_eq!(Minter::from_tag(\"psyche\"), None);\n*1018|        assert_eq!(Minter::from_tag(\"epoch\"), None);\n 1019|        assert_eq!(Minter::from_tag(\"bogus\"), None);\n 1020|        // Legacy is a real, parseable tag (the reserved-shrinking discriminant).\n 1021|        assert_eq!(Minter::from_tag(\"legacy\"), Some(Minter::Legacy));\n## endpoint.rs\n 86|\n*87|/// Where the peer pump records its last-tick heartbeat (epoch ms, M8\n 88|/// decision 23 / REQ-DAEMON-5): written each pump tick (D4), read by\n 89|/// `spt daemon status` so a half-dead daemon (pump stalled, dispatcher\n 90|/// alive — the 2026-06-07 hfenduleam shape) renders honestly.\n...\n 94|\n*95|/// Read the pump's last-tick heartbeat (epoch ms). `None` = never written\n 96|/// (a pre-M8 daemon, or one that has not completed a first tick).\n 97|// [impl->REQ-CLI-2]\n 98|pub fn read_pump_heartbeat() -> Option<u64> {\n## frame.rs\n 161|            \"protocol_version\": 2,\n*162|            \"kind\": \"attach\",\n 163|            \"payload\": { \"session\": \"abc\" },\n 164|            \"future_field_an_older_broker_never_heard_of\": { \"nested\": [1, 2, 3] }\n 165|        })\n...\n 170|        assert_eq!(decoded.protocol_version, 2);\n*171|        assert_eq!(decoded.kind, \"attach\");\n 172|        assert_eq!(decoded.payload, json!({ \"session\": \"abc\" }));\n 173|    }\n 174|\n## grants.rs\n 204|    node_hex: &str,\n*205|    epochs: &mut spt_store::epoch::EpochSource,\n 206|    subnet: &str,\n 207|    policy: &crate::notif::NotifSurfacePolicy,\n 208|    owlery: &Path,\n...\n 540|            let store = spt_store::notif::NotifStore::open_at(&home.join(\"notifs.db\")).unwrap();\n*541|            let mut epochs = spt_store::epoch::EpochSource::load_from(&home.join(\"epoch\"));\n 542|            let policy = NotifSurfacePolicy {\n 543|                subnets: vec![(\"home\".into(), false)],\n 544|                vis: VisibilityStore::default(),\n\n## bin/\n### xlate_choreo_fixture.rs\n 33|//! set). The int test reads that file and asserts NO operator keystroke payload\n*34|//! (e.g. the controller's typed bytes) ever appears — only `init` / `event` /\n 35|//! content-free `input` lines do. The binary never sees PTY keystroke content.\n 36|\n 37|use std::fs::OpenOptions;\n...\n 101|            // emit `{\"commit\":true}`. The broker's inject worker must hit its\n*102|            // commit deadline, flush the buffered controller input, release the\n 103|            // floor, and FAULT this binary (after which idle inbound SPOOLS — raw\n 104|            // inject was removed as a delivery path, ADR-0022 amendment).\n 105|            //\n...\n 143|/// truncation and many settle-chunks wide, yet it fits inside a 24-row screen grid so a\n*144|/// cold-attach repaint (ADR-0031 = current screen) still carries the HEAD (a taller\n 145|/// payload scrolls HEADSTART off the visible grid).\n 146|fn big_multiline_payload() -> String {\n 147|    let mut s = String::from(\"HEADSTART \");\n\nShowing files 1-20 of 161+. Use skip=20 for the next page, or narrow paths/pattern.\n\nSkipped missing paths: packages, test"}],"details":{"scopePath":"src, crates, tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":179,"fileCount":20,"files":["crates/spt-daemon/src/access.rs","crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/autostart.rs","crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/codec.rs","crates/spt-daemon/src/config.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/crc_swap.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deadline.rs","crates/spt-daemon/src/deelevate.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/effect.rs","crates/spt-daemon/src/endpoint.rs","crates/spt-daemon/src/frame.rs","crates/spt-daemon/src/grants.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/access.rs","count":1},{"path":"crates/spt-daemon/src/applyhost.rs","count":2},{"path":"crates/spt-daemon/src/attach.rs","count":20},{"path":"crates/spt-daemon/src/autostart.rs","count":1},{"path":"crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","count":3},{"path":"crates/spt-daemon/src/brain.rs","count":20},{"path":"crates/spt-daemon/src/brainproc.rs","count":10},{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/codec.rs","count":2},{"path":"crates/spt-daemon/src/config.rs","count":4},{"path":"crates/spt-daemon/src/conn.rs","count":20},{"path":"crates/spt-daemon/src/crc_swap.rs","count":20},{"path":"crates/spt-daemon/src/daemon.rs","count":20},{"path":"crates/spt-daemon/src/deadline.rs","count":2},{"path":"crates/spt-daemon/src/deelevate.rs","count":3},{"path":"crates/spt-daemon/src/dispatch.rs","count":17},{"path":"crates/spt-daemon/src/effect.rs","count":8},{"path":"crates/spt-daemon/src/endpoint.rs","count":2},{"path":"crates/spt-daemon/src/frame.rs","count":2},{"path":"crates/spt-daemon/src/grants.rs","count":2}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## access.rs\n 41│//! message traffic.\n*42│//! Control surfaces (terminal attach, file transfer) are never replies; their\n 43│//! serves always gate as [`InboundClass::Unsolicited`].\n 44│\n 45│use spt_store::access::{AccessStore, RecentOutbound, REPLY_WINDOW_MS};\n## applyhost.rs\n 320│    // brain-restart trigger rides after the swap (ADR-0018 D3-3); it does not\n*321│    // attach to any session (the broker holds every hosted endpoint across the\n 322│    // brain cycle — that is the whole point).\n 323│    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n 324│\n    │...\n 451│/// the planned brain-restart trigger (ADR-0018 D3-3) rides after the binary\n*452│/// swap. It does NOT attach to a session: the broker holds every hosted endpoint\n 453│/// across the brain cycle, so apply never touches one. `Err` means the broker\n 454│/// never answered (a dead broker fails the apply with the seat untouched).\n 455│fn connect_retry(broker_name: &str) -> io::Result<Brain> {\n## attach.rs\n  *1│//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport\n   2│//! onto a local session for an operator on another node.\n   3│//!\n   4│//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\n    │...\n  20│//! eats the operator's `Input` records (both sides starve; found the hard way\n *21│//! in `tests/attach.rs`). Idempotency holds without it because each direction\n  22│//! dedups at its **effect**: the PTY write is journaled by the operator-minted\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    │...\n  46│\n *47│use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\n  48│use spt_net::net::ndjson;\n  49│\n  50│use crate::access::{access_check, AccessDecision, InboundClass};\n    │...\n  54│\n *55│/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a\n  56│/// served viewport is a **driver** — attaching wakes the endpoint, the\n  57│/// viewport ending detaches it (active → dormant; the transition echo is\n  58│/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\n    │...\n  68│pub enum AttachServeOutcome {\n *69│    /// The access gate refused the requesting origin, OR a `Control` attach hit\n  70│    /// an endpoint a DIFFERENT operator already controls (busy-refuse,\n  71│    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the\n  72│    /// busy guidance client-side from the endpoint's `driven_by`.\n    │...\n  79│    Detached,\n *80│    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n *81│    /// (REQ-KICK-1): the loud `Displaced` notice was sent and the stream\n  82│    /// finished. The endpoint stays driven (by the taker) — no dormant edge.\n *83│    Displaced,\n  84│}\n  85│\n  86│/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\n *87│/// arrives — gates wake (controller-only) and PTY input.\n  88│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n  89│enum ServeRole {\n *90│    /// The interactive controller (input + resize; wakes the endpoint).\n *91│    Controller,\n *92│    /// A read-only viewer (output only; wake-neutral).\n *93│    Viewer,\n  94│}\n  95│\n *96│/// Minimum spacing between skip-to-live re-subscribes after a viewer eviction\n *97│/// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). HARD constraint: an evicted viewer that is\n  98│/// hopelessly behind under a sustained flood will be re-evicted immediately after\n  99│/// each re-subscribe; without spacing that is an evict→resubscribe CPU spin. With\n 100│/// it, the operator sees an intermittent LIVE burst each interval (forward\n    │...\n 103│\n*104│/// The CONTROLLER fell behind the live ring further than the ring holds — the\n 105│/// dropped frames are GONE (`tail - delivered_through > cap_chunks`), so an\n 106│/// exactly-once re-fetch is IMPOSSIBLE. Distinguishes the irrecoverable edge from\n 107│/// an ordinary fatal so the serve loop can surface it (a marked truncation),\n 108│/// rather than silent-skipping (a B2 lie) or spinning. Full graceful handling is\n*109│/// DEFERRED to `REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND`.\n*110│// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n 111│#[derive(Debug)]\n 112│pub struct ControllerIrrecoverablyBehind {\n 113│    /// The frozen `delivered_through` floor the ring has already rolled past.\n    │...\n 120│            f,\n*121│            \"controller fell irrecoverably behind the live ring at seq {} (dropped frames rolled out of the ring)\",\n 122│            self.floor\n 123│        )\n 124│    }\n    │...\n 129│/// Is `e` the brain's strict reject-gap fatal (`output gap: got seq N want M`)?\n*130│/// That is the only error the controller gap-resume recovers from; every other\n 131│/// error propagates unchanged.\n 132│fn is_output_gap(e: &io::Error) -> bool {\n 133│    e.kind() == io::ErrorKind::InvalidData && e.to_string().contains(\"output gap\")\n## autostart.rs\n  6│//! operator marks an endpoint a **startup default** with `spt endpoint run\n *7│//! --save` (the endpoint analog of the shipped `subnet attach --save`\n  8│//! precedent — doyle-ruled shape (a)); the daemon replays the saved runs here\n  9│//! at start.\n 10│//!\n## brain.rs\n  56│};\n *57│use spt_net::net::attach::AttachIntent;\n  58│use crate::transport::{send_hello, LocalSocketTransport};\n  59│\n *60│/// Wall-clock now in epoch milliseconds — the source for `gen_start` (matches\n *61│/// the epoch-ms stamping used elsewhere in the tree; no date dependency).\n  62│pub fn now_ms() -> u64 {\n  63│    SystemTime::now()\n  64│        .duration_since(UNIX_EPOCH)\n    │...\n  68│\n *69│/// The brain's durable state across a handoff: enough to re-attach gaplessly and\n  70│/// advance the generation. `gen_start_ms` is recorded for observability but is\n  71│/// **deliberately not authoritative on restart** — [`Brain::handoff`] sets a\n  72│/// fresh `gen_start` from `now()` (2.4).\n    │...\n 107│    Exit { session_id: u64, code: Option<i32> },\n*108│    /// The resolved controller/viewer role of a [`Brain::attach_as`] subscribe\n*109│    /// (REQ-RCVIEW-1 / REQ-KICK-1). The serving brain wakes (controller-only),\n*110│    /// stays wake-neutral (viewer), or refuses the operator (busy).\n 111│    Subscribed {\n 112│        session_id: u64,\n 113│        outcome: SubscribeOutcome,\n 114│    },\n*115│    /// The current PTY size of a session, pushed to a VIEWER (the letterbox feed,\n 116│    /// REQ-RCVIEW-1).\n 117│    Size {\n 118│        session_id: u64,\n    │...\n 121│    },\n*122│    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n 123│    /// (REQ-KICK-1). The serving brain forwards a loud notice and detaches.\n*124│    Displaced { session_id: u64, by: String },\n*125│    /// This VIEWER's bounded subscription queue overflowed and it was evicted\n*126│    /// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). DISTINCT from a session-exit EOF: the\n 127│    /// serving brain re-subscribes from the current ring floor (skip-to-live)\n*128│    /// rather than tearing the attach down.\n 129│    ViewerEvicted { session_id: u64 },\n 130│    /// An idempotent effect is durably applied (or was deduped — `applied_now`\n 131│    /// distinguishes). The exactly-once ack for a [`Brain::send_effect`] (B5).\n    │...\n 301│    io_timeout: Option<Duration>,\n*302│    /// #6 (ADR-0031): a COLD attach (`from_seq == 0`, set by [`Brain::attach`] /\n 303│    /// [`Brain::attach_as`]) receives a synthesized clean-screen REPAINT as its\n 304│    /// first `KIND_OUTPUT` frame, at pseudo-seq `broker.next_seq - 1` — NOT seq 0.\n 305│    /// When `true`, the strict legacy `next_seq` path adopts that first frame's seq\n    │...\n 369│    /// caller (the brain-only update engine, `idempotent`/`daemon_e2e`/`handoff`\n*370│    /// integration tests) replays output through it with no later `attach`. So it\n*371│    /// stays. The P1c controller-writer reorder (a `serve_attach` re-serve that\n 372│    /// ALSO subscribes from 0 over this same socket → two racing\n 373│    /// `controller_writer`s) is fixed in the BROKER, not by dropping this\n*374│    /// subscribe: the epoch gate ([`OutputLog::become_controller`]) guarantees a\n 375│    /// single live writer per connection, and the seeded `session_cursors` below\n 376│    /// routes the consumer through the dedup-below + snap-above path so any\n 377│    /// at-least-once boundary re-send is tolerated, never a fatal reject-gap\n*378│    /// ([`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]).\n 379│    ///\n 380│    /// **Test-only (restoration D4-3).** The production resume path is\n 381│    /// [`Brain::cold_start`] + [`Brain::resume_sessions`] (broker cursor-of-record),\n    │...\n 383│    /// tests. See [`BrainState`].\n*384│    // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n 385│    pub fn handoff(name: &str, prior: BrainState, now_ms: u64) -> io::Result<Self> {\n 386│        let mut conn = LocalSocketTransport::connect(name)?;\n 387│        send_hello(&mut conn, Role::Brain)?;\n    │...\n 409│\n*410│    /// The wall-clock generation start (epoch ms) — always this generation's own\n 411│    /// start time, never a rehydrated value.\n 412│    pub fn gen_start_ms(&self) -> u64 {\n 413│        self.gen_start_ms\n## brainproc.rs\n 181│    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\n*182│    // continuity from the broker — re-attach EVERY hosted session in resume mode\n 183│    // from its delivered cursor, the production replacement for the retired\n 184│    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n 185│    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n    │...\n 278│        }\n*279│        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n 280│        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n 281│        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n*282│        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n 283│        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n 284│        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\n 285│        // window, so without this the wedge never drains: the promotion DRAINED gate\n    │...\n 473│    /// Whether the OLD brain generation's broker subscriber connection is DRAINED —\n*474│    /// its local controller conn CLOSED or stall-evicted, no blocked writes still\n 475│    /// pending on it (REQ-UPDATE-PROMOTE-DRAINED). Reads BROKER TRUTH in-process (no\n 476│    /// brain round-trip): the supervisor promotes a ready candidate only once this is\n 477│    /// `true`, so `brain.ready` arriving inside the W2 `BRAIN_WRITE_DEADLINE` window\n    │...\n 572│    fn old_gen_drained(&self) -> bool {\n*573│        // Drained ⟺ no hosted session has a wedged LOCAL (brain-own) controller conn.\n 574│        // No broker (net-less / bind-failed) → no control plane → drained. A broker\n*575│        // with no wedged local controller (the common case, incl. no daemon-hosted\n 576│        // sessions) reads drained at once → the normal fast promote on `brain.ready`.\n 577│        self.broker\n 578│            .as_ref()\n    │...\n 592│/// = the A11 kill+wait precedes `rollback`), so no brain-side pump can be\n*593│/// advertising and the [`spt_store::epoch::EpochSource`] single-writer assumption\n*594│/// (epoch.rs) holds at this `EpochSource::load`. **Do NOT fire broker-side notifs\n 595│/// while a brain is live:** concurrent `EpochSource` read-modify-write across the\n 596│/// two processes mints duplicate epochs (KH 4.8 lost-update). The forward fix for\n 597│/// any future broker-side live-notif is cross-process serialization (a file\n    │...\n 603│    };\n*604│    let mut epochs = spt_store::epoch::EpochSource::load();\n 605│    let policy = crate::notif::NotifSurfacePolicy::load();\n 606│    let node_hex = crate::presence::local_node_hex();\n 607│    let owlery = spt_store::perch::owlery_dir();\n    │...\n 668│/// `BRAIN_WRITE_DEADLINE`, so a new brain can signal ready while the OLD generation's\n*669│/// local controller conn is still wedged (the 22:47 apply \"promoted\" onto a frozen\n 670│/// control plane). So readiness is LATCHED (`ready_seen` — a candidate that stamped\n 671│/// ready then exited is not demoted, preserving the ready-then-exit-still-promotes\n 672│/// semantic) and promotion waits for `env.old_gen_drained()` too. While ready-seen\n## broker.rs\n  13│//! crash). The broker — and every hosted child — must survive that untouched,\n *14│//! and the new brain must re-attach to the output stream **gaplessly and\n  15│//! exactly-once** (Spike #1 made real). Two mechanisms:\n  16│//!\n  17│//! - **Sequenced, buffered output.** Each session's output is appended to an\n    │...\n  44│\n *45│use spt_net::net::attach::AttachIntent;\n  46│use spt_net::net::endpoint::EndpointAddr;\n  47│use spt_store::perch::{resolve_perch_path, ParentHint};\n  48│\n    │...\n  89│\n *90│/// The viewer-channel depth (frames). A bounded SPSC queue per viewer; when it\n *91│/// OVERFLOWS the viewer is evicted — the drain `try_send`s and never blocks\n *92│/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render\n *93│/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n *94│/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n *95│/// owns the replay batch), so a viewer attaching to a busy session is not\n  96│/// spuriously evicted.\n  97│const VIEWER_CHANNEL_DEPTH: usize = 256;\n  98│\n *99│/// Soft cap on concurrent viewers per session (REQ-HAZARD-VIEWER-ISOLATION:\n*100│/// bounds the writer-thread count). A `Viewer` attach beyond the cap is refused.\n 101│const MAX_VIEWERS: usize = 32;\n 102│\n*103│/// Whether a viewer's bounded-queue `try_send` result demands EVICTION\n*104│/// (REQ-HAZARD-VIEWER-ISOLATION): a `Full` queue (the viewer can't keep up with\n 105│/// the live stream) or a `Disconnected` one (its writer thread ended after a\n*106│/// dead socket). The drain evicts on either and NEVER blocks on a viewer — this\n 107│/// is the pure decision the fan-out applies under the log lock.\n*108│// [impl->REQ-HAZARD-VIEWER-ISOLATION]\n 109│fn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {\n 110│    r.is_err()\n 111│}\n 112│\n*113│/// One read-only viewer's isolated sink (REQ-HAZARD-VIEWER-ISOLATION). The drain\n 114│/// thread `try_send`s output frames into `tx` (a bounded queue); a dedicated\n*115│/// `writer` thread does the BLOCKING socket write, so a wedged viewer backs up\n 116│/// only its own queue — never the drain. When the queue overflows the sink is\n 117│/// dropped (the `tx` closes → the writer ends) and removed from the map.\n 118│struct ViewerSink {\n*119│    /// The bounded live-frame queue to this viewer's writer thread.\n 120│    tx: SyncSender<Envelope>,\n*121│    /// The viewer's IPC send half — kept ONLY for identity matching on detach\n*122│    /// (a dropped brain connection removes its viewer rows); never written here.\n 123│    send: SharedSend,\n*124│    /// Set TRUE when this viewer is EVICTED for overflow (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT),\n 125│    /// distinguishing an eviction from a normal teardown (session end / conn drop).\n 126│    /// `append` stores `true` BEFORE dropping the sink (and thus `tx`); the writer,\n 127│    /// on its `rx.recv()` Err, reads this and — when true — emits a\n## codec.rs\n 64│    fn frame_round_trips_through_buffer() {\n*65│        let env = Envelope::new(\"attach\", json!({ \"session\": \"s1\", \"n\": 7 }));\n 66│        let mut buf: Vec<u8> = Vec::new();\n 67│        write_frame(&mut buf, &env).unwrap();\n 68│\n   │...\n 71│        assert_eq!(got.protocol_version, IPC_PROTOCOL_VERSION);\n*72│        assert_eq!(got.kind, \"attach\");\n 73│        assert_eq!(got.payload, json!({ \"session\": \"s1\", \"n\": 7 }));\n 74│    }\n 75│\n## config.rs\n  23│\n *24│/// Default drop-file precedence protection window (epoch-ms), mirroring the\n  25│/// interim `spt_live` guard. Surfaced here so the one config source owns it.\n  26│pub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 2_000;\n  27│\n    │...\n  67│/// REQ-ENDPOINT-AUTOSTART): the daemon REPLAYS these at start — the endpoint\n *68│/// analog of the shipped `subnet attach/detach --save` precedent. Minimal by\n  69│/// design: the adapter option is re-resolved fresh at each replay (an adapter\n  70│/// update between restarts is honored, an uninstalled one loud-skips), and the\n  71│/// endpoint's home subnet is NOT recorded (home is durable on the perch —\n    │...\n 134│    pub pulse_period: Duration,\n*135│    /// The drop-file precedence protection window in epoch-ms (6.5).\n 136│    pub protection_window_ms: u64,\n 137│    /// The orphan-watch grace window (B7): the recovery wait before a\n 138│    /// supervised-crash teardown rechecks liveness and signs off (1.1).\n    │...\n 180│    /// REQ-SUBNET-5): the daemon's boot reset seeds the live\n*181│    /// `attachment.json` from this list — `spt subnet detach/attach --save`\n 182│    /// writes it. Default empty = every held subnet starts attached.\n 183│    pub detached_subnets: Vec<String>,\n 184│    /// The worker-reap TTL floor (seconds since a worker's `started`), W-3\n## conn.rs\n  11│//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n *12│//! logical stall-evict (`stall_evict_controller`) releases only the controller\n  13│//! ROLE — it never cancels the in-flight pipe write or closes the physical\n  14│//! connection, so the writer (and every write queued behind the same gate)\n  15│//! stays wedged until the peer process dies (~128 s in the field capture, on a\n    │...\n  53│//! data), followed by one more `CancelIoEx` covering a read re-issued in the\n *54│//! cancel→disconnect window.\n  55│//!\n  56│//! No new output queue (the bounded per-sink queues + isolated writer threads\n  57│//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\n    │...\n  95│/// Wall-clock + monotonic stamp for daemon-stderr correlation records\n *96│/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\n  97│/// stderr has no per-line timestamps and broker+brain share one file — without\n  98│/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\n  99│/// to field incident times (wall).\n    │...\n 109│\n*110│/// Attribution-label cap: facts accrete per role attach (a long-lived brain\n*111│/// conn can be controller + viewer + presence subscriber over its life), and\n 112│/// the label must stay BOUNDED — one truncation marker, then no further growth.\n 113│const LABEL_CAP: usize = 240;\n 114│\n    │...\n 119│    Done,\n*120│    /// The pre-write gate closure declined (e.g. a superseded controller\n*121│    /// writer's epoch check) — nothing was written; the conn stays healthy.\n 122│    Superseded,\n 123│}\n 124│\n    │...\n 152│    dog_cv: Condvar,\n*153│    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\n 154│    /// conn refuses every future write and is never re-gated.\n 155│    poisoned: AtomicBool,\n 156│    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\n    │...\n 230│    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n*231│    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 232│    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 233│    /// already-dead counterpart, the offline-peer dial churn) emits the\n 234│    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n    │...\n 265│    /// UNCONDITIONAL): `write-start` (first write only), `transport-close`\n*266│    /// (drop), `writer-exit` / `*-attach` / `*-replaced` (emitted by the\n 267│    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\n*268│    /// timeout/cancel leg is the retirement record itself.\n 269│    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 270│    fn lifecycle(&self, event: &str, extra: &str) {\n 271│        let sep = if extra.is_empty() { \"\" } else { \" \" };\n    │...\n 280│    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\n*281│    /// no-op, and re-cancel closes the race where a write was issued a beat\n*282│    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\n 283│    /// `cause` (the organic caller's write error) rides the retirement record\n 284│    /// when the retirement is NOT deadline-driven.\n 285│    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\n    │...\n 317│    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\n*318│    /// new read in after the cancel), and defuses the drop-limbo flush; the\n*319│    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\n 320│    /// `shutdown` in [`poison_and_cancel`] already did all of this.\n 321│    // On Unix the cfg(windows) teardown strips out and the idempotence\n 322│    // guard's early `return` becomes the function tail — needless_return\n    │...\n 339│    /// absolute deadline. If the op is still in flight at the deadline,\n*340│    /// poison + cancel, re-canceling periodically until the op reports\n 341│    /// completion (the writer clears `inflight` the instant `write_frame`\n 342│    /// returns — that return IS the buffer-release handshake), then sever.\n 343│    fn watchdog(&self) {\n    │...\n 382│            // Deadline: abort out of band, then wait for the canceled op to\n*383│            // report completion before severing (re-cancel each slice — a\n*384│            // cancel that raced the WriteFileEx issuance reaches nothing, so\n 385│            // one shot is not enough). Latch timed_out FIRST so the aborted\n 386│            // writer observes its failure as a DEADLINE retirement (wedge),\n 387│            // not an organic conn death.\n    │...\n 463│    /// endpoint/session where known — e.g. `role=brain`,\n*464│    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n 465│    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n 466│    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\n 467│    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    │...\n 484│    /// attribution format — the broker's sink machinery uses it for\n*485│    /// `writer-exit`, attach/replace, and detach events (leg d).\n 486│    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 487│    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\n 488│        self.inner.lifecycle(event, extra);\n## crc_swap.rs\n  88│///    `<target>`.\n *89│/// 3. **Cleanup** — delete the displaced originals once the whole loop committed.\n  90│///\n  91│/// **C1 (REQ-CRC-SWAP-OLD-DISPLACE): displace, never replace.** The prior impl\n  92│/// renamed the original to a FIXED `<target>.old`. On a live box a surviving\n    │...\n 101│/// On ANY failure in the commit loop, the already-committed renames are **rolled\n*102│/// back** (restore each displaced original over `<target>`) and the error —\n 103│/// wrapped with the op + both paths (C1: a bare `os error 5` cost a full RCA) — is\n 104│/// returned, so the install dir is never left a half-new mix. The caller (W3d)\n 105│/// restarts the OLD resident child and reports the update failed.\n    │...\n 123│    // the target itself is present. CRASH-RECOVERY GUARD (doyle C1 review): if a\n*124│    // prior run displaced the original to `.old` and DIED before committing `.new`,\n 125│    // the target is MISSING and that `.old` is the LAST GOOD COPY of the binary —\n 126│    // GCing it before this run's commit lands would, on any failure between here and\n 127│    // commit, leave the box with NO binary at all (the prior impl never destroyed the\n    │...\n 148│    // Phase 2 — tight commit loop, tracking what to roll back on failure. Each entry\n*149│    // is (target, displaced-original-path-or-None) so rollback + cleanup use the\n 150│    // ACTUAL fresh `.old[-N]` name, not a fixed one.\n 151│    let mut committed: Vec<(&Path, Option<PathBuf>)> = Vec::new();\n 152│    for swap in plan {\n 153│        let target = swap.target.as_path();\n*154│        let displaced = if target.exists() {\n 155│            // DISPLACE: rename the original ASIDE to a fresh, non-colliding name so a\n 156│            // still-mapped `<target>.old` from a prior run is never renamed OVER.\n 157│            let old = fresh_old_path(target);\n    │...\n 168│            // This one didn't land: undo its own displacement, then prior commits.\n*169│            if let Some(old) = &displaced {\n 170│                let _ = rename(old, target);\n 171│            }\n 172│            rollback(&committed, rename);\n    │...\n 175│        }\n*176│        committed.push((target, displaced));\n 177│    }\n 178│\n*179│    // Phase 3 — whole loop committed: drop the displaced originals (best-effort; a\n 180│    // still-mapped one stays but is uniquely named → the next run's Phase-0 GC\n 181│    // retries it, never a brick).\n*182│    for (_, displaced) in &committed {\n*183│        if let Some(old) = displaced {\n 184│            let _ = std::fs::remove_file(old);\n 185│        }\n 186│    }\n    │...\n 195│\n*196│/// A FRESH displaced-original path: `<target>.old` if free, else `<target>.old-1`,\n 197│/// `.old-2`, … — the first non-existing name. This is the C1 fix: never reuse (and\n 198│/// so never rename OVER) a `.old` a prior run stranded still-mapped. Mirrors the spt\n 199│/// self-updater's `spt.exe.old-<counter>`.\n    │...\n 256│/// Restore each already-committed rename: move `<target>` back to `<target>.new`\n*257│/// and the displaced original back over `<target>`. Best-effort (a rollback is\n 258│/// already an error path; nothing more we can do but try).\n 259│fn rollback(\n 260│    committed: &[(&Path, Option<PathBuf>)],\n    │...\n 262│) {\n*263│    for (target, displaced) in committed.iter().rev() {\n 264│        let _ = rename(target, &new_path(target));\n*265│        if let Some(old) = displaced {\n 266│            let _ = rename(old, target);\n 267│        }\n 268│    }\n    │...\n 487│        );\n*488│        // The second swap's original was displaced then restored by the same-swap undo.\n 489│        assert_eq!(\n 490│            fs::read(&second_target).unwrap(),\n 491│            b\"second-original\",\n    │...\n 530│    /// copy of a real image and SPAWN a process from it (image-map lock on Windows),\n*531│    /// then apply: the original is DISPLACED to `.old-1` (never renamed OVER the\n 532│    /// mapped `.old`), so apply SUCCEEDS and the target gets the new bytes.\n 533│    ///\n 534│    /// RED-FIRST: revert `fresh_old_path` to a fixed `.old` (rename-OVER) and on\n    │...\n 582│            b\"NEW-IMAGE-BYTES\",\n*583│            \"the target received the new bytes (the original was displaced, not replaced-over)\"\n 584│        );\n 585│    }\n 586│\n*587│    /// The displaced original lands at a FRESH `.old-N` when `.old` is occupied, and\n 588│    /// a plain (unmapped) stale `.old` is GC'd on the next apply.\n 589│    // [unit->REQ-CRC-SWAP-OLD-DISPLACE]\n 590│    #[test]\n    │...\n 631│\n*632│    /// CRASH-RECOVERY GUARD (doyle C1 review): when a prior run displaced the\n 633│    /// original to `<target>.old` then DIED before committing `<target>.new`, the\n 634│    /// target is MISSING and that `.old` is the LAST GOOD COPY. This apply must NOT\n 635│    /// GC it at Phase 0 (a failure before commit would leave NO binary) — it commits\n    │...\n 644│\n*645│        // The staged replacement exists; the TARGET is MISSING (a prior run displaced\n 646│        // it aside then crashed pre-commit) and `<target>.old` is the last good copy.\n 647│        write(&s.join(\"bin\"), b\"V2\");\n 648│        let stranded_old = i.join(\"bin.old\");\n## daemon.rs\n  139│        // attachment state starts from the config's startup defaults — an\n *140│        // unsaved attach/detach flip deliberately does not survive a daemon\n  141│        // restart (`--save` is the persistence path).\n  142│        // [impl->REQ-SUBNET-5]\n  143│        {\n     │...\n  155│                // [impl->REQ-CLI-OUTPUT-MARKDOWN]\n *156│                eprintln!(\"SUBNET_DETACHED:{name} (startup default — run: spt subnet attach {name})\");\n  157│            }\n  158│        }\n  159│\n     │...\n  206│                        // racing the network up at boot. Retry net in the\n *207│                        // background and attach it to the broker when it\n  208│                        // succeeds, so the node becomes reachable WITHOUT a\n  209│                        // manual restart; the brain notices `net-status` flip\n  210│                        // enabled and starts the consumers then.\n     │...\n  702│        }\n *703│        // setsid: detach into a NEW SESSION, not just a new process group —\n  704│        // a daemon auto-started inside an ssh/non-interactive session\n  705│        // otherwise dies with it (sshd HUPs the whole session on\n  706│        // disconnect; observed live in the 2026-06-06 fleet deploy, where\n     │...\n  854│    // worked before (just can't escape that job; logged for diagnosis).\n *855│    // [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n  856│    match create_process_detached(program, args, CREATE_BREAKAWAY_FROM_JOB) {\n  857│        Ok(pid) => Ok(pid),\n  858│        Err(e) if e.raw_os_error() == Some(ERROR_ACCESS_DENIED) => {\n     │...\n  879│/// CAN deny it (the L1 finding).\n *880│// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n  881│#[cfg(windows)]\n  882│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n  883│pub(crate) enum LaunchRung {\n     │...\n  896│/// pure decision seam can be unit-tested against it.\n *897│// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n  898│#[cfg(windows)]\n  899│pub(crate) fn launch_ladder() -> [LaunchRung; 4] {\n  900│    [\n     │...\n  911│/// without spawning anything.\n *912│// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n  913│#[cfg(windows)]\n  914│pub(crate) fn drive_ladder(\n  915│    ladder: [LaunchRung; 4],\n     │...\n  931│/// logging/return, not readiness (WMI/schtasks pids are the launcher's view).\n *932│// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n  933│#[cfg(windows)]\n  934│pub(crate) fn launch_daemon_job_neutral(program: &str, args: &[String]) -> io::Result<u32> {\n  935│    // Test hook (job_escape_e2e): force a single rung so a green that silently\n     │...\n  970│\n *971│/// Detach the calling process from its console (`FreeConsole`) — the broker's\n *972│/// self-detach for the auto-start ladder's window-allocating rungs. The WMI /\n  973│/// schtasks rungs launch the broker via `cmd /c … start \"\" /b spt daemon run`;\n  974│/// @15fdf58 set DETACHED_PROCESS on the cmd WRAPPER, but `start /b` then launched\n  975│/// the inner broker (a console-subsystem exe) with no console to inherit, so it\n     │...\n 1076│/// terminal Job), not ours.\n*1077│// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 1078│#[cfg(windows)]\n 1079│fn wrapped_daemon_command(program: &str, args: &[String]) -> String {\n 1080│    let mut s = String::from(\"cmd.exe /c \");\n     │...\n 1128│/// to the next rung — never a silent \"launched\".\n*1129│// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 1130│#[cfg(windows)]\n 1131│fn spawn_daemon_via_wmi(program: &str, args: &[String]) -> io::Result<u32> {\n 1132│    use base64::engine::general_purpose::STANDARD as B64;\n     │...\n 1187│/// quoting is best-effort.\n*1188│// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 1189│#[cfg(windows)]\n 1190│fn spawn_daemon_via_schtasks(program: &str, args: &[String]) -> io::Result<u32> {\n 1191│    use std::os::windows::process::CommandExt;\n     │...\n 1341│\n*1342│    // [unit->REQ-HAZARD-VIEWER-CLOSE-DETACH] the launch ladder order is exactly\n 1343│    // WMI → schtasks → breakaway → in-job: the job-neutral creators come FIRST\n 1344│    // (a daemon born outside any terminal job never needs to claw back out),\n 1345│    // breakaway is the demoted fallback, in-job the last resort.\n     │...\n 1385│\n*1386│    // [unit->REQ-HAZARD-VIEWER-CLOSE-DETACH] the pure decision seam: drive_ladder\n 1387│    // returns the FIRST rung that succeeds (rung-1 Ok → Wmi, nothing else tried).\n 1388│    #[cfg(windows)]\n 1389│    #[test]\n     │...\n 1399│\n*1400│    // [unit->REQ-HAZARD-VIEWER-CLOSE-DETACH] the error→rung map: a failing rung-1\n 1401│    // (WMI) falls through to rung-2 (schtasks); the returned rung is the one that\n 1402│    // actually produced the pid, NOT a silent rung-1 \"success\".\n 1403│    #[cfg(windows)]\n     │...\n 1417│\n*1418│    // [unit->REQ-HAZARD-VIEWER-CLOSE-DETACH] every rung failing surfaces the LAST\n 1419│    // error (never a false Ok) after trying all four in order.\n 1420│    #[cfg(windows)]\n 1421│    #[test]\n     │...\n 1441│\n*1442│    // [unit->REQ-HAZARD-VIEWER-CLOSE-DETACH] the MECHANISM proof: a child spawned\n 1443│    // by the real `detached_no_inherit` (CREATE_BREAKAWAY_FROM_JOB) ESCAPES a\n 1444│    // KILL_ON_JOB_CLOSE job, so terminating the job does NOT reap it — the OS\n 1445│    // contract the daemon's tab-close survival relies on. The inverse of reap.rs's\n     │...\n 1650│        // exact breakaway-unreliability that makes WMI the PRIMARY launcher\n*1651│        // (REQ-HAZARD-VIEWER-CLOSE-DETACH); the WMI escape is proven by\n 1652│        // `job_escape_e2e`, and the no-regression fallback by\n 1653│        // `detached_no_inherit_falls_back_under_a_breakaway_denying_job`.\n 1654│        if diag.contains(\"gc_in_any_job=1\") {\n## deadline.rs\n  58│pub struct DeadlineAnchor {\n *59│    /// Epoch-ms grid origin. Periodic fires land at `anchor_ms + k·interval_ms`.\n  60│    pub anchor_ms: u64,\n  61│    /// Grid spacing in ms. Always `> 0` (the constructor degrades a zero value).\n  62│    pub interval_ms: u64,\n    │...\n 190│pub struct OneShotDeadline {\n*191│    /// Absolute epoch-ms target. Fires once when `now_ms >= target_ms`.\n 192│    pub target_ms: u64,\n 193│}\n 194│\n## deelevate.rs\n 512│    // are rejected with ERROR_INVALID_PARAMETER (87; hit live, second\n*513│    // elevated verification round). Detach therefore rides\n 514│    // CREATE_NEW_CONSOLE with the console window HIDDEN via the startup\n 515│    // info (STARTF_USESHOWWINDOW + SW_HIDE) — the child still owns no\n 516│    // caller console and no window appears. CREATE_NEW_PROCESS_GROUP keeps\n    │...\n 526│    /// daemon-owned reap, never a failed spawn).\n*527│    /// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 528│    const CREATE_BREAKAWAY_FROM_JOB: u32 = 0x0100_0000;\n 529│    /// STARTUPINFOW.dwFlags: honor wShowWindow.\n 530│    const STARTF_USESHOWWINDOW: u32 = 0x0000_0001;\n    │...\n 600│    /// Whether an elevated `daemon run` here WOULD respawn de-elevated and\n*601│    /// detach (vanish into the background) — i.e. a real de-elevation target\n 602│    /// exists: elevated AND a desktop shell token exists AND that shell is\n 603│    /// itself unelevated. `false` when not elevated, when there is no desktop\n 604│    /// shell (a headless / service session — e.g. an elevated CI runner), or\n## dispatch.rs\n  23│//! The kind-tagged families collide on variant names (`request` is both\n *24│//! sync's and attach's opener; `chunk`/`done`/`err` recur everywhere), so the\n  25│//! demux keys on the first line's **shape**, not its kind alone:\n  26│//!\n  27│//! | first line | family |\n    │...\n  29│//! | `kind=request` + `sync_id` | sync pull ([`crate::sync::serve_sync`]) |\n *30│//! | `kind=request` + `session_id` | remote-drive attach (D9-1-3) |\n  31│//! | `kind=request` + `endpoint`+`event` | remote rest op ([`crate::resthost::serve_rest`]) |\n  32│//! | `kind=request` + `owner`+`shell_ref` | cross-node shell link ([`crate::linkhost::serve_shell_link`]) |\n  33│//! | `kind=query` | update pull ([`crate::propagate::serve_update`]) |\n    │...\n  74│\n *75│use crate::attach::serve_attach;\n  76│use crate::brain::{Brain, BrokerEvent};\n  77│use crate::notifsync::{apply_notif_feed, NotifPolicy};\n  78│use crate::propagate::serve_update;\n    │...\n 101│    Registry,\n*102│    /// A remote-drive attach (`AttachRecord::Request`).\n*103│    Attach,\n 104│    /// A file transfer (`XferRecord::Fetch`/`Push`).\n 105│    Xfer,\n 106│    /// A remote rest op (`RestRecord::Request` — suspend/wake from another\n    │...\n 112│    /// A subnet-scoped liveness probe (`ServeProbeRecord::Request` — \"serving\n*113│    /// subnet X?\", the `--nodes` view's detach-aware refinement, REQ-SUBNET-5).\n 114│    ServeProbe,\n 115│    /// Unclassifiable — dropped.\n 116│    Unknown,\n    │...\n 122│        Some(\"request\") if line.get(\"sync_id\").is_some() => StreamFamily::Sync,\n*123│        Some(\"request\") if line.get(\"session_id\").is_some() => StreamFamily::Attach,\n 124│        Some(\"request\") if line.get(\"endpoint\").is_some() && line.get(\"event\").is_some() => {\n 125│            StreamFamily::Rest\n 126│        }\n    │...\n 195│/// stream to a family worker, until `stop` is set. Blocks the calling thread\n*196│/// (the daemon spawns it on its own); workers detach (each ends with its\n 197│/// stream).\n 198│pub fn run_dispatch_loop(\n 199│    broker_name: &str,\n    │...\n 334│        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n*335│        StreamFamily::Attach => {\n 336│            // The gate subject is the session's OWNING endpoint, resolved\n 337│            // from the broker's table (the spawn's label) — never from wire\n 338│            // bytes. An unlabeled or unknown session refuses, fail-closed.\n 339│            //\n*340│            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n 341│            // cannot know our local session id, so it names the ENDPOINT. When the\n 342│            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n 343│            // we actually host a live session for it (D5 session-presence gate;\n 344│            // D6: a stale registry row that named a gone endpoint resolves to no\n*345│            // session → refuse cleanly, never a false attach). The endpoint id is\n 346│            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\n 347│            // `endpoint`); the origin subject stays the handshake identity. The\n 348│            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\n    │...\n 352│            // before serve_attach subscribes — never mid-serve, which would swallow\n*353│            // interleaved attach records) and `None` on the local path.\n 354│            let (endpoint, remote_session) = match line\n 355│                .get(\"endpoint_id\")\n 356│                .and_then(|v| v.as_str())\n    │...\n 358│            {\n*359│                Some(ep) => match crate::attach::resolve_local_session(&mut brain, ep) {\n 360│                    Some(sid) => (Some(ep.to_string()), Some(sid)),\n 361│                    None => (None, None),\n 362│                },\n    │...\n 392│                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n*393│                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n 394│                }\n 395│            }\n 396│        }\n    │...\n 712│\n*713│        let attach = json!({\"kind\": \"request\", \"session_id\": 3, \"from_seq\": 0});\n*714│        assert_eq!(classify_first_line(&attach), StreamFamily::Attach);\n 715│\n 716│        // [unit->REQ-INST-6] the rest op's opener disambiguates by shape too.\n 717│        let rest =\n    │...\n 736│        let reg = json!({\"subnet\": \"home\", \"endpoint_id\": \"ling\",\n*737│            \"instance\": {\"node\": \"n1\", \"status\": \"Active\", \"epoch\": 4}});\n 738│        assert_eq!(classify_first_line(&reg), StreamFamily::Registry);\n 739│    }\n 740│\n## effect.rs\n  129│/// monotonically SHRINKS (a live producer MUST stamp a real tag, never `legacy`).\n *130│/// `psyche`/`epoch` are deliberately ABSENT — they are the `EpochSource` notif/lease\n  131│/// counter domain, never broker `apply_once` producers (Amendment 2); a journal tag\n  132│/// with no stamp site is a doc'd-but-dead knob. The tag is TEXTUAL so a recovered\n  133│/// journal line is self-describing (see [`Minter::as_tag`]).\n     │...\n  241│\n *242│/// Run a tracing-only op (ADR-0034 Decision 2: the rest family + rc attach\n  243│/// stream-open) with a ONE-shot self-heal. On the typed no-longer-held reply (a\n  244│/// broker restart dropped the conn/stream the op had journaled) re-mint a FRESH\n  245│/// op from the SAME minter and run ONCE more — a tracing op's redelivery \"needs\n     │...\n  367│    ///\n *368│    /// **Bug #16 (REQ-BROKER-ATTACH-JOURNAL-RESILIENT):** the broker outlives brain\n  369│    /// respawns, so a single panic while the journal lock was held would otherwise\n  370│    /// poison the mutex permanently — and every subsequent journaled op\n  371│    /// (`dispatch_net_stream_open` → `apply_once`) would then panic its per-conn\n     │...\n  378│    /// re-driven effect either dedups or re-attempts — no double-apply, no lost op.\n *379│    // [impl->REQ-BROKER-ATTACH-JOURNAL-RESILIENT]\n  380│    fn lock_recover(&self) -> std::sync::MutexGuard<'_, Inner> {\n  381│        self.inner\n  382│            .lock()\n     │...\n  688│\n *689│    // [unit->REQ-BROKER-ATTACH-JOURNAL-RESILIENT] bug #16: a poisoned journal mutex\n  690│    // must NOT permanently brick subsequent journaled ops. Before the fix every\n  691│    // `apply_once` after a panic-while-locked hit `.expect(\"effect journal lock\n  692│    // poisoned\")` and killed its per-conn reply thread → attaches deadlined. Now\n     │...\n  812│    // GLOBAL journal lock forever — so EVERY other journaled effect (another\n *813│    // keystroke, another attach's journaled input) wedges, and the single-\n  814│    // threaded dispatch can no longer service `spt rc` attaches. Both units below\n  815│    // FAIL against the current (unfixed) code = the RED repro. They COMPILE\n  816│    // against current APIs (no use of the not-yet-existing `is_durable()`).\n     │...\n 1014│        }\n*1015│        // The dropped-minter proof: psyche/epoch are the EpochSource domain, never\n 1016│        // journal minters — parsing their would-be tags yields None.\n 1017│        assert_eq!(Minter::from_tag(\"psyche\"), None);\n*1018│        assert_eq!(Minter::from_tag(\"epoch\"), None);\n 1019│        assert_eq!(Minter::from_tag(\"bogus\"), None);\n 1020│        // Legacy is a real, parseable tag (the reserved-shrinking discriminant).\n 1021│        assert_eq!(Minter::from_tag(\"legacy\"), Some(Minter::Legacy));\n## endpoint.rs\n 86│\n*87│/// Where the peer pump records its last-tick heartbeat (epoch ms, M8\n 88│/// decision 23 / REQ-DAEMON-5): written each pump tick (D4), read by\n 89│/// `spt daemon status` so a half-dead daemon (pump stalled, dispatcher\n 90│/// alive — the 2026-06-07 hfenduleam shape) renders honestly.\n   │...\n 94│\n*95│/// Read the pump's last-tick heartbeat (epoch ms). `None` = never written\n 96│/// (a pre-M8 daemon, or one that has not completed a first tick).\n 97│// [impl->REQ-CLI-2]\n 98│pub fn read_pump_heartbeat() -> Option<u64> {\n## frame.rs\n 161│            \"protocol_version\": 2,\n*162│            \"kind\": \"attach\",\n 163│            \"payload\": { \"session\": \"abc\" },\n 164│            \"future_field_an_older_broker_never_heard_of\": { \"nested\": [1, 2, 3] }\n 165│        })\n    │...\n 170│        assert_eq!(decoded.protocol_version, 2);\n*171│        assert_eq!(decoded.kind, \"attach\");\n 172│        assert_eq!(decoded.payload, json!({ \"session\": \"abc\" }));\n 173│    }\n 174│\n## grants.rs\n 204│    node_hex: &str,\n*205│    epochs: &mut spt_store::epoch::EpochSource,\n 206│    subnet: &str,\n 207│    policy: &crate::notif::NotifSurfacePolicy,\n 208│    owlery: &Path,\n    │...\n 540│            let store = spt_store::notif::NotifStore::open_at(&home.join(\"notifs.db\")).unwrap();\n*541│            let mut epochs = spt_store::epoch::EpochSource::load_from(&home.join(\"epoch\"));\n 542│            let policy = NotifSurfacePolicy {\n 543│                subnets: vec![(\"home\".into(), false)],\n 544│                vis: VisibilityStore::default(),\n\n## bin/\n### xlate_choreo_fixture.rs\n  33│//! set). The int test reads that file and asserts NO operator keystroke payload\n *34│//! (e.g. the controller's typed bytes) ever appears — only `init` / `event` /\n  35│//! content-free `input` lines do. The binary never sees PTY keystroke content.\n  36│\n  37│use std::fs::OpenOptions;\n    │...\n 101│            // emit `{\"commit\":true}`. The broker's inject worker must hit its\n*102│            // commit deadline, flush the buffered controller input, release the\n 103│            // floor, and FAULT this binary (after which idle inbound SPOOLS — raw\n 104│            // inject was removed as a delivery path, ADR-0022 amendment).\n 105│            //\n    │...\n 143│/// truncation and many settle-chunks wide, yet it fits inside a 24-row screen grid so a\n*144│/// cold-attach repaint (ADR-0031 = current screen) still carries the HEAD (a taller\n 145│/// payload scrolls HEADSTART off the visible grid).\n 146│fn big_multiline_payload() -> String {\n 147│    let mut s = String::from(\"HEADSTART \");","missingPaths":["packages","test"]},"isError":false,"timestamp":1784195740876}}
{"type":"message","id":"afbe3b44","parentId":"e915002d","timestamp":"2026-07-16T09:55:52.948Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning targeted code and doc exploration**"},{"type":"toolCall","id":"call_3IKqHIktj2J8HWbHyGeemMdx|fc_008390f508d1d09b016a58aaa8e45c819ab59544c8e9a5558d","name":"read","arguments":{"i":"Reading project domain model","path":"CONTEXT.md","selector":"1-280"}},{"type":"toolCall","id":"call_OqH0NzPjFkqL2rz1g6oen8Jc|fc_008390f508d1d09b016a58aaa8e46c819a9e933dd3ed5d7d07","name":"read","arguments":{"i":"Reading PTY known hazards","path":"docs/KNOWN-HAZARDS.md","selector":null}},{"type":"toolCall","id":"call_3FjeLfkr7FlP82E8sVAv6U6d|fc_008390f508d1d09b016a58aaa8e470819ab95d840ba1914735","name":"read","arguments":{"i":"Reading debug rollout guidance","path":"docs/DEBUG-ROLLOUT.md","selector":null}},{"type":"toolCall","id":"call_cXbNJeHSL2BSYlABw8AL5QuH|fc_008390f508d1d09b016a58aaa8e478819a83aca8f4bf8238d4","name":"read","arguments":{"i":"Reading broker isolation ADR","path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","selector":"1-280"}},{"type":"toolCall","id":"call_8VY2CDrB9Q7BvXnlj91ab73T|fc_008390f508d1d09b016a58aaa8e47c819a8250f378bb6801bb","name":"read","arguments":{"i":"Reading PTY digest ADR","path":"docs/adr/0008-live-activity-buffer-pty-digest.md","selector":"1-260"}},{"type":"toolCall","id":"call_4ds6uMDcA5Srp2FO1zlnMDLi|fc_008390f508d1d09b016a58aaa8e480819aaca6d500119b60ab","name":"read","arguments":{"i":"Reading screen repaint ADR","path":"docs/adr/0031-server-side-screen-grid-render-repaint.md","selector":"1-260"}},{"type":"toolCall","id":"call_dcCMiQIhh8QexYXiedVEE8yG|fc_008390f508d1d09b016a58aaa8e484819aae0e765a82272612","name":"grep","arguments":{"i":"Locating rc command implementation","pattern":"\\b(RemoteControl|remote_control|run_rc|cmd_rc|rc_attach|AttachIntent|--take|--view|Detach)\\b","path":"crates/spt/src;crates/spt/tests","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_BcHSYYk9Gf6WMWVS1hBSSNIP|fc_008390f508d1d09b016a58aaa8e494819abab7e9a17d562b00","name":"grep","arguments":{"i":"Indexing broker ownership symbols","pattern":"^(pub\\(crate\\) |pub )?(struct|enum|fn|impl) |become_controller|stall_evict_controller|reap_dead_controller|detach_conn|controller_writer|viewer_writer|SubscribeOutcome|controller_epoch","path":"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/conn.rs;crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/attach.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":16073,"output":507,"cacheRead":13824,"cacheWrite":0,"totalTokens":30404,"reasoningTokens":58,"cost":{"input":0.080365,"output":0.01521,"cacheRead":0.006912,"cacheWrite":0,"total":0.10248700000000001}},"stopReason":"toolUse","timestamp":1784195740916,"responseId":"resp_008390f508d1d09b016a58aa9d3ec8819aa90cec996f8b060e","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_008390f508d1d09b016a58aa9fabf4819aa4ac6fdec9d99b77","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKqoDUeGZGKwv80ZX1atLjQJW-B-ku1_BHdOPF_kdCpNjhfocl92Hk6eF-BnJwR-6LDU5dw5WNcYTGxKcBENouujczogS_ThmGpDylBqaqI5fZST8yNsEQrDGVCqYAYISvNUT97EjcX4Rulv5jrHhC4HnrCocSV9nHCv6fgET6HhDufcwDs5N-GPMqcYsvyKOq40t6qQmczdpNvGRnTGfzbMJmsID6DwZcuMtydTFpfqnrRmxekyGAUdsswAevLSEqjNYwXFe39hT2UGrKi9HqwfvMHqTRVNsQuQxKQ2Wi6hyi16X0zyuPTip2a4el6y0WgfIJ8sk3B4NhVUwN2itQD7Fpk_yIKDnJaR83F-xKwZhke446qRA1mhtqn8tG8ElYNXFjMc8wnZjWyodI6-WazWq_Milp95YW-Rj7t6IXfifI5xswk8nTp3FBE0SX5pTqiLcE4h1reeQWGhZfkpodOd-ak68XzLYhPKL4z4qUysqTsF7UCHV-3OzngcXedn8JXyPSbN1Il4YYSaQGaoUCRUftPTWsm4XCr8fi-sWNL0FwPqnryrAibhfZHpFa5ZtxBRy60_1iO0AcpO5WcWq2X9lLW_feTCTkEiSMs4_h5VHpL9qu3PFjYSejwPpB9Q3hxfq-WDAVA-pG6ZU6f_3Z1pcXe7-61zXxZLyZ8ZDM834nDfa0ZP-nwOZ0-58DFHyqaU9zpnv3aCqXg91Pp6_rvgOhr10uITxxjxPhq5wKP1IXiBv8qKCWAceN9HyxTYjcGsmYBhM73FZVPD5ZevmsFe_D1g-TqhDzyJTI_NLsaGQqD3xyz20K3GP4KP-E6hK3tdf9glTaMW4FogMuKquU5_Kt1gWSL8ic9IZi5YNF6Bad-1d1XqWQxmXWmADdcUDqXZBLC8pqnLdL0-hRvvVW1cdmpdvVY43Fqb5LvFn0o3X7pA8QK1LdTzw7aI4MHqsbJgC1s59lyxswxMOKOoiaQaV9oDxA4wf596HTumYL8Zg87lX2hif9twAuE29ZsJsKbHKlUWA1TFFbzOkOZbSJ-0iLhy7KeSXBSyOdC0FCtsMszY5USXEHfNS5JnPVWFjL4QruFQoBS_KrUZW5hwwnMB2tiWGtrrS3FVdIo2q-PrykvRT6wbAbNMAV52nu_ocKJ0_7hem3k7SyiJadBKuCUJew_IIDu4E5otNkNeALFn2N1OC_A_CmIW9wusKBfx1fj3UZtXvFl5rlXHLsgADcWRx6KMY7-7dK3FgKIlYa3tcy36PtclphDTs5mpXa9ZDi09NKzD6z8bHRf4Yjw5NJ3W2g==","summary":[{"type":"summary_text","text":"**Planning targeted code and doc exploration**"}]},{"id":"fc_008390f508d1d09b016a58aaa8e45c819ab59544c8e9a5558d","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading project domain model\",\"path\":\"CONTEXT.md\",\"selector\":\"1-280\"}","call_id":"call_3IKqHIktj2J8HWbHyGeemMdx","name":"read"},{"id":"fc_008390f508d1d09b016a58aaa8e46c819a9e933dd3ed5d7d07","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading PTY known hazards\",\"path\":\"docs/KNOWN-HAZARDS.md\",\"selector\":null}","call_id":"call_OqH0NzPjFkqL2rz1g6oen8Jc","name":"read"},{"id":"fc_008390f508d1d09b016a58aaa8e470819ab95d840ba1914735","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading debug rollout guidance\",\"path\":\"docs/DEBUG-ROLLOUT.md\",\"selector\":null}","call_id":"call_3FjeLfkr7FlP82E8sVAv6U6d","name":"read"},{"id":"fc_008390f508d1d09b016a58aaa8e478819a83aca8f4bf8238d4","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker isolation ADR\",\"path\":\"docs/adr/0018-broker-brain-process-isolation-restoration.md\",\"selector\":\"1-280\"}","call_id":"call_cXbNJeHSL2BSYlABw8AL5QuH","name":"read"},{"id":"fc_008390f508d1d09b016a58aaa8e47c819a8250f378bb6801bb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading PTY digest ADR\",\"path\":\"docs/adr/0008-live-activity-buffer-pty-digest.md\",\"selector\":\"1-260\"}","call_id":"call_8VY2CDrB9Q7BvXnlj91ab73T","name":"read"},{"id":"fc_008390f508d1d09b016a58aaa8e480819aaca6d500119b60ab","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading screen repaint ADR\",\"path\":\"docs/adr/0031-server-side-screen-grid-render-repaint.md\",\"selector\":\"1-260\"}","call_id":"call_4ds6uMDcA5Srp2FO1zlnMDLi","name":"read"},{"id":"fc_008390f508d1d09b016a58aaa8e484819aae0e765a82272612","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating rc command implementation\",\"pattern\":\"\\\\b(RemoteControl|remote_control|run_rc|cmd_rc|rc_attach|AttachIntent|--take|--view|Detach)\\\\b\",\"path\":\"crates/spt/src;crates/spt/tests\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_dcCMiQIhh8QexYXiedVEE8yG","name":"grep"},{"id":"fc_008390f508d1d09b016a58aaa8e494819abab7e9a17d562b00","type":"function_call","status":"completed","arguments":"{\"i\":\"Indexing broker ownership symbols\",\"pattern\":\"^(pub\\\\(crate\\\\) |pub )?(struct|enum|fn|impl) |become_controller|stall_evict_controller|reap_dead_controller|detach_conn|controller_writer|viewer_writer|SubscribeOutcome|controller_epoch\",\"path\":\"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/conn.rs;crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/attach.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_BcHSYYk9Gf6WMWVS1hBSSNIP","name":"grep"}]},"duration":12030.673100000247,"ttft":2734.167900000699,"contextSnapshot":{"promptTokens":29897,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_3IKqHIktj2J8HWbHyGeemMdx|fc_008390f508d1d09b016a58aaa8e45c819ab59544c8e9a5558d","toolName":"read","startedAt":"2026-07-16T09:55:52.948Z","args":{"path":"CONTEXT.md"},"intent":"Reading project domain model"},"id":"5646bceb","parentId":"afbe3b44","timestamp":"2026-07-16T09:55:52.948Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_OqH0NzPjFkqL2rz1g6oen8Jc|fc_008390f508d1d09b016a58aaa8e46c819a9e933dd3ed5d7d07","toolName":"read","startedAt":"2026-07-16T09:55:52.948Z","args":{"path":"docs/KNOWN-HAZARDS.md"},"intent":"Reading PTY known hazards"},"id":"fba14088","parentId":"5646bceb","timestamp":"2026-07-16T09:55:52.948Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_3FjeLfkr7FlP82E8sVAv6U6d|fc_008390f508d1d09b016a58aaa8e470819ab95d840ba1914735","toolName":"read","startedAt":"2026-07-16T09:55:52.949Z","args":{"path":"docs/DEBUG-ROLLOUT.md"},"intent":"Reading debug rollout guidance"},"id":"78dbc580","parentId":"fba14088","timestamp":"2026-07-16T09:55:52.949Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_cXbNJeHSL2BSYlABw8AL5QuH|fc_008390f508d1d09b016a58aaa8e478819a83aca8f4bf8238d4","toolName":"read","startedAt":"2026-07-16T09:55:52.949Z","args":{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md"},"intent":"Reading broker isolation ADR"},"id":"3837f3df","parentId":"78dbc580","timestamp":"2026-07-16T09:55:52.949Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_8VY2CDrB9Q7BvXnlj91ab73T|fc_008390f508d1d09b016a58aaa8e47c819a8250f378bb6801bb","toolName":"read","startedAt":"2026-07-16T09:55:52.949Z","args":{"path":"docs/adr/0008-live-activity-buffer-pty-digest.md"},"intent":"Reading PTY digest ADR"},"id":"bed5ce2a","parentId":"3837f3df","timestamp":"2026-07-16T09:55:52.949Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4ds6uMDcA5Srp2FO1zlnMDLi|fc_008390f508d1d09b016a58aaa8e480819aaca6d500119b60ab","toolName":"read","startedAt":"2026-07-16T09:55:52.949Z","args":{"path":"docs/adr/0031-server-side-screen-grid-render-repaint.md"},"intent":"Reading screen repaint ADR"},"id":"fba341c0","parentId":"bed5ce2a","timestamp":"2026-07-16T09:55:52.949Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_dcCMiQIhh8QexYXiedVEE8yG|fc_008390f508d1d09b016a58aaa8e484819aae0e765a82272612","toolName":"grep","startedAt":"2026-07-16T09:55:52.949Z","args":{"path":"crates/spt/src;crates/spt/tests"},"intent":"Locating rc command implementation"},"id":"6c2a2a67","parentId":"fba341c0","timestamp":"2026-07-16T09:55:52.949Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BcHSYYk9Gf6WMWVS1hBSSNIP|fc_008390f508d1d09b016a58aaa8e494819abab7e9a17d562b00","toolName":"grep","startedAt":"2026-07-16T09:55:52.949Z","args":{"path":"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/conn.rs;crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/attach.rs"},"intent":"Indexing broker ownership symbols"},"id":"00cbb7e7","parentId":"6c2a2a67","timestamp":"2026-07-16T09:55:52.949Z"}
{"type":"message","id":"30dd4445","parentId":"00cbb7e7","timestamp":"2026-07-16T09:55:52.958Z","message":{"role":"toolResult","toolCallId":"call_4ds6uMDcA5Srp2FO1zlnMDLi|fc_008390f508d1d09b016a58aaa8e480819aaca6d500119b60ab","toolName":"read","content":[{"type":"text","text":"# Server-side screen grid + clean repaint on attach\n\n## Status\n\naccepted (2026-06-30) · companion to `V0.19.0-P6-SCREEN-GRID-DESIGN.md` · relates to [ADR-0008](0008-live-activity-buffer-pty-digest.md) (render-vs-content split) and [ADR-0001](0001-clean-room-fork-from-claude-skill-owl.md) (clean-room provenance).\n\n## Context\n\nA cold attach streams the entire retained **raw-byte ring** as its initial batch (`become_controller`, broker.rs:402-416). For an alt-screen TUI (Claude Code) that ring is a mid-stream rendering protocol; replayed raw into a fresh terminal it corrupts the screen + scrollback (field bugs #6/#7/#8). We need a server-side render model that interprets the byte stream into an authoritative current screen and synthesizes one clean repaint at attach.\n\nTwo questions this ADR settles:\n1. Does a server-side PTY-byte interpreter **contradict ADR-0008**, which retired the PTY-byte parser?\n2. May we take a **third-party VT parser** dependency under ADR-0001 (clean-room everything except the sister project's stable wire/schema formats)?\n\n## Decision\n\n<!-- [doc->REQ-BROKER-SCREEN-GRID] the server-side render grid + clean repaint on attach:\n     a clean-room `ScreenGrid` (vte::Perform) interprets the byte stream into an\n     authoritative current screen; `become_controller`/`add_viewer` emit one synthesized\n     repaint instead of the raw ring. Companion design: V0.19.0-P6-SCREEN-GRID-DESIGN.md. -->\n\n**1. The render grid does NOT reverse ADR-0008.** ADR-0008's governing split (2026-06-12 amendment) is: **render surfaces read the PTY; content surfaces read the logs.** It retired the PTY-byte parser only for **content** — the digest, which now projects from normalized session logs — because extracting *semantic turn content* from a repaint-heavy byte stream is fragile and harness-specific. The screen grid is a **render** surface: it reconstructs the *visible screen*, not conversation content. It is the render-reads-PTY side ADR-0008 explicitly preserved, and it resolves the exact corruption ADR-0008 named (\"alt-screen PTY = rendering protocol; repaints corrupt it\"). The content parser stays retired. This ADR records the distinction so a future reader does …\n\n**2. Take a vetted VT-parser dependency (`vte`), clean-room the grid.** The byte→action layer is `vte` (the alacritty terminal parser state machine): small, single-purpose, battle-tested against real-world escape sequences. The `ScreenGrid` (cells, cursor, main/alt buffer, modes) implementing `vte::Perform` is clean-room. Rationale under ADR-0001: ADR-0001 defaults to clean-room, but its intent is to avoid copying the sister project's evolving logic and to keep provenance clean — a **universal, standardized, spec-fragile-at-the-edges VT state machine** is neither sister-derived nor a place where re-implementation adds value; a hand-rolled parser's edge-sequence bugs would produce precisely the rendering artifacts #6 exists to eliminate. The dependency is nar…\n\n**Scope:** current screen only (viewport + cursor + alt/main). Scrollback stays the raw ring (history). The grid drives only the attach repaint. Wire format unchanged (`AttachRecord::Output`); only the initial-batch *content* changes. See the design doc for architecture, hazard integration, and the build plan.\n\n**Amendment (2026-07-06, REMOTE-TRUTH E-2):** the repaint also carries the child's **last-set window title** (OSC 0/2, captured on the grid, cleared by RIS) — a cold attach previously painted a perfect screen under the attacher's stale title because `vte`'s default drops OSC.\n\n## Consequences\n\n- New first-party dependency `vte`; folded into the release Cargo.lock first-party-name discipline (a version pin + audit at cut).\n- The grid is the **OS-neutral render-replay layer** CONTEXT §terminal anticipated (forkpty raw-pipe vs ConPTY repaint-on-resize both feed one grid); it slots behind `SessionSurface`/`OutputStream`.\n- `become_controller`/`add_viewer` emit a synthesized clean repaint as the initial batch; live frames after attach are unchanged raw bytes. Cross-node attach (#4) and any client are agnostic.\n- Must preserve the attach/serve hazard invariants (REQ-HAZARD-INJECT-CONTROL-COEXIST 7.12, CONTROLLER-WRITER-REORDER 7.21, VIEWER-ISOLATION 7.7 + skip-to-live, the wedge family) — the grid is on the output path only, touches neither the input/effect journal nor the QUIC deadlines.\n- Unlocks the follow-up sticky ID-marker redo (#8/#14 marker-half) as a grid-backed per-frame overlay (separate REQ).\n- Composes with #12 (REQ-RC-WIN-VT-OUTPUT): the repaint is ANSI bytes; on Win10 conhost it renders only once VT-output-processing is enabled client-side (#12).\n\n\n[Some lines truncated to 768 chars]"}],"details":{"displayContent":{"text":"# Server-side screen grid + clean repaint on attach\r\n\r\n## Status\r\n\r\naccepted (2026-06-30) · companion to `V0.19.0-P6-SCREEN-GRID-DESIGN.md` · relates to [ADR-0008](0008-live-activity-buffer-pty-digest.md) (render-vs-content split) and [ADR-0001](0001-clean-room-fork-from-claude-skill-owl.md) (clean-room provenance).\r\n\r\n## Context\r\n\r\nA cold attach streams the entire retained **raw-byte ring** as its initial batch (`become_controller`, broker.rs:402-416). For an alt-screen TUI (Claude Code) that ring is a mid-stream rendering protocol; replayed raw into a fresh terminal it corrupts the screen + scrollback (field bugs #6/#7/#8). We need a server-side render model that interprets the byte stream into an authoritative current screen and synthesizes one clean repaint at attach.\r\n\r\nTwo questions this ADR settles:\r\n1. Does a server-side PTY-byte interpreter **contradict ADR-0008**, which retired the PTY-byte parser?\r\n2. May we take a **third-party VT parser** dependency under ADR-0001 (clean-room everything except the sister project's stable wire/schema formats)?\r\n\r\n## Decision\r\n\r\n<!-- [doc->REQ-BROKER-SCREEN-GRID] the server-side render grid + clean repaint on attach:\r\n     a clean-room `ScreenGrid` (vte::Perform) interprets the byte stream into an\r\n     authoritative current screen; `become_controller`/`add_viewer` emit one synthesized\r\n     repaint instead of the raw ring. Companion design: V0.19.0-P6-SCREEN-GRID-DESIGN.md. -->\r\n\r\n**1. The render grid does NOT reverse ADR-0008.** ADR-0008's governing split (2026-06-12 amendment) is: **render surfaces read the PTY; content surfaces read the logs.** It retired the PTY-byte parser only for **content** — the digest, which now projects from normalized session logs — because extracting *semantic turn content* from a repaint-heavy byte stream is fragile and harness-specific. The screen grid is a **render** surface: it reconstructs the *visible screen*, not conversation content. It is the render-reads-PTY side ADR-0008 explicitly preserved, and it resolves the exact corruption ADR-0008 named (\"alt-screen PTY = rendering protocol; repaints corrupt it\"). The content parser stays retired. This ADR records the distinction so a future reader does …\n\r\n**2. Take a vetted VT-parser dependency (`vte`), clean-room the grid.** The byte→action layer is `vte` (the alacritty terminal parser state machine): small, single-purpose, battle-tested against real-world escape sequences. The `ScreenGrid` (cells, cursor, main/alt buffer, modes) implementing `vte::Perform` is clean-room. Rationale under ADR-0001: ADR-0001 defaults to clean-room, but its intent is to avoid copying the sister project's evolving logic and to keep provenance clean — a **universal, standardized, spec-fragile-at-the-edges VT state machine** is neither sister-derived nor a place where re-implementation adds value; a hand-rolled parser's edge-sequence bugs would produce precisely the rendering artifacts #6 exists to eliminate. The dependency is nar…\n\r\n**Scope:** current screen only (viewport + cursor + alt/main). Scrollback stays the raw ring (history). The grid drives only the attach repaint. Wire format unchanged (`AttachRecord::Output`); only the initial-batch *content* changes. See the design doc for architecture, hazard integration, and the build plan.\r\n\r\n**Amendment (2026-07-06, REMOTE-TRUTH E-2):** the repaint also carries the child's **last-set window title** (OSC 0/2, captured on the grid, cleared by RIS) — a cold attach previously painted a perfect screen under the attacher's stale title because `vte`'s default drops OSC.\r\n\r\n## Consequences\r\n\r\n- New first-party dependency `vte`; folded into the release Cargo.lock first-party-name discipline (a version pin + audit at cut).\r\n- The grid is the **OS-neutral render-replay layer** CONTEXT §terminal anticipated (forkpty raw-pipe vs ConPTY repaint-on-resize both feed one grid); it slots behind `SessionSurface`/`OutputStream`.\r\n- `become_controller`/`add_viewer` emit a synthesized clean repaint as the initial batch; live frames after attach are unchanged raw bytes. Cross-node attach (#4) and any client are agnostic.\r\n- Must preserve the attach/serve hazard invariants (REQ-HAZARD-INJECT-CONTROL-COEXIST 7.12, CONTROLLER-WRITER-REORDER 7.21, VIEWER-ISOLATION 7.7 + skip-to-live, the wedge family) — the grid is on the output path only, touches neither the input/effect journal nor the QUIC deadlines.\r\n- Unlocks the follow-up sticky ID-marker redo (#8/#14 marker-half) as a grid-backed per-frame overlay (separate REQ).\r\n- Composes with #12 (REQ-RC-WIN-VT-OUTPUT): the repaint is ANSI bytes; on Win10 conhost it renders only once VT-output-processing is enabled client-side (#12).\r\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\adr\\0031-server-side-screen-grid-render-repaint.md"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195752958}}
{"type":"message","id":"14c42d22","parentId":"30dd4445","timestamp":"2026-07-16T09:55:52.958Z","message":{"role":"toolResult","toolCallId":"call_3FjeLfkr7FlP82E8sVAv6U6d|fc_008390f508d1d09b016a58aaa8e470819ab95d840ba1914735","toolName":"read","content":[{"type":"text","text":"# Debug rollout runbook\n\n<!-- [doc->REQ-UPD-6] -->\n\nDebug rollout is the maintainer-only fast path for testing a local spt-core\nbuild across a trusted lab subnet. It uses the normal signed self-update\nsubstrate: no raw peer file-copy, no production `spt` CLI surface, and no\nembedded debug trust anchor.\n\nUse this when debugging needs a local build to reach multiple lab nodes quickly.\nDo not use it for public releases; use `docs/RELEASE-RUNBOOK.md` for that.\n\n## Mental model\n\n- A rollout is a signed `SignedUpdateSet`: one signed metadata record, with one\n  artifact digest per Rust target triple.\n- Each recipient verifies the set under its node-local `release-keys.json`,\n  selects only its own platform artifact, stages it, and then follows the normal\n  consent/apply policy.\n- Debug and stable are separate channels. A debug-pinned node accepts only\n  `channel = \"debug\"` offers; a stable-pinned node accepts only stable offers.\n- Debug versions are monotonic within the debug channel. To recover from a bad\n  debug build, publish a higher debug version, even if the bytes are a previous\n  known-good binary.\n- A broker-touching candidate may be staged, but apply refuses it while\n  broker-held runtime is live. Shut down or suspend hosted endpoints first; the\n  first flow does not auto-cycle them.\n\n## One-time lab setup\n\nGenerate a debug key on the coordinator:\n\n```powershell\ncargo run -p xtask -- debug-keygen dev-debug-2026\n```\n\nSet the printed seed as a long-lived coordinator-local environment variable:\n\n```powershell\n$env:SPT_DEBUG_RELEASE_SEED = \"<seed_hex>\"\n```\n\nPin each lab node to the debug channel with the printed public key:\n\n```powershell\ncargo run -p xtask -- debug-pin --key-id dev-debug-2026 --public-key <public_hex>\n```\n\nThis writes `$SPT_HOME/identity/release-keys.json` with the debug public key and\n`\"channel\": \"debug\"`. Removing the key or setting the channel back to\n`\"stable\"` removes the node from debug rollout eligibility.\n\n## Stage a rollout\n\nFrom the fast coordinator, stage an update set into the local release cache:\n\n```powershell\ncargo run -p xtask -- debug-rollout --build-current --artifact \"x86_64-unknown-linux-gnu=<path-to-linux-spt>\"\n```\n\nCommon flags:\n\n- `--build-current` builds and includes the coordinator's current platform.\n- `--artifact <target=path>` adds an already-built artifact for another target,\n  such as `x86_64-pc-windows-msvc` or `x86_64-unknown-linux-gnu`.\n- `--version <u64>` overrides the local debug sequence. Use a higher value when\n  recovering from lost local state.\n- `--stage-dir <path>` stages somewhere other than `$SPT_HOME/releases`.\n- `--state <path>` stores the local debug sequence somewhere other than\n  `target/debug-rollout-state.json`.\n\nThe staged update then propagates through the normal pull-based update pump.\nThere is intentionally no bespoke push path.\n\n## Apply and observe\n\nOn a default-gated node, apply is still explicit:\n\n```powershell\nspt update apply\n```\n\nNodes configured for full-auto update may apply after staging without a prompt.\nFor quick debugging, run or poke the daemon/update pump on lab nodes so they\nquery peers promptly.\n\n**Convergence watcher (M8-D4, decision 19).** Instead of hand-walking nodes,\nrun the maintainer watcher from the coordinator:\n\n```powershell\ncargo run -p xtask -- debug-converge --version <N> [--subnet <name>] [--timeout 120] [--poll 3]\n```\n\nIt polls every expected node (the coordinator's trust rows; `--nodes <hex,…>`\noverrides) over the status-only update query and prints a per-node table —\n`Applied` / `StagedAwaitingConsent` / `NotPinned` / `Rejected{reason}` /\n`Offline` — exiting `0` only when every node applied the target (`1`\notherwise, `2` on usage). Spec + state model: `docs/DEBUG-CONVERGE-PLAN.md`.\nThe manual walk below remains the fallback.\n\nUseful outcomes:\n\n- `NoArtifactForPlatform` means the signed set did not include the recipient's\n  Rust target triple.\n- `WrongChannel` means the node is not pinned to `debug`.\n- `Rollback` means the offered debug version is not greater than the node's\n  current debug-channel version.\n- `RefusedClass(BrokerBreaking)` means the candidate touches broker-held\n  resources; quiesce hosted endpoints first.\n\n## Agent checklist\n\n1. Read `CONTEXT.md` \"debug rollout\" and ADR-0016 before changing the flow.\n2. Keep debug tooling in `xtask` or other maintainer tooling, not the public\n   `spt` CLI/help surface.\n3. Keep verification on the production update substrate: signed metadata,\n   per-platform artifact digest, channel pinning, monotonic version, and\n   apply-time re-verification.\n4. Add or update traceability evidence for `REQ-UPD-6`.\n5. Run `cargo fmt`, focused update tests, and `traceable-reqs check`.\n"}],"details":{"displayContent":{"text":"# Debug rollout runbook\r\n\r\n<!-- [doc->REQ-UPD-6] -->\r\n\r\nDebug rollout is the maintainer-only fast path for testing a local spt-core\r\nbuild across a trusted lab subnet. It uses the normal signed self-update\r\nsubstrate: no raw peer file-copy, no production `spt` CLI surface, and no\r\nembedded debug trust anchor.\r\n\r\nUse this when debugging needs a local build to reach multiple lab nodes quickly.\r\nDo not use it for public releases; use `docs/RELEASE-RUNBOOK.md` for that.\r\n\r\n## Mental model\r\n\r\n- A rollout is a signed `SignedUpdateSet`: one signed metadata record, with one\r\n  artifact digest per Rust target triple.\r\n- Each recipient verifies the set under its node-local `release-keys.json`,\r\n  selects only its own platform artifact, stages it, and then follows the normal\r\n  consent/apply policy.\r\n- Debug and stable are separate channels. A debug-pinned node accepts only\r\n  `channel = \"debug\"` offers; a stable-pinned node accepts only stable offers.\r\n- Debug versions are monotonic within the debug channel. To recover from a bad\r\n  debug build, publish a higher debug version, even if the bytes are a previous\r\n  known-good binary.\r\n- A broker-touching candidate may be staged, but apply refuses it while\r\n  broker-held runtime is live. Shut down or suspend hosted endpoints first; the\r\n  first flow does not auto-cycle them.\r\n\r\n## One-time lab setup\r\n\r\nGenerate a debug key on the coordinator:\r\n\r\n```powershell\r\ncargo run -p xtask -- debug-keygen dev-debug-2026\r\n```\r\n\r\nSet the printed seed as a long-lived coordinator-local environment variable:\r\n\r\n```powershell\r\n$env:SPT_DEBUG_RELEASE_SEED = \"<seed_hex>\"\r\n```\r\n\r\nPin each lab node to the debug channel with the printed public key:\r\n\r\n```powershell\r\ncargo run -p xtask -- debug-pin --key-id dev-debug-2026 --public-key <public_hex>\r\n```\r\n\r\nThis writes `$SPT_HOME/identity/release-keys.json` with the debug public key and\r\n`\"channel\": \"debug\"`. Removing the key or setting the channel back to\r\n`\"stable\"` removes the node from debug rollout eligibility.\r\n\r\n## Stage a rollout\r\n\r\nFrom the fast coordinator, stage an update set into the local release cache:\r\n\r\n```powershell\r\ncargo run -p xtask -- debug-rollout --build-current --artifact \"x86_64-unknown-linux-gnu=<path-to-linux-spt>\"\r\n```\r\n\r\nCommon flags:\r\n\r\n- `--build-current` builds and includes the coordinator's current platform.\r\n- `--artifact <target=path>` adds an already-built artifact for another target,\r\n  such as `x86_64-pc-windows-msvc` or `x86_64-unknown-linux-gnu`.\r\n- `--version <u64>` overrides the local debug sequence. Use a higher value when\r\n  recovering from lost local state.\r\n- `--stage-dir <path>` stages somewhere other than `$SPT_HOME/releases`.\r\n- `--state <path>` stores the local debug sequence somewhere other than\r\n  `target/debug-rollout-state.json`.\r\n\r\nThe staged update then propagates through the normal pull-based update pump.\r\nThere is intentionally no bespoke push path.\r\n\r\n## Apply and observe\r\n\r\nOn a default-gated node, apply is still explicit:\r\n\r\n```powershell\r\nspt update apply\r\n```\r\n\r\nNodes configured for full-auto update may apply after staging without a prompt.\r\nFor quick debugging, run or poke the daemon/update pump on lab nodes so they\r\nquery peers promptly.\r\n\r\n**Convergence watcher (M8-D4, decision 19).** Instead of hand-walking nodes,\r\nrun the maintainer watcher from the coordinator:\r\n\r\n```powershell\r\ncargo run -p xtask -- debug-converge --version <N> [--subnet <name>] [--timeout 120] [--poll 3]\r\n```\r\n\r\nIt polls every expected node (the coordinator's trust rows; `--nodes <hex,…>`\r\noverrides) over the status-only update query and prints a per-node table —\r\n`Applied` / `StagedAwaitingConsent` / `NotPinned` / `Rejected{reason}` /\r\n`Offline` — exiting `0` only when every node applied the target (`1`\r\notherwise, `2` on usage). Spec + state model: `docs/DEBUG-CONVERGE-PLAN.md`.\r\nThe manual walk below remains the fallback.\r\n\r\nUseful outcomes:\r\n\r\n- `NoArtifactForPlatform` means the signed set did not include the recipient's\r\n  Rust target triple.\r\n- `WrongChannel` means the node is not pinned to `debug`.\r\n- `Rollback` means the offered debug version is not greater than the node's\r\n  current debug-channel version.\r\n- `RefusedClass(BrokerBreaking)` means the candidate touches broker-held\r\n  resources; quiesce hosted endpoints first.\r\n\r\n## Agent checklist\r\n\r\n1. Read `CONTEXT.md` \"debug rollout\" and ADR-0016 before changing the flow.\r\n2. Keep debug tooling in `xtask` or other maintainer tooling, not the public\r\n   `spt` CLI/help surface.\r\n3. Keep verification on the production update substrate: signed metadata,\r\n   per-platform artifact digest, channel pinning, monotonic version, and\r\n   apply-time re-verification.\r\n4. Add or update traceability evidence for `REQ-UPD-6`.\r\n5. Run `cargo fmt`, focused update tests, and `traceable-reqs check`.\r\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,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\DEBUG-ROLLOUT.md"}}},"isError":false,"timestamp":1784195752958}}
{"type":"message","id":"7e0e41a8","parentId":"14c42d22","timestamp":"2026-07-16T09:55:52.959Z","message":{"role":"toolResult","toolCallId":"call_8VY2CDrB9Q7BvXnlj91ab73T|fc_008390f508d1d09b016a58aaa8e47c819a8250f378bb6801bb","toolName":"read","content":[{"type":"text","text":"# Live activity buffer (PTY digest)\n\n## Status\n\naccepted (2026-06-01) · **source mechanism superseded by the 2026-06-12 amendment below** (product surface stands) · **implementation reconciled onto the log source in M9 (2026-06-13)** — the PTY-parse engine + `pty_digest` seam are retired; the digest is an on-demand projection of normalized session-log records · **source mechanism FULLY superseded by [ADR-0019](0019-digest-adapter-declared-extractor-session-spanning.md) (2026-06-13)** — the M9 \"no manifest seam / rides `[history]`\" stance is reversed: the digest gets its own adapter-declared `[digest]` extractor, thread-spanning, and a two-origin (activity + injected-context) merge. Only this ADR's product-surface framing survives (broadened from \"recent activity\" to \"recent context\").\n\n## Context\n\nWe want a rolling, human-glanceable \"what is this agent doing right now\" view — the last ~N user turns + agent output between, with tool-usage sprints collapsed to a single `used: …` line — that a Shell, CLI, or GUI pane can render. The obvious implementation (spt-core parses the live PTY byte stream) looks like it **contradicts the history-subsystem decision** (CONTEXT §history subsystem), which deliberately refused to build per-harness parsers (\"unbounded, version-fragile maintenance burden\") and pushed parsing into adapters (Path A) or a native store the adapter writes (Path B). A future reader would reasonably flag the apparent reversal. This ADR records why it is *not* one, and the shape chosen.\n\n## Decision\n\nAdd 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\nThe parsing-boundary distinction that resolves the tension:\n- **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 opaque command templates) — **not** a built-in per-harness parser. The rejected maintenance burden stays in the adapter.\n- **It parses the live PTY byte stream, not the harness transcript.** A light rolling regex over bytes the broker already holds is a categorically different artifact from locating + parsing version-fragile harness transcript files (the thing Path A/B exist to avoid).\n\nShape:\n- **Broker-PTY-dependent → spt-hosted-primary.** Needs the broker-owned PTY (topology 2). **Harness-hosted (topology 1) is capability-gated:** an adapter that can feed spt-core the stream (e.g. a `pi`-style harness) opts in via a declared capability; harnesses that own their process tree opaquely (Claude Code) cannot, and the digest is simply unavailable there.\n- **Structured, source-tagged output** (input / agent / collapsed-tool entries) so consumers render richly. Window depth N (~3) is a config knob; tool-arg truncation (~25 chars) is spt-core-controlled *presentation* (the template controls *extraction*).\n- **Two access modes over one transport.** The digest is a parsed projection of the same PTY stream the broker streams for R-TERM-3, so: **snapshot pull** (`spt digest <[subnet:]id[@node]>`) and a **broker-pushed structured-delta stream** (subscribe; pushes only changes for near-realtime reflection). The delta-stream is a structured overlay riding the **same R-TERM-3 byte-stream substrate** (raw vs parsed = two channels on the one session surface). Access is **address-gated** (same as messaging; respects ADR-0006 visibility).\n- **Optional \"Option (C)\" persistence:** opt-in per-endpoint, append digests to the native history store (Path B) as a coarse activity log — a third logging surface, off by default, not a replacement for A/B.\n\n## Consequences\n\n- New v1 requirement to register: the live activity buffer + the `input_pattern`/`agent_pattern`/`tool_pattern[]` manifest seam + `spt digest` (pull) + the digest delta-stream (push). Lands with **M3** (needs the spt-term broker + PTY); Shell consumption is M5; the GUI pane is the frontend milestone; persistence is M3+.\n- The manifest schema (`docs/MANIFEST.md`) gains the `pty_digest` seam; the capability-declaration seam gains the harness-hosted stream-feed opt-in.\n- Does **not** reverse the history-subsystem decision — the no-built-in-parser rule still holds; the digest honors it by running adapter-supplied patterns over the live byte stream rather than parsing harness transcripts.\n- Builds on R-TERM-1/2/3 (broker, session surface, byte-stream) and ADR-0006 (address-gating).\n\n## Amendment — source mechanism superseded: normalized session logs, never PTY parsing (2026-06-12)\n\n**What changes:** the digest's *source*. The original decision parsed the live PTY byte stream; the digest is now a **rolling tail-projection of the endpoint's normalized session logs** — the same normalized history the echo-commune consumes (history subsystem Path A locate+normalize / Path B native store). One pipeline, two consumers. Harnesses with no usable logs push entries directly via a new **`api digest-entry`** verb.\n\n**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 parsing carries **zero new burden** because Paths A/B already pay for it — the digest just consumes their output. The governing split: **render surfaces read the PTY** (R-TERM raw byte ring, unchanged); **content surfaces read the logs**.\n\n**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\n**Improved consequence:** the digest becomes **topology-independent** — harness-hosted endpoints (Claude Code's per-session JSONL is Path A's worked example) get digests too; the original \"unavailable under opaque harnesses\" limitation dissolves. Freshness = file-watch + adapter hook nudges; a near-realtime **incremental** normalize story is design-phase work for the implementing milestone.\n\n**Implementation debt recorded:** `spt-daemon::digest` (`DigestEngine` + the manifest `pty_digest` pattern seam) implements the superseded shape; reconciliation + the `docs/MANIFEST.md` schema revision land with the implementing milestone.\n\n**Debt discharged (M9, 2026-06-13):** the PTY-parse engine (`spt-term`'s `DigestParser` + `spt-daemon`'s byte-feed `DigestEngine`) and the `[pty_digest]` manifest seam are **retired**. The digest is now an **on-demand projection** of the endpoint's normalized session-log records: `spt-term::projection` folds the published **digest-record contract** (the spt-core-owned `{role, text, tool}` overlay an adapter targets in its `[history]` lines, or pushes via `api digest-entry`) into the same structured `Digest`; `spt-daemon::digest::project_endpoint_digest` projects on demand at the snapshot-pull / reproject handlers and `DigestHub::project_and_publish` drives the delta-stream. Topology-independent (a Claude-Code-hosted endpoint's Path A JSONL projects with no b…\n\n\n[Some lines truncated to 768 chars]"}],"details":{"displayContent":{"text":"# Live activity buffer (PTY digest)\r\n\r\n## Status\r\n\r\naccepted (2026-06-01) · **source mechanism superseded by the 2026-06-12 amendment below** (product surface stands) · **implementation reconciled onto the log source in M9 (2026-06-13)** — the PTY-parse engine + `pty_digest` seam are retired; the digest is an on-demand projection of normalized session-log records · **source mechanism FULLY superseded by [ADR-0019](0019-digest-adapter-declared-extractor-session-spanning.md) (2026-06-13)** — the M9 \"no manifest seam / rides `[history]`\" stance is reversed: the digest gets its own adapter-declared `[digest]` extractor, thread-spanning, and a two-origin (activity + injected-context) merge. Only this ADR's product-surface framing survives (broadened from \"recent activity\" to \"recent context\").\r\n\r\n## Context\r\n\r\nWe want a rolling, human-glanceable \"what is this agent doing right now\" view — the last ~N user turns + agent output between, with tool-usage sprints collapsed to a single `used: …` line — that a Shell, CLI, or GUI pane can render. The obvious implementation (spt-core parses the live PTY byte stream) looks like it **contradicts the history-subsystem decision** (CONTEXT §history subsystem), which deliberately refused to build per-harness parsers (\"unbounded, version-fragile maintenance burden\") and pushed parsing into adapters (Path A) or a native store the adapter writes (Path B). A future reader would reasonably flag the apparent reversal. This ADR records why it is *not* one, and the shape chosen.\r\n\r\n## Decision\r\n\r\nAdd 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.\r\n\r\nThe parsing-boundary distinction that resolves the tension:\r\n- **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 opaque command templates) — **not** a built-in per-harness parser. The rejected maintenance burden stays in the adapter.\r\n- **It parses the live PTY byte stream, not the harness transcript.** A light rolling regex over bytes the broker already holds is a categorically different artifact from locating + parsing version-fragile harness transcript files (the thing Path A/B exist to avoid).\r\n\r\nShape:\r\n- **Broker-PTY-dependent → spt-hosted-primary.** Needs the broker-owned PTY (topology 2). **Harness-hosted (topology 1) is capability-gated:** an adapter that can feed spt-core the stream (e.g. a `pi`-style harness) opts in via a declared capability; harnesses that own their process tree opaquely (Claude Code) cannot, and the digest is simply unavailable there.\r\n- **Structured, source-tagged output** (input / agent / collapsed-tool entries) so consumers render richly. Window depth N (~3) is a config knob; tool-arg truncation (~25 chars) is spt-core-controlled *presentation* (the template controls *extraction*).\r\n- **Two access modes over one transport.** The digest is a parsed projection of the same PTY stream the broker streams for R-TERM-3, so: **snapshot pull** (`spt digest <[subnet:]id[@node]>`) and a **broker-pushed structured-delta stream** (subscribe; pushes only changes for near-realtime reflection). The delta-stream is a structured overlay riding the **same R-TERM-3 byte-stream substrate** (raw vs parsed = two channels on the one session surface). Access is **address-gated** (same as messaging; respects ADR-0006 visibility).\r\n- **Optional \"Option (C)\" persistence:** opt-in per-endpoint, append digests to the native history store (Path B) as a coarse activity log — a third logging surface, off by default, not a replacement for A/B.\r\n\r\n## Consequences\r\n\r\n- New v1 requirement to register: the live activity buffer + the `input_pattern`/`agent_pattern`/`tool_pattern[]` manifest seam + `spt digest` (pull) + the digest delta-stream (push). Lands with **M3** (needs the spt-term broker + PTY); Shell consumption is M5; the GUI pane is the frontend milestone; persistence is M3+.\r\n- The manifest schema (`docs/MANIFEST.md`) gains the `pty_digest` seam; the capability-declaration seam gains the harness-hosted stream-feed opt-in.\r\n- Does **not** reverse the history-subsystem decision — the no-built-in-parser rule still holds; the digest honors it by running adapter-supplied patterns over the live byte stream rather than parsing harness transcripts.\r\n- Builds on R-TERM-1/2/3 (broker, session surface, byte-stream) and ADR-0006 (address-gating).\r\n\r\n## Amendment — source mechanism superseded: normalized session logs, never PTY parsing (2026-06-12)\r\n\r\n**What changes:** the digest's *source*. The original decision parsed the live PTY byte stream; the digest is now a **rolling tail-projection of the endpoint's normalized session logs** — the same normalized history the echo-commune consumes (history subsystem Path A locate+normalize / Path B native store). One pipeline, two consumers. Harnesses with no usable logs push entries directly via a new **`api digest-entry`** verb.\r\n\r\n**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 parsing carries **zero new burden** because Paths A/B already pay for it — the digest just consumes their output. The governing split: **render surfaces read the PTY** (R-TERM raw byte ring, unchanged); **content surfaces read the logs**.\r\n\r\n**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).\r\n\r\n**Improved consequence:** the digest becomes **topology-independent** — harness-hosted endpoints (Claude Code's per-session JSONL is Path A's worked example) get digests too; the original \"unavailable under opaque harnesses\" limitation dissolves. Freshness = file-watch + adapter hook nudges; a near-realtime **incremental** normalize story is design-phase work for the implementing milestone.\r\n\r\n**Implementation debt recorded:** `spt-daemon::digest` (`DigestEngine` + the manifest `pty_digest` pattern seam) implements the superseded shape; reconciliation + the `docs/MANIFEST.md` schema revision land with the implementing milestone.\r\n\r\n**Debt discharged (M9, 2026-06-13):** the PTY-parse engine (`spt-term`'s `DigestParser` + `spt-daemon`'s byte-feed `DigestEngine`) and the `[pty_digest]` manifest seam are **retired**. The digest is now an **on-demand projection** of the endpoint's normalized session-log records: `spt-term::projection` folds the published **digest-record contract** (the spt-core-owned `{role, text, tool}` overlay an adapter targets in its `[history]` lines, or pushes via `api digest-entry`) into the same structured `Digest`; `spt-daemon::digest::project_endpoint_digest` projects on demand at the snapshot-pull / reproject handlers and `DigestHub::project_and_publish` drives the delta-stream. Topology-independent (a Claude-Code-hosted endpoint's Path A JSONL projects with no b…\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\adr\\0008-live-activity-buffer-pty-digest.md"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195752959}}
{"type":"message","id":"45a3394f","parentId":"7e0e41a8","timestamp":"2026-07-16T09:55:52.959Z","message":{"role":"toolResult","toolCallId":"call_cXbNJeHSL2BSYlABw8AL5QuH|fc_008390f508d1d09b016a58aaa8e478819a83aca8f4bf8238d4","toolName":"read","content":[{"type":"text","text":"# Restore broker/brain process isolation (correct the in-process-collapse regression)\n\n## Status\n\nAccepted (2026-06-09) — **extends and amends ADR-0004.** The design was independently verified (agent `doyle`, verified-with-amendments); implementation is a dedicated future milestone, sequenced next (before `spt-claude-code`). Full rationale, the `file:line` audit, and the per-decision alternatives live in `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.\n\n## Context\n\nADR-0004 decided the broker/brain split **as two processes** — a stable broker kernel (PTY masters, harness children, sockets, the Iroh/QUIC endpoint) beneath a freely-restartable brain — specifically to satisfy the no-endpoint-drop self-update invariant (REQ-UPD-3): a routine (brain-only) update restarts the brain while the broker process survives, so no hosted endpoint terminates. The spikes (01/03/04/05/06) proved this with two separate binaries.\n\nOn 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 was made).\n\nConsequences of the drift:\n- 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- `spt update apply` performs an in-process `Brain::handoff` that re-attaches a subscriber within the *same old process* — a no-op for a binary swap. `update.rs:233-234`'s \"the live daemon execs the verified new binary\" is aspirational and was never wired.\n- New code does not run until an unrelated restart/logon. Observed live: `enlyzeam` ran 0.3.0 with the valid 0.3.2 binary on disk for ~a day, continuing to reproduce the `\\r`-corruption bug the update was meant to fix.\n- **REQ-DAEMON-2 and REQ-UPD-3 carry `int` evidence that proves only the in-process handoff shape** (`tests/update.rs`, `brain_swap.rs`, the M3b-B9 daemon E2E) — i.e. the regression is masked in the requirement registry: the tests pass while proving the wrong thing.\n\n## Decision\n\n**Restore the ADR-0004 two-process model.** The brain runs new code by restarting onto the swapped binary while the broker process — holding every continuity-bearing resource — survives. The design decisions (grill Q2–Q8, hardened by six verification amendments `[V1]`–`[V6]`):\n\n1. **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.\n2. **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-spawn `std::env::current_exe()` (`readlink(/proc/self/exe)`, inode-tracking) then **follows the rename to `.old-N`**, so the broker respawned the brain onto the OLD bytes while recording `applied` (caught live on kitsubito, v0.4.1 roll). Fix: capture the canonical ex…\n3. **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]`** Only phase-significant loops convert — the idempotent pump cadences need none.\n4. **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 is new-brain × old-broker, so a CI-real old-broker × new-brain compat test across the verb surface is required (else KH-2.3 returns).\n5. **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).\n6. **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 state before ready-promotion (pre-ready writes stay N-1-readable), or auto-rollback silently breaks on the first schema migration.\n7. **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.\n8. **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\n## Consequences\n\n- **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- **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- **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- **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\n\n[Some lines truncated to 768 chars]"}],"details":{"displayContent":{"text":"# Restore broker/brain process isolation (correct the in-process-collapse regression)\r\n\r\n## Status\r\n\r\nAccepted (2026-06-09) — **extends and amends ADR-0004.** The design was independently verified (agent `doyle`, verified-with-amendments); implementation is a dedicated future milestone, sequenced next (before `spt-claude-code`). Full rationale, the `file:line` audit, and the per-decision alternatives live in `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.\r\n\r\n## Context\r\n\r\nADR-0004 decided the broker/brain split **as two processes** — a stable broker kernel (PTY masters, harness children, sockets, the Iroh/QUIC endpoint) beneath a freely-restartable brain — specifically to satisfy the no-endpoint-drop self-update invariant (REQ-UPD-3): a routine (brain-only) update restarts the brain while the broker process survives, so no hosted endpoint terminates. The spikes (01/03/04/05/06) proved this with two separate binaries.\r\n\r\nOn 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 was made).\r\n\r\nConsequences of the drift:\r\n- 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.**\r\n- `spt update apply` performs an in-process `Brain::handoff` that re-attaches a subscriber within the *same old process* — a no-op for a binary swap. `update.rs:233-234`'s \"the live daemon execs the verified new binary\" is aspirational and was never wired.\r\n- New code does not run until an unrelated restart/logon. Observed live: `enlyzeam` ran 0.3.0 with the valid 0.3.2 binary on disk for ~a day, continuing to reproduce the `\\r`-corruption bug the update was meant to fix.\r\n- **REQ-DAEMON-2 and REQ-UPD-3 carry `int` evidence that proves only the in-process handoff shape** (`tests/update.rs`, `brain_swap.rs`, the M3b-B9 daemon E2E) — i.e. the regression is masked in the requirement registry: the tests pass while proving the wrong thing.\r\n\r\n## Decision\r\n\r\n**Restore the ADR-0004 two-process model.** The brain runs new code by restarting onto the swapped binary while the broker process — holding every continuity-bearing resource — survives. The design decisions (grill Q2–Q8, hardened by six verification amendments `[V1]`–`[V6]`):\r\n\r\n1. **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.\r\n2. **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-spawn `std::env::current_exe()` (`readlink(/proc/self/exe)`, inode-tracking) then **follows the rename to `.old-N`**, so the broker respawned the brain onto the OLD bytes while recording `applied` (caught live on kitsubito, v0.4.1 roll). Fix: capture the canonical ex…\n3. **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]`** Only phase-significant loops convert — the idempotent pump cadences need none.\r\n4. **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 is new-brain × old-broker, so a CI-real old-broker × new-brain compat test across the verb surface is required (else KH-2.3 returns).\r\n5. **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).\r\n6. **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 state before ready-promotion (pre-ready writes stay N-1-readable), or auto-rollback silently breaks on the first schema migration.\r\n7. **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.\r\n8. **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.\r\n\r\n## Consequences\r\n\r\n- **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.\r\n- **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.\r\n- **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.\r\n- **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).\r\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\adr\\0018-broker-brain-process-isolation-restoration.md"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195752959}}
{"type":"message","id":"61d72253","parentId":"45a3394f","timestamp":"2026-07-16T09:55:52.997Z","message":{"role":"toolResult","toolCallId":"call_3IKqHIktj2J8HWbHyGeemMdx|fc_008390f508d1d09b016a58aaa8e45c819ab59544c8e9a5558d","toolName":"read","content":[{"type":"text","text":"# spt-core\n\n**Platform scope:** Windows + Linux for v1. macOS is out (no test machine available) but kept structurally easy — `portable-pty` and Iroh both support it, so macOS is a later test/CI-budget decision, not a re-architecture.\n\n**Legacy migration:** it should be possible — ideally *automatic* — for a user to migrate an existing `claude_skill_owl` (modern SPT) install to spt-core (identity, agents, tracked Psyche context). Exact mechanism deferred to design; the commitment is that migration is a first-class supported path, not a manual rebuild.\n\nHarness-independent core for the SPT ecosystem. Provides inter-agent messaging, live-agent lifecycle, terminal wrapping, self-update, and networking primitives — as both a Rust library workspace and a canonical reference binary. Designed so any agent runtime (Claude Code, Codex, Cursor, headless, future harnesses) can interface with the SPT ecosystem either by shelling out to the binary or by linking the crates directly.\n\nSuccessor to `claude_skill_owl` (today's \"modern SPT\"), which is being rebuilt as `spt-core` to untether the system from Claude Code and lift it to a general-purpose agent-ecosystem core.\n\n## Language\n\n**spt-core**:\nThe system. Canonical name. The Rust workspace and the umbrella project.\n\n**spt.exe / spt** (canonical binary):\nThe reference binary built from the workspace. Replaces today's `owl.exe`. Most external integrations (plugins, hooks, scripts in other harnesses) interact with spt-core *only* through this binary — fire-and-forget subcommands, long-running listeners under a parent harness's process supervisor, etc. Unix builds use the same name without `.exe`.\n\n**library workspace**:\nThe set of Rust crates that compose spt-core. Consumers that want a deeper integration than shelling out to `spt.exe` link these crates directly. The reference binary is itself a consumer of the workspace. The expected non-binary consumers are future first-party services that link Rust directly.\n\n**spt plugin** (separate downstream project — NOT an spt-core deliverable):\nA rebuilt version of today's Claude Code `spt` plugin. It is the **first consumer** built *atop* spt-core and the **acceptance proof** of spt-core v1 (it reaches feature parity with modern SPT while delegating all core functionality to spt-core, primarily via `spt.exe`, with deeper hooks where useful) — but it **lives and builds in its own repository, outside spt-core**. It is a Claude-Code-specific *adapter*: it holds the Claude Code conventions (hooks, slash-commands, skill/plugin layout, `claude` session-invocation). **spt-core itself contains zero Claude Code conventions** — only the harness-agnostic contract the plugin binds to. The only adapter-shaped artifact ever in this repo is a generic mock/test adapter exercising the manifest + `api` contract (PR…\n\n**Pi** (disambiguation — two meanings, never conflate):\n(1) **Pi, the coding agent/harness** (`badlogic/pi-mono`) — a harness example alongside Claude Code and Codex; this is the meaning in user-facing harness lists. (2) **Pi-class node** — Raspberry-Pi-class low-power hardware hosting a Shell-only or headless SPT node; an incidental hardware descriptor, never an explicit product example. Public-facing docs must disambiguate or avoid the bare word.\n_Avoid_: bare \"Pi node\" when the harness is meant.\n\n**spt-daemon** (per-machine supervisor):\nThe single always-on, one-per-machine logical supervisor. Owns the PTYs for all hosted sessions, the node's network identity + WAN endpoint, the subnet registry, all spools, **all poll-listener logic, and all Psyche/pulse loops** — everything is consolidated here (no separate poll-listener or Psyche-wrapper processes; listeners already touch sessions directly under capsule/idle, and Psyche wrappers already invoke harness binaries directly, so they belong in the one supervisor). Collapses what the sister project planned as a separate `spt-node` daemon into one process — see Networking. The `spt-node` separate-deliverable concept is retired.\n\nInternally the logical daemon is split into two implementation layers for seamless self-update (see Self-update):\n- **broker** (stable \"kernel\") — holds *only* the un-transferable, must-not-die resources: PTY master fds, the spawned harness child processes, and listening network sockets. Minimal, dumb, versioned local IPC. Almost never updates.\n- **daemon brain** (\"userspace\") — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk state and re-attaches to the broker's held handles.\n\nLogical addressing is unchanged — still one per-machine `spt-daemon`; the broker is an internal layer, not separately addressable. There is exactly **one broker per machine** (per `SPT_HOME`) — *not* one per endpoint: a single broker holds every hosted endpoint's resources, and it is present whenever the daemon runs, even with zero endpoints online (the bare-daemon case). It is therefore the always-present per-machine layer, which is why the single-daemon lock + liveness anchor belong to it.\n\n**in-session relay**:\nA thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle delivery into an spt-hosted PTY goes through an opt-in adapter **translation binary** (`[message-idle-translation-binary]`, ADR-0022): a pure stdin→stdout filter spt-core lifecycle-manages — it reads the `<EVENT>` feed on stdin, emits keystroke-commands (`{key}`/`{…\n\n### Deliverable shape\n\nspt-core ships **both** a library workspace and a canonical binary:\n\n- **Library crates** — the deeper integration path. Used by future first-party services that link Rust directly.\n- **`spt.exe` / `spt`** — the canonical binary, built from the workspace. The primary integration path for harness plugins and external tooling, which mostly fire it as a subprocess at various surfaces (one-shot commands, poll listeners under a Monitor-tool-equivalent, hook tap-ins).\n\nBoth surfaces are first-class. Wire-protocol parity between them is a versioning concern from day one (a non-Rust client speaking to `spt.exe` and a Rust client linking the crates must see the same observable behavior).\n\n## Runtime model\n\nspt-core is harness-independent: it does not know about Claude Code, Codex, Cursor, or any other agent runtime. All harness-specific surfaces (how to invoke an agent session, fetch conversation history for an echo commune, detect activity/idleness, etc.) are abstracted behind a runtime layer that consumers supply.\n\n**AgentRuntime** (Rust trait, implementation detail):\nThe internal Rust abstraction over a harness. Anything spt-core needs to do *to* or *with* an agent goes through this trait. Most consumers never see it directly — they configure spt-core via a manifest, and spt-core's default `ManifestRuntime` implementation executes against the manifest.\n\n**harness contract** (umbrella term):\nThe full surface a harness binds to in order to participate in the spt-core ecosystem. Has two equally-important halves: the **runtime manifest** (outbound — how spt-core drives the harness) and the **subcommand surface** (inbound — how the harness reports events back to spt-core). A harness implementation is one TOML/YAML manifest + a binding from the harness's own hook system into `spt.exe <subcommand>` calls.\n\n**runtime manifest** (outbound half of the harness contract):\nA declarative configuration file (TOML/YAML — schema TBD) that tells spt-core how to drive a specific harness. Declares: how to invoke an agent session, how to look up conversation history for an echo commune, how to spawn/resume a Psyche-equivalent, which binary or command implements each harness-side operation, and which endpoint types this harness supports. spt-core is the actor for each of these; the manifest tells it what to do.\n\nExample shape (illustrative): `spt.exe --manifest spt-plugin.toml live start <id>`. A harness like the planned spt plugin wraps this invocation into the `$LIVE` / `$OWL` environment variables it injects into its sessions, so harness-internal callers continue to invoke `$LIVE` / `$OWL` unchanged.\n\n**subcommand surface** (inbound half of the harness contract):\nThe stable set of `spt.exe <subcommand>` entry points that harnesses bind their own hook systems to. When the harness's runtime emits an event (subagent started, tool just invoked, user typed `/clear`, session crashed), the harness's hook fires a short-lived `spt.exe <subcommand>` invocation that mutates on-disk SPT state (perch registry, spool, etc.). spt-core publishes this surface; harnesses author the bindings.\n\n**Naming convention:** these inbound, machinery-facing commands are prefixed **`api `/`api-`** (e.g. `spt api bind`, `spt api state`) to distinguish them from the agent-facing verbs an agent invokes directly (`send`, `ring`, `ready`, …). The `api` namespace is the harness/adapter commands-API; the unprefixed namespace is the agent surface.\n\nTogether: manifest + subcommand surface = the complete harness API. A sidecar-style long-running adapter process speaking a wire protocol is explicitly **deferred** as a possible v2 alternative for harnesses that outgrow the manifest+hooks shape (e.g. need streaming or in-memory state across events). Not built day-one.\n\n**adapter manifest header** (`adapter_name` + version compat):\nEvery manifest declares a unified **`adapter_name`** (e.g. `claude-spt`), carried on every `api` invocation too. It is load-bearing: one daemon hosts endpoints from multiple adapters, so it resolves an endpoint's manifest + seams by `adapter_name`; adapter-update ripples target by it; capability/manifest lookup and telemetry key on it. The header also declares the adapter's own version and a **`min_spt_core_version`** — the minimum spt-core the adapter requires. This declaration must be **readable before an adapter update is applied** (it lives in the manifest header / a small metadata file fetched first), so spt-core can verify compatibility / expected supported features *before* committing the update. If installed spt-core < the adapter's `min_spt_core_ver…\n\n<!-- [doc->REQ-MANIFEST-2] -->\n**adapter profile** (ratified 2026-06-11, Gateway grill; future spt-core milestone — first beneficiaries `spt-claude-code` and the usbip shell):\nA named **sparse overlay** on its parent adapter manifest. Merge semantics are **leaf-replace**: a profile key replaces the whole value at that path (arrays included — never spliced or appended). The merged result is a complete manifest, and the profile behaves as a distinct adapter option everywhere: canonical addressing is the composite **`<adapter>:<profile>`** (`claude-spt:work`, `spt-usbip-driver:hid-only`) in every place a bare `adapter_name` rides today (perch `info.json`, capability resolution, `api` invocations, `spt adapter list`); the bare name = the parent unmodified. **Two sources, one semantics:** a **shipped profile** is declared inside the parent manifest by the adapter dev and updates as one unit with it; a **local profile** is a node-local …\n_Avoid_: \"manifest fork\", \"child adapter\", per-profile versioning.\n\n**adapter strings** (ratified 2026-06-11, Gateway grill):\n<!-- [doc->REQ-MANIFEST-3] -->\nA `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** p…\n<!-- [doc->REQ-MANIFEST-5] -->\n**File-backed strings** (M12-W3): a `[strings]` value MAY be a **file pointer** instead of an inline literal — a value-position table with **exactly one** key `file`: `skill = { file = \"skill.md\" }`. `get-string` resolves it to the file's **contents** (so large bodies — skill-instructions, hint text — stay out of the manifest). The exactly-one-key rule is the disambiguation: any other table shape stays an opaque nested strings tree (existing trees untouched), and `{ file = … }` is reserved as the pointer form (it can't double as inline data). Files live in the adapter's per-adapter aux dir **`adapters/<adapter>/strings/`** (sibling of `profiles/`), referenced by a path relative to it that **must stay inside that dir** (HAZARD-class containment: `..` traversa…\n_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); \"adapter KV store\" as a separate registry; putting user files in the adapter-shipped `strings/` dir (clobbered by updates — use a local profile).\n\n**manifest substitution in `[strings]`** (ratified 2026-06-25, v0.16.0 update-arc grill):\n`get-string` resolves a set of **adapter-static** substitution keys inside a returned string value at **read time** (lazily, like file-backed strings): **`{adapter_dir}`** — the registry record's precise `source_dir` (the install dir; survives updates; the same dir bare-program resolution already uses) — and **`{adapter_name}`**. Session-scoped keys (`{id}`/`{session_id}`/…) are **not** available: `get-string` carries no session context today, and a `get-string --session-id` for session-scoped substitution is a deferred, larger change. The load-bearing invariant is preserved: **spt-core still never executes a string** — it substitutes and returns; the *adapter's own wrapper* executes the result. Canonical use: a harness hook dispatcher resolves its own packe…\n_Avoid_: session-scoped substitution through bare `get-string`; reading this as spt-core executing a string (it never does — the adapter wrapper executes the resolved value).\n\n**keyword hints** (ratified 2026-06-12 — core milestone A):\n<!-- [doc->REQ-MANIFEST-4] -->\nOnce-per-session usage/syntax hints, a first-class adapter feature: the manifest's `[hints]` section declares entries of `{keywords (literal default, regex opt-in), text}`; the adapter's user-prompt hook pipes the **full user message** to `spt api hint --session <id>` (stdin) and receives matched hint lines (`keyword hint for SPT adapter <name>: \"<kw>\"-->{text}`) for its context-injection channel. The daemon keeps a per-session seen-set — each hint fires **once per session** (a `/clear` mints a new session, naturally re-arming) — and emits at most **one hint per message**. **Tiebreak when a message matches multiple hints:** scan in declaration order and emit the FIRST match whose hint this session has not yet seen; if the declaration-order-first matching hin…\n_Avoid_: unconditional static context (that's the adapter's own preamble); firing per-message.\n\n**adapter update declaration** (manifest field):\n<!-- [doc->REQ-UPD-9] -->\nEach adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = \"user/repo\"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed …\n\n**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):\n<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->\nA `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS…\n<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->\nAn optional **`[update].message`** (avenue-agnostic) is a plain multi-line operator notice surfaced to stdout, markdown-rendered (the helpfmt prose path), **only when an update is actually applied** (the version changed) — never on a no-op. It is read from the newly-installed manifest with no `{key}` substitution; its use is to announce a post-update action (e.g. \"run `/reload-plugins` in any ongoing sessions\").\n\n**composite update — `[update.post]`** (ratified 2026-06-25, v0.16.0 update-arc grill; ADR-0029):\nAn optional **avenue-agnostic** post-step `{ command, self_verifies }` spt-core runs **after** the primary avenue resolves — in the same `spt adapter update` **and at `spt adapter add`** (install is the first update, so a fresh install conducts the post-step too; bug-#1 operator ruling, v0.19.0 — the eager-extract acquisition runs it post-registration, a delegated acquisition after the acquisition succeeds, and only the payload-less `file_pull` PENDING add defers it to the payload's arrival) — so one lever pulls the adapter `.spt` (`gh_release`) **and** runs a delegated reconcile (e.g. an adapter's `claude plugin update` cross-platform binary). It runs **foreground and bounded** (the subprocess-timeout hazard bound, 120s; never backgrounded — when the CLI re…\n\n**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.\n_Avoid_: calling the Psyche loop or an on-demand extractor a \"resident\" binary; \"restart the endpoint\" for what is a per-binary cycle.\n\n**session-invocation declaration** (manifest field, noted for spt-plugin parity):\nHow the harness spawns agent sessions, including Psyche and echo-commune sessions. For the rebuilt spt-plugin, Psyche and echo communes must migrate **off `claude -p`** (imminent Claude Code billing changes) to headless `claude` sessions (`--resume` for the Psyche). This is an adapter/manifest concern, not a core concern, but the parity milestone must carry it.\n\n### Manifest seams (outbound contract, detailed)\n\nGoverning principle: **SPT is not a harness.** Model choice, billing shape, harness-internal env, and harness-internal context are entirely the adapter's concern, expressed inside the adapter's own command templates. spt-core owns only the template *mechanism* (substitution keys), the substitution *values* it is responsible for, and the surrounding lifecycle. Env for the *endpoint binary itself* is auto-handled by spt-core/broker; env for the *agent running inside* that binary is the adapter's config (e.g. the CC plugin config).\n\n**spawn-session seam** — launch a new agent session on this node. Manifest provides: a command template; `cwd`/project; a `headless` flag (optional, default false — for the GUI's resume-of-compatible-adapters); a `resume` flag (optional); and the `commune` + `signoff` file directories relative to `cwd` (so the daemon knows where to watch). Substitution keys spt-core can supply: `{id}` and, optionally, a spt-core-generated valid session UUID (e.g. injected as `--session-id {uuid}`) so an adapter can skip the post-spawn seam. spt-core does **not** inject: harness-internal env (broker handles binary env; adapter handles in-session env), and **no initial-context handoff** (not needed at start — the agent is prompted for context once its session is up; the first …\n- **id resolution:** `id` is optional. With no id, spt-core reproduces today's no-id `/spt:live` behavior — run the lone live agent if that's all the project has; show a picker with proposed default IDs if the project has none; let the user choose if there are several.\n\n**post-spawn seam** — the just-launched binary calls an spt-core command on boot (via the adapter's SessionStart-equivalent hook) to bind itself. Needed because the harness's own session id usually isn't known until after the binary runs. Payload: the harness `session_id` (when binary-generated rather than spt-core-injected); the `parent_pid` (the stable session-binding anchor — see KNOWN-HAZARDS 2.1); an endpoint identity/type confirmation; optionally a local HTTP port the binary listens on (for HTTP-mode input delivery, below); and a **boot nonce** (a generation/boot discriminator so a respawn-after-crash bind can't be confused with a stale duplicate — guards KNOWN-HAZARDS 2.4). The call flips the perch from skeleton → live.\n\n**post-spawn is optional only under a strict commitment:** an adapter may forgo post-spawn *only* if it (a) injects the spt-core-generated session UUID at spawn AND (b) guarantees the launched-process pid IS the stable session-binding anchor (no wrapper-script / subprocess pid indirection). If either does not hold, post-spawn must fire to report `session_id` and/or `parent_pid`. UUID-injection alone suppresses only the `session_id` reporting, not the binding.\n\n**spawn-psyche seam** — two command templates: fresh-start and resume (the resume template includes `$session_id`). Both include `$psyche_prompt` — the revival essentials spt-core feeds the Psyche (timestamp, incoming event envelope). Everything else is the adapter's: model selection (in its template), and any harness-specific instructions the Psyche needs (Write-tool usage, commune dir) supplied as a static preamble before `$psyche_prompt` or as adapter SessionStart additionalContext. spt-core owns `$psyche_prompt` content; the adapter owns the rest.\n\n**history subsystem** (covers echo-commune source logs, resume briefs, and Shell logs) — two supported paths:\n- **Path A — adapter-owned logs.** Manifest declares a locate-template (keyed by `$session_id`) + a **normalize-command the adapter owns** that emits spt-core's expected normalized format. spt-core docs must teach adapter devs how to build a conformant parser.\n- **Path B — spt-core-native history store.** spt-core exposes a `history-log` command/API; the adapter writes its logs to spt-core in the native format and spt-core stores them. Rationale: spt-core needs its own log store for Shells anyway, and this simplifies integration for flexible/DIY harnesses.\n- The **echo-commune seam** is then just a command template (adapter picks the model) that consumes whichever history path is configured for the session.\n- **Why adapter-owned normalize over spt-core built-in parsers** (grounded in a Codex-CLI vs Claude-Code comparison): transcript formats diverge sharply and move fast. Claude Code = one flat JSONL per session, project-partitioned, locatable directly from the session id (`~/.claude/projects/<hash>/<id>.jsonl`). Codex = date-partitioned **rollout files** (`~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<id>.jsonl`) where the id is only a filename *substring* (must recursive-glob to locate), a **3-level tagged envelope** (`{timestamp,type,payload}` → tagged `ResponseItem` → tagged `ContentItem` with distinct `input_text`/`output_text`), tool calls as separate top-level items, a **second SQLite index that can desync from the files**, Limited/Extended persistence mode…\n- **Profile-relocated transcript roots — env-read capture** (ratified 2026-06-30, counter-38 field-bug grill; supersedes a rejected harness-specific `{config_dir}` proposal): a Path-A locate-template (and the `[digest].source` that reuses it) may reference a manifest-declared **`[env.<VAR>] direction = \"read\"`** var (e.g. `{CLAUDE_CONFIG_DIR}`). spt-core captures the **declared** read-vars — an explicit allowlist, never the whole env — from the session's launch environment at **bind** (the only point the env is present; the ephemeral `[digest]` extractor runs later in the daemon context where it is gone), persists them in the perch, and substitutes them into the locate-template + the extractor's env at digest time. The var's **fallback** is the `[env]` direc…\n- **`[digest]` mirrors history's two strategies — locate ownership** (ratified 2026-06-30, counter-38 W6 design-gate): like `[history]`, `[digest]` supports **`fetcher`** (the adapter's extractor **locates + reads + emits** normalized digest records; spt-core runs it bounded and consumes its stdout, doing **no** locate and **no** pre-read) alongside the original **`locate_normalize`** (`source` template → spt-core locates a **single** file + reads + pipes the bytes to the extractor as a pure stdin→stdout normalizer). The pre-read `locate_normalize` mode only works when the transcript is a **single fully-templatable path**; a **partitioned** layout — CC's project-slug subdir (`projects/<munge(cwd)>/<id>.jsonl`) or Codex's date-glob (`must recursive-glob to lo…\n\n**activity/idle detection** — **not** PTY-quiescence (insufficient: e.g. CC's AskUserQuestion stalls the PTY while holding stdin and needing nuanced input) and **not** a manifest-declared idle signal. Instead, the adapter calls spt-core activity/idle commands at the right moments (from its hooks); those commands manage activity/idle **sentinels inside the session perch**. The idle state lives in the perch, owned by spt-core via the commands API exposed to adapter devs.\n\n**inject-input seam** — message delivery into a running session. Configurable per activity-state (activity / idle / both); multiple methods, any combination:\n- PTY injection (with or without key/submit sequences) — spt-hosted topology;\n- adapter hooks calling spt-core poll commands;\n- an in-adapter-session child relay (à la CC's Monitor tool);\n- adapter manifest requesting HTTP POST delivery to the endpoint binary on a local port (shared via the post-spawn seam).\nNote: even spt-hosted sessions default to hook injection (or the adapter's equivalent) as the non-disruptive path **during activity**; some adapters prefer the in-session relay regardless of topology.\n\n**activity-gated delivery** — an inbound message routes by the receiver's activity sentinel (above). While the endpoint is **active**, the message spools for the receiver's own hook-poll to drain (non-disruptive — the *active window*). On **idle** (or an idle transition before a hook drains it), it delivers immediately — translation binary (spt-hosted) → relay-poll (either topology) → spool, in that fallback order (the *idle window*). The send-side axes below modulate which of these two windows a message is eligible for.\n\n**message delivery axes** — a sent message carries independent modifiers on orthogonal axes; it is **not** a single \"type\". The flag on each axis defaults to the unrestricted value:\n- **delivery window** (*when*) — **default** (both windows; delivers in whichever fires first) · **idle-only** (held for the idle window; delivered immediately if already idle) · **active-only** (active window only — the receiver's hook-poll; never wakes an idle agent). *active-only* is the renamed legacy **deferred** (the `deferred=1` spool column + `api poll --include-deferred` are its internal/adapter-facing names). Mutually exclusive.\n- **channel restriction** (*through what*) — **unrestricted** (any configured inject method) · **prefer-native** (the translation binary if one is running, else fall back to the standard methods) · **force-native** (the translation binary and nothing else — no fallback, no spool-to-another-method). Mutually exclusive; composes with the window. *\"Native\"* = the `[message-idle-translation-binary]` PTY channel.\n- **persistence** (*how long it waits*) — **durable** (default; spooled until delivered or TTL) · **ephemeral** (dropped if it cannot deliver in its accepted window — at the moment the window opens with no live carrier, or at TTL, whichever is first). Ephemeral is the **only** path permitted to drop silently (the REQ-HAZARD-IDLE-SILENT-NONDELIVERY carve-out); every non-ephemeral path spools and reports non-delivery. <!-- v0.15.0 PARTIAL (W3): ephemeral evaporation covers the spt-hosted-binary no-carrier-at-window leg (the idle-transition drain drops ephemeral rows the binary cannot take) + the TTL leg (purge). The harness-hosted relay \"window opens with no live *listener*\" leg is NOT yet delivered — it needs relay carrier-presence detection (same separate-co…\nWindow restricts *when* delivery is accepted, channel restricts *which method* carries it, persistence restricts *how long* it waits — they compose freely (e.g. `force-native` + `active-only` = the binary injects during the active window, never idle; `force-native` + `ephemeral` = binary-or-nothing).\n\n**message metadata (`json`)** — a sender may attach an opaque JSON metadata block (`--json-payload`), carried as a single attr-escaped `json=\"…\"` envelope attribute **alongside** (never replacing) the body. spt-core never interprets it — pure verbatim passthrough across every rail (spool / TCP / WAN / EVENT-PART), parsed only by the receiving adapter (its hooks and/or translation binary). Collision-proof by construction: the structured data lives **inside** the single `json` value, so it can never forge spt-core's control/identity attributes (`from`, `type`, …). Available to any sender — it confers no spt-core authority; what a custom field *means* is the receiving adapter's trust decision (the same posture as `from`-is-never-payload-trusted).\n\n**resume-session seam** — two distinct forms:\n- **fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> That command is **`spt api psyche-download <id> [--session-id <sid>]`**: it emits the durable resume brief (role → live-context → project-context, project resolved from the perch's bound cwd) to stdout for the adapter's SessionStart hook to inject as additional context, and APPENDS any **not-yet-synthesized** commune/signoff drop as a `<pending-commune>`/`<pending-signoff>` slice AFTER the durable tiers — closing the window where a just-dropped commune…\n- **continue-existing:** resume an existing harness session under the adapter (its native resume).\n\n**capability declaration** — which endpoint types a harness/node can host (a Pi node might host only Shells, never a LiveAgent). Static manifest list, consumed by the subnet registry so a node advertises its hostable types. Exact shape is design-open.\n\n**adapter-update seam** — file-pull or delegated-command (see Self-update). Locked.\n\nThere is no separate \"model/billing\" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model. The full manifest schema is `docs/MANIFEST.md`; key model-level facts from it:\n\n- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.\n- **A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github/<safe>/` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binaries is **self-contained**, needing no PATH placement. <!-- [doc->REQ-INSTALL-11] --> Applies to the `[digest]` extractor, the `[session.psyche_init]` runner, and the `adapter digest-proof` tool; the install dir is the registry record's `source_dir` (precise) for the daemon-resolved paths — the `[digest]` extractor and the daemon-h…\n- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.\n- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.\n- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs` — its own adapter, not a binary-swap). <!-- [doc->REQ-MANIFEST-6] --> A fallback **target is addressed as `<adapter>:<profile>`** (not just a bare adapter_name) and resolves through the one composite-addressing resolver (`registry::resolve_option`), so a fallback may select a shipped or local profile (`ccs`, `ccs:<profile>`) exactly as any other adapter-option read site does. *Contract only at M12-W3 — the addressing resolves; the node-wide setting + its rate-limit invocation belong to the consuming milestone (no reader exists yet, so no config field is added).* Adapte…\n- **Config knobs** (pulse period, echo-commune window, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override** — never per-adapter.\n- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> This includes the **checkpoint sentinel `!!checkpoint!!`** — the agent-checkpoint trigger an adapter embeds in a commune/signoff drop body (one bare token = checkpoint with default wake; a `!!checkpoint!! <text> !!checkpoint!!` pair makes the inter-marker text a custom wake directive). It is spt-core control metadata: spt-core STRIPS every occurrence (keeping the inter-marker text) before the drop body reaches agent context, at BOTH points it can — the resume `<pending-*>` presentation (pre-synthesis) and the durable tier write (post-synthesis) — so the marker never surfaces or …\n\n### Inbound `api` surface (detailed)\n\nAll commands below are `api`-prefixed (machinery-facing). Every `api` invocation **and** every manifest carries a unified **`adapter_name`** string (e.g. `claude-spt`) identifying the owning adapter. This is load-bearing: one daemon hosts endpoints from multiple adapters (`claude-spt`, `spt-codex`, `spt-pi`), so the daemon resolves an endpoint's manifest + seams (history normalize-command, inject method, update avenue) by its `adapter_name`; adapter-update ripples target by it; capability lookup and telemetry key on it.\n\n<!-- [doc->REQ-API-4] -->\n**Manifest resolution from `--adapter` (since v0.8.0).** `spt api <cmd> --adapter <name[:profile]>` resolves the registered adapter's manifest, `:profile` overlay, and install dir from the registry when `--manifest` is omitted — a registered adapter's `api` calls need only `--adapter`. `--manifest <path>` becomes an optional **override** (an unregistered or local-dev manifest): when present, the manifest loads from that file and the install dir is its parent directory; when absent, both come from the registry record (the install dir is the record's precise `source_dir`). An unregistered adapter with no `--manifest` degrades to no-manifest rather than failing.\n\n- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.\n- **`api listen`** — *long-running* relay/poll listener that an adapter-owned (harness-hosted) session owns as a child process; streams the daemon's events to the session's stdout. Distinct from the short-lived `api poll`. This is the heir to today's Monitor-bound `$LIVE start` poll loop.\n- **`api poll`** — short-lived drain of queued messages for a session (the hook-injection delivery path). `--include-deferred` optionally also drains deferred rows, for adapter flexibility (default excludes them — KNOWN-HAZARDS 1.4/4.4).\n- **`api state <busy|idle>`** — adapter reports session activity; writes the activity/idle sentinel in the session perch. By default `api state idle` also writes the **echo-commune gate sentinel** (`.more-done`-equiv — modern spt couples them); `--no-gate` suppresses that coupling, and a standalone **`api echo-gate <set|clear>`** gives granular adapters explicit control over when echo communes may fire, independent of idle.\n- **`api worker-start`** / **`api worker-stop`** — Worker (subagent) perch create/teardown under the parent (nested, registry-tracked).\n- **`api worker-poll`** — a Worker (subagent) receives its queued messages (inbound from Self or sibling Workers).\n- **`api boundary <clear|compact>`** — context-boundary report; **carries the new `session_id`** (it rotates on `/clear` or `/compact`), so the daemon rebinds the perch to the new session id while keeping the stable identity + `parent_pid` anchor. Authors a **Self-resume commune** (resume the Self session → commune file-drop) rather than a background echo — strong live-context signal at the boundary (see `docs/CONTEXT-MEMORY.md`). **Rotation credential** (ADR-0032): the proof of association for this one verb belongs to the **departed** session (its sid, or the perch token) — the new sid is the *payload*, never the *proof* — so adapters persist the current sid across the rotation (endpoint-keyed adapter state, NOT per-session env) and present it; the design-t…\n- **`api session-end`** — session stop/crash report → soft teardown by default (preserve perch + spool + tracked history for recovery — KNOWN-HAZARDS 6.2). **`--erase`** instead hard-wipes the perch and tracked history (for ephemeral/secondary adapters that act as robust agent-spawned-agent surfaces).\n\n**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running host would let the daemon re-create or re-host mid-purge); **`--force`** stops it first (→ the daemon reconcile un-hosts it and reaps its Psyche) and then purges. **`--yes`** skips the interactive confirm (the CI path); purge refuses removing the **caller's own run…\n_Avoid_: consent-gating it (it is intentionally ungated, for CI); treating it as a sync/remote op (local-only); a soft variant (purge is always the hard, full wipe — soft teardown is `endpoint stop`).\n<!-- [doc->REQ-ENDPOINT-PURGE] -->\n\n- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.\n- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).\n- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.\n\n**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike.\n\n### Startup flows (the two topologies)\n\n**Adapters never resolve `$SPT_HOME`.** spt-core install registers its binary directory on the system-wide PATH, so adapters call `spt api …` on any OS without path math. All harness↔daemon bridging goes through `spt api` commands (the daemon is always running, or auto-started — below), so there is **no adapter-written file** in the bind path.\n\n**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent).** Key constraint: the SPT *live agent* does not exist until the agent invokes start — the `live_id` isn't chosen at session boot, and `$LIVE start` is itself invoked *behind the Monitor tool*, so it becomes the long-running relay. So binding cannot happen at SessionStart directly. A **seed record** (daemon-held, in-memory — not a file) bridges the gap:\n1. The harness's SessionStart hook calls **`spt api seed --pid <parent_pid> --session-id <sid> [cwd]`**. The daemon records an ephemeral in-memory **seed entry** keyed by `parent_pid` — the session details the spt-hosted topology would share directly, minus the not-yet-chosen `live_id`. The seed is **adapter-agnostic**: it carries no `adapter_name`. <!-- [doc->REQ-START-5] --> *Which* adapter/profile a session belongs to is resolved later, at bind, as a read against the live registry (below) — so one SessionStart hook seeds correctly no matter which harness adapters are installed, and an `adapter add` after the seed is never missed. In-memory (not a file) avoids drive churn and the `$SPT_HOME` resolution nuisance; seeds are consumed within seconds, so persis…\n2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. It self-discovers its `parent_pid`, the daemon matches the seed entry by that pid (validated against `session_id` to defeat PID-recycling — KNOWN-HAZARDS 5.1), **resolves the owning adapter/profile** (the bind-time resolution below), creates/revives the perch binding `live_id` ↔ session details, then enters the long-running relay loop streaming events to stdout.\n3. The always-on daemon holds the perch, spool, registry, and daemon-spawns the Psyche (via the spawn-psyche seam) — no separate wrapper. The relay is purely the delivery pipe.\n   - Seed entry refreshed on each SessionStart (keeps `session_id` current across `/clear`, since `parent_pid` is stable while the harness process persists). If a harness has no SessionStart-equiv, `start` may carry the details directly as args — the seed is the preferred convenience, not the only path.\n   - The same seed + bind-time resolution serves a **ReadyAgent** bringup (`$SPT ready`/poll), not just a LiveAgent — a harness-hosted ready agent is seeded and resolved identically (it just binds a poll listener, no Psyche).\n\n**Bind-time adapter/profile resolution (ADR-0021).** Because the seed is adapter-agnostic, `listen`/`poll` resolve the owning adapter/profile when they bind, as a pure read — never a seed-time snapshot that could drift. `--adapter <name[:profile]>` is an **optional override** on the `api` group (an explicit choice for adapter dev/iteration); omitted, resolution runs:\n1. the seed's `parent_pid` → that process's **executable basename** (case-insensitive, `.exe`-stripped);\n2. **candidate adapters** = registered `kind=\"harness\"` adapters whose **`host_binaries`** (the manifest match-key) contains that basename; <!-- [doc->REQ-MANIFEST-8] -->\n3. **profile**: the durable **active-profile pointer** (`spt adapter use <adapter>[:profile]` writes it; one default per `host_binary`) wins; unset → the freshest candidate adapter by `registered_at_ms`, base profile (a specific profile is only ever chosen by the pointer), name-ascending on ties; <!-- [doc->REQ-INSTALL-12] -->\n4. zero candidates → a friendly error naming the binary and the `--adapter` escape. The pointer is a standing user preference (durable on disk, never auto-written by install/update); the seed is ephemeral — see ADR-0021.\n\n**Daemon auto-start:** the daemon is per-machine always-on (OS-service registered), but any `spt api` invocation that needs it will **start it if absent** (fresh boot, crash, never-installed-as-service). `$SPT listen` for the first SPT session on a machine thus transparently spins up the daemon. Ensure-running lives in the `api` layer generally; `listen` is the reliable anchor.\n\n**spt-hosted (terminal wrapper / GUI launcher; the daemon launches the binary into a broker PTY):**\n1. The frontend/CLI launches the agent: the daemon runs the **spawn-session** command template into a broker-held PTY.\n2. The binary boots and fires **`api bind`** (or skips it under the strict UUID-injection + stable-pid commitment). **No catalyst/seed file** — the daemon is the launcher, already holds a direct channel (it spawned the process and owns the PTY), so a file round-trip would only add drive churn for no benefit.\n3. The daemon delivers events; method is **manifest-configurable per activity-state** — direct PTY injection, or a relay even here (some adapters prefer a relay over PTY injection for idle delivery), or HTTP. During *activity*, delivery still defaults to the non-disruptive hook-injection path, not raw PTY writes.\n4. Psyche is daemon-spawned, same as above.\n\nSo the old `$LIVE start` splits by topology: harness-hosted = SessionStart writes an adapter-agnostic seed → `$SPT listen <id>` consumes seed (by `parent_pid`) + resolves adapter/profile (ADR-0021) + binds + relays — legacy parity (`$LIVE start <id>` → `$SPT listen <id>`, no mandatory `--adapter`); spt-hosted = daemon spawn-session + `api bind` (direct, no file). The asymmetry is the file-bridge-only-when-no-direct-channel principle.\n\n**Env-var aliases:** adapters inject clean env-var aliases for in-session invocation (heirs to today's `$OWL`/`$LIVE`), e.g. **`$SPT` = `spt api`** so a Monitor-bound call reads `$SPT listen <id>`. spt-core supplies the subcommands; the adapter supplies the env aliases (manifest philosophy).\n\n### Endpoint types\n\nEach perch advertises an **endpoint type** — a tag that says what shape of entity lives at that perch and what operations it accepts. The set of day-one types:\n\n**ReadyAgent**:\nMinimal SPT participant — a perch + a poll listener, no Psyche, no live-agent wrapper. Direct heir to the sister project's \"ready agent\".\n\n**LiveAgent**:\nA Self with a Psyche companion. Composite logical actor; addressable as one ID, but its component perches (the Self's, the Psyche's) live independently. Direct heir to the sister project's \"live agent\".\n\n**Psyche**:\nThe Psyche companion's own perch, distinct from its paired LiveAgent's perch. First-class endpoint type so messages addressed to a LiveAgent's Psyche route directly without ambiguity. **A Psyche is a bounded per-event turn, not a resident process (since v0.25.0).** Each psyche-relevant event (a pulse fire, a commune/signoff drop, a session-custody transition) runs **exactly one** bounded turn through the psyche role template, spawned by the daemon, which exits at turn end — there is no long-lived psyche loop or psyche pid between events. <!-- [doc->REQ-PSYCHE-EPHEMERAL-DRIVER] --> **Liveness = turns succeed** — never a PID or a resident-process check. A Self perch is online-and-hosted whether or not any psyche turn is in flight; a psyche turn failure of any …\n\n*I/O & trust boundary (ADR-0012):* the Psyche is a **sandboxed** actor — it may read and write files but **cannot send messages or reach the network itself**. Its inbound context arrives two ways: events/messages the daemon hands it, and **commune/signoff file-drops** (Self → daemon → Psyche; the *Summarizer* authors the commune delta). Its **sole outbound** is **reply/notify intents** the daemon relays as its **outbound proxy** — emitted as `<EVENT type=\"reply\">`/`<EVENT type=\"notify\">` (the shared envelope grammar). A *reply* reaches **only the sender it answers**; a *notify* reaches **only the agent's own user** — the Psyche carries no target and cannot address arbitrary endpoints (the daemon strips/re-stamps `from=` before relaying).\n\n*Psyche-host health — harness-reachable failure signal (v0.8.1, REQ-HAZARD-LIVEHOST-BOOT-RACE):* a LiveAgent's `status=online` is daemon-authoritative liveness and **stays authoritative** — but it does not by itself prove the daemon hosted a Psyche. When the brain's live-host reconcile fails to spawn the Psyche (e.g. the adapter's psyche binary is absent from its install dir, or the net-less boot-race starves the host), that failure was previously **silent** — only an `eprintln!` on the brain's invisible stderr, while a harness (and a human via `spt endpoint list` / `whoami`) reads **perch state**, never brain stderr. The Self perch's `info.json` therefore carries an additive, N-1-safe `psyche_host_error` field (`{reason, ts, attempts}`): a **current-state**…\n\n**Summarizer**:\nThe ephemeral, cheap model that builds a **commune delta** from a Self's recent turns and feeds it *into* the **Psyche** as inbound context. A distinct actor from the Psyche — different (cheaper) model, fire-and-forget, **no perch** (not an endpoint type). It authors *commune* deltas only, **never** *reply*/*notify*.\n_Avoid_: conflating with the Psyche; \"echo-commune model\".\n\n**Worker**:\nA subagent's perch under a parent LiveAgent. Created on subagent start, torn down on subagent stop. Replaces today's \"working perch\" concept; first-class type so cross-communication between a Self and its workers (and worker↔worker) is addressable.\n\n**SptNode**:\nA machine's participation in an SPT subnet, identified by an Ed25519 public key generated on first run. First-class so networking primitives can address nodes directly as message targets, not only as transport peers. The node identity and network endpoint are hosted by the machine's `spt-daemon` (see Networking), not a separate process.\n\n<!-- [doc->REQ-EP-6] -->\n**Gateway** (concept ratified 2026-06-11; registered via the open type system, first instance downstream):\nA **human-backed endpoint** — a user's specialized window into the subnet from a device or surface with no conventional-harness compatibility. Nothing LLM-shaped runs there; the intelligence at the endpoint is the **user**. Addressable like any endpoint (receives digests/messages, sends via the normal verbs) and may **own Shells** (it is an owning endpoint — see §Shell model). Distinct from a Shell: a Shell is *driven from elsewhere*; a Gateway *originates* interaction. No `tracked/` mind, no Psyche (LiveAgent affordances). First instance: the `spt-lecturn` adapter's Playdate endpoint (own repo).\n\n<!-- [doc->REQ-MSG-5] -->\nA message sent from a Gateway carries **the user's authority** — it *is* the user speaking through a device — and is delivered typed **`user-msg`** (ratified 2026-06-12) so receiving agents weight it as user instruction, not peer-agent chatter. The type is **identity-gated, never payload-trusted** (the KH 7.3/7.5 posture): the daemon permits `user-msg` only from user-backed origins (a Gateway endpoint, the local user's own CLI) and re-stamps an agent-family sender's `user-msg` down to plain `msg` — authority comes from who you are, not what you wrote.\n\n<!-- [doc->REQ-MSG-6] -->\n_Implemented posture_: the **local** user-backed origins are honored end-to-end — a locally-hosted Gateway endpoint (info.json `state=\"gateway\"`) and the local user's CLI (M9-T4/T5). The **cross-node WAN** path is being completed (trust posture **ratified 2026-06-13**): the **subnet membership boundary is the trust boundary**. A subnet is a collection of machines the user already trusts, so a `user-msg` arriving over the subnet from a **Gateway-typed** origin is honored as the user's authority; the daemon does **not** defend against a subnet member *forging* the Gateway type — an in-subnet compromise is out of scope by construction (if the subnet is breached at all, the trust model is already void). The origin's type is read from its advertised registry **`e…\n\nA Gateway endpoint binary is revived by **existing machinery only** (settled 2026-06-12, two corrections deep): while running, the bridged device's link liveness drives ordinary **instance state** (sustained device silence → dormant; device contact → active — the driver-attach rule). Across a node restart, revival rides a **co-located shell's wake-watcher** — the Gateway typically owns a shell instance on its own gateway host; that shell's offline wake-watcher (one of the two classes of third-party binary spt-core boot-launches — the other is the [[AlwaysOnEndpoint]] resident binary) holds the device-contact surface and fires the standard **wake resolution** (\"owner suspended → revive the owner\"). No Gateway-manifest watcher, no autostart flag, no new mechan…\n_Avoid_: calling a Gateway a Shell or an agent; \"console\", \"remote\".\n\n**PresenceChannel** (broker endpoint — concept locked, impl deferred past v1):\nA *broker* endpoint, not an interaction surface. Job: (1) **presence resolution** — track which node + endpoint the user most recently interacted with; (2) **shell brokering** — locate/instantiate the right Shell on that node and relay between the agent and the user. An agent \"just knows how to reach the user\" by firing at its PresenceChannel; the channel figures out the rest. Also a durable, **shell-agnostic 2-way thread**: messages persist in the channel, not in any one Shell, so the user can be sent a message via a phone messaging-Shell and surface/continue that same agent conversation later at a GameRobot Shell. Shells are interchangeable I/O windows onto the channel's thread.\n\nThree interaction styles:\n- **dispatch** — fire-and-forget: \"reach the user with this payload\"; channel delivers via the best available Shell.\n- **bind** — sustained drive: \"give me a Shell of capability X\"; channel instantiates and the agent drives it directly until teardown. Supports operating a *specific* Shell regardless of where the user currently is (agent transience).\n- **thread** — the persistent conversation that floats across Shells; the user can pick it up from any Shell, and 2-way payloads (text/audio/image/video, subject to the Shell's supported types) flow both directions.\n\nPresence datum: `(last_active_node, last_active_endpoint, timestamp)`. The `last_active_endpoint` field lets an agent choose between messaging that specific endpoint vs. driving a parallel instance of itself.\n\n**AlwaysOnEndpoint** (always-on endpoint; concept ratified 2026-06-21 — core kind, first instance downstream `spt-discord`):\nA **resident, addressable endpoint that hosts no mind.** Its binary is daemon-**supervised continuously and runs independent of any agent's liveness** (up even when zero agents are online) — unlike an *agent endpoint* (a hosted mind with a Psyche + `tracked/` context) and unlike a **Shell** (single-owner, *driven*). It is **two-way addressable**: agents message it (to drive whatever external surface it fronts) and it messages out — notably it may call `endpoint wake <id>` to draw an offline agent online (wake authorization is **target-side**, so no special caller right is needed — see the wake-watcher/sleep-wake model). Declared by an adapter's **`[always-on]`** manifest section; the supervised binary is **one per adapter-option** (`<adapter>[:profile]`), an…\n_Avoid_: calling it a Shell (owner-less + not driven) or an agent (no mind); \"service\" alone (it is addressable, not faceless); a sleep/wake resting model (it does not rest).\n\n**instance state (active / dormant / suspended / offline)**:\nThe four liveness states a per-endpoint registry row (`registry::Status`) advertises across the subnet. The **active/dormant pair is the multi-instance routing differentiator**: an endpoint may run on several nodes at once (cross-node context sync) — the **active** instance is the bare-`id` routing target, its live siblings are **dormant**. The canonical meaning (the resting state machine, `resting.rs`, implements active/dormant/suspended; offline is registry-only):\n…\n\n…\n\n…\nThe CLI sibling of the frontend's guided resume — one command that lists endpoints **grouped by locality, most-recently-used within each group**: `on-node / current-project → on-node / other-project → off-node`, mirroring the *resolution policy*'s local-first preference. Selection **chains conditionally**: a **running** instance → attach/tap-in (no adapter step — already live under one); a **non-running** endpoint → into the **adapter selector** (*adapter selection*: history head = default → prior adapters → \"choose a different adapter\") → *home subnet* / other creation prompts as needed → launch; a **\"+ new endpoint\"** entry → the full creation flow. Off-node picks respect the reach + consent gates (remote-drive of your own running instance is ungated; a co…\n\n[Showing lines 1-283 of 883. Use :284 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"# spt-core\r\n\r\n**Platform scope:** Windows + Linux for v1. macOS is out (no test machine available) but kept structurally easy — `portable-pty` and Iroh both support it, so macOS is a later test/CI-budget decision, not a re-architecture.\r\n\r\n**Legacy migration:** it should be possible — ideally *automatic* — for a user to migrate an existing `claude_skill_owl` (modern SPT) install to spt-core (identity, agents, tracked Psyche context). Exact mechanism deferred to design; the commitment is that migration is a first-class supported path, not a manual rebuild.\r\n\r\nHarness-independent core for the SPT ecosystem. Provides inter-agent messaging, live-agent lifecycle, terminal wrapping, self-update, and networking primitives — as both a Rust library workspace and a canonical reference binary. Designed so any agent runtime (Claude Code, Codex, Cursor, headless, future harnesses) can interface with the SPT ecosystem either by shelling out to the binary or by linking the crates directly.\r\n\r\nSuccessor to `claude_skill_owl` (today's \"modern SPT\"), which is being rebuilt as `spt-core` to untether the system from Claude Code and lift it to a general-purpose agent-ecosystem core.\r\n\r\n## Language\r\n\r\n**spt-core**:\r\nThe system. Canonical name. The Rust workspace and the umbrella project.\r\n\r\n**spt.exe / spt** (canonical binary):\r\nThe reference binary built from the workspace. Replaces today's `owl.exe`. Most external integrations (plugins, hooks, scripts in other harnesses) interact with spt-core *only* through this binary — fire-and-forget subcommands, long-running listeners under a parent harness's process supervisor, etc. Unix builds use the same name without `.exe`.\r\n\r\n**library workspace**:\r\nThe set of Rust crates that compose spt-core. Consumers that want a deeper integration than shelling out to `spt.exe` link these crates directly. The reference binary is itself a consumer of the workspace. The expected non-binary consumers are future first-party services that link Rust directly.\r\n\r\n**spt plugin** (separate downstream project — NOT an spt-core deliverable):\r\nA rebuilt version of today's Claude Code `spt` plugin. It is the **first consumer** built *atop* spt-core and the **acceptance proof** of spt-core v1 (it reaches feature parity with modern SPT while delegating all core functionality to spt-core, primarily via `spt.exe`, with deeper hooks where useful) — but it **lives and builds in its own repository, outside spt-core**. It is a Claude-Code-specific *adapter*: it holds the Claude Code conventions (hooks, slash-commands, skill/plugin layout, `claude` session-invocation). **spt-core itself contains zero Claude Code conventions** — only the harness-agnostic contract the plugin binds to. The only adapter-shaped artifact ever in this repo is a generic mock/test adapter exercising the manifest + `api` contract (PR…\n\r\n**Pi** (disambiguation — two meanings, never conflate):\r\n(1) **Pi, the coding agent/harness** (`badlogic/pi-mono`) — a harness example alongside Claude Code and Codex; this is the meaning in user-facing harness lists. (2) **Pi-class node** — Raspberry-Pi-class low-power hardware hosting a Shell-only or headless SPT node; an incidental hardware descriptor, never an explicit product example. Public-facing docs must disambiguate or avoid the bare word.\r\n_Avoid_: bare \"Pi node\" when the harness is meant.\r\n\r\n**spt-daemon** (per-machine supervisor):\r\nThe single always-on, one-per-machine logical supervisor. Owns the PTYs for all hosted sessions, the node's network identity + WAN endpoint, the subnet registry, all spools, **all poll-listener logic, and all Psyche/pulse loops** — everything is consolidated here (no separate poll-listener or Psyche-wrapper processes; listeners already touch sessions directly under capsule/idle, and Psyche wrappers already invoke harness binaries directly, so they belong in the one supervisor). Collapses what the sister project planned as a separate `spt-node` daemon into one process — see Networking. The `spt-node` separate-deliverable concept is retired.\r\n\r\nInternally the logical daemon is split into two implementation layers for seamless self-update (see Self-update):\r\n- **broker** (stable \"kernel\") — holds *only* the un-transferable, must-not-die resources: PTY master fds, the spawned harness child processes, and listening network sockets. Minimal, dumb, versioned local IPC. Almost never updates.\r\n- **daemon brain** (\"userspace\") — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk state and re-attaches to the broker's held handles.\r\n\r\nLogical addressing is unchanged — still one per-machine `spt-daemon`; the broker is an internal layer, not separately addressable. There is exactly **one broker per machine** (per `SPT_HOME`) — *not* one per endpoint: a single broker holds every hosted endpoint's resources, and it is present whenever the daemon runs, even with zero endpoints online (the bare-daemon case). It is therefore the always-present per-machine layer, which is why the single-daemon lock + liveness anchor belong to it.\r\n\r\n**in-session relay**:\r\nA thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle delivery into an spt-hosted PTY goes through an opt-in adapter **translation binary** (`[message-idle-translation-binary]`, ADR-0022): a pure stdin→stdout filter spt-core lifecycle-manages — it reads the `<EVENT>` feed on stdin, emits keystroke-commands (`{key}`/`{…\n\r\n### Deliverable shape\r\n\r\nspt-core ships **both** a library workspace and a canonical binary:\r\n\r\n- **Library crates** — the deeper integration path. Used by future first-party services that link Rust directly.\r\n- **`spt.exe` / `spt`** — the canonical binary, built from the workspace. The primary integration path for harness plugins and external tooling, which mostly fire it as a subprocess at various surfaces (one-shot commands, poll listeners under a Monitor-tool-equivalent, hook tap-ins).\r\n\r\nBoth surfaces are first-class. Wire-protocol parity between them is a versioning concern from day one (a non-Rust client speaking to `spt.exe` and a Rust client linking the crates must see the same observable behavior).\r\n\r\n## Runtime model\r\n\r\nspt-core is harness-independent: it does not know about Claude Code, Codex, Cursor, or any other agent runtime. All harness-specific surfaces (how to invoke an agent session, fetch conversation history for an echo commune, detect activity/idleness, etc.) are abstracted behind a runtime layer that consumers supply.\r\n\r\n**AgentRuntime** (Rust trait, implementation detail):\r\nThe internal Rust abstraction over a harness. Anything spt-core needs to do *to* or *with* an agent goes through this trait. Most consumers never see it directly — they configure spt-core via a manifest, and spt-core's default `ManifestRuntime` implementation executes against the manifest.\r\n\r\n**harness contract** (umbrella term):\r\nThe full surface a harness binds to in order to participate in the spt-core ecosystem. Has two equally-important halves: the **runtime manifest** (outbound — how spt-core drives the harness) and the **subcommand surface** (inbound — how the harness reports events back to spt-core). A harness implementation is one TOML/YAML manifest + a binding from the harness's own hook system into `spt.exe <subcommand>` calls.\r\n\r\n**runtime manifest** (outbound half of the harness contract):\r\nA declarative configuration file (TOML/YAML — schema TBD) that tells spt-core how to drive a specific harness. Declares: how to invoke an agent session, how to look up conversation history for an echo commune, how to spawn/resume a Psyche-equivalent, which binary or command implements each harness-side operation, and which endpoint types this harness supports. spt-core is the actor for each of these; the manifest tells it what to do.\r\n\r\nExample shape (illustrative): `spt.exe --manifest spt-plugin.toml live start <id>`. A harness like the planned spt plugin wraps this invocation into the `$LIVE` / `$OWL` environment variables it injects into its sessions, so harness-internal callers continue to invoke `$LIVE` / `$OWL` unchanged.\r\n\r\n**subcommand surface** (inbound half of the harness contract):\r\nThe stable set of `spt.exe <subcommand>` entry points that harnesses bind their own hook systems to. When the harness's runtime emits an event (subagent started, tool just invoked, user typed `/clear`, session crashed), the harness's hook fires a short-lived `spt.exe <subcommand>` invocation that mutates on-disk SPT state (perch registry, spool, etc.). spt-core publishes this surface; harnesses author the bindings.\r\n\r\n**Naming convention:** these inbound, machinery-facing commands are prefixed **`api `/`api-`** (e.g. `spt api bind`, `spt api state`) to distinguish them from the agent-facing verbs an agent invokes directly (`send`, `ring`, `ready`, …). The `api` namespace is the harness/adapter commands-API; the unprefixed namespace is the agent surface.\r\n\r\nTogether: manifest + subcommand surface = the complete harness API. A sidecar-style long-running adapter process speaking a wire protocol is explicitly **deferred** as a possible v2 alternative for harnesses that outgrow the manifest+hooks shape (e.g. need streaming or in-memory state across events). Not built day-one.\r\n\r\n**adapter manifest header** (`adapter_name` + version compat):\r\nEvery manifest declares a unified **`adapter_name`** (e.g. `claude-spt`), carried on every `api` invocation too. It is load-bearing: one daemon hosts endpoints from multiple adapters, so it resolves an endpoint's manifest + seams by `adapter_name`; adapter-update ripples target by it; capability/manifest lookup and telemetry key on it. The header also declares the adapter's own version and a **`min_spt_core_version`** — the minimum spt-core the adapter requires. This declaration must be **readable before an adapter update is applied** (it lives in the manifest header / a small metadata file fetched first), so spt-core can verify compatibility / expected supported features *before* committing the update. If installed spt-core < the adapter's `min_spt_core_ver…\n\r\n<!-- [doc->REQ-MANIFEST-2] -->\r\n**adapter profile** (ratified 2026-06-11, Gateway grill; future spt-core milestone — first beneficiaries `spt-claude-code` and the usbip shell):\r\nA named **sparse overlay** on its parent adapter manifest. Merge semantics are **leaf-replace**: a profile key replaces the whole value at that path (arrays included — never spliced or appended). The merged result is a complete manifest, and the profile behaves as a distinct adapter option everywhere: canonical addressing is the composite **`<adapter>:<profile>`** (`claude-spt:work`, `spt-usbip-driver:hid-only`) in every place a bare `adapter_name` rides today (perch `info.json`, capability resolution, `api` invocations, `spt adapter list`); the bare name = the parent unmodified. **Two sources, one semantics:** a **shipped profile** is declared inside the parent manifest by the adapter dev and updates as one unit with it; a **local profile** is a node-local …\n_Avoid_: \"manifest fork\", \"child adapter\", per-profile versioning.\r\n\r\n**adapter strings** (ratified 2026-06-11, Gateway grill):\r\n<!-- [doc->REQ-MANIFEST-3] -->\r\nA `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** p…\n<!-- [doc->REQ-MANIFEST-5] -->\r\n**File-backed strings** (M12-W3): a `[strings]` value MAY be a **file pointer** instead of an inline literal — a value-position table with **exactly one** key `file`: `skill = { file = \"skill.md\" }`. `get-string` resolves it to the file's **contents** (so large bodies — skill-instructions, hint text — stay out of the manifest). The exactly-one-key rule is the disambiguation: any other table shape stays an opaque nested strings tree (existing trees untouched), and `{ file = … }` is reserved as the pointer form (it can't double as inline data). Files live in the adapter's per-adapter aux dir **`adapters/<adapter>/strings/`** (sibling of `profiles/`), referenced by a path relative to it that **must stay inside that dir** (HAZARD-class containment: `..` traversa…\n_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); \"adapter KV store\" as a separate registry; putting user files in the adapter-shipped `strings/` dir (clobbered by updates — use a local profile).\r\n\r\n**manifest substitution in `[strings]`** (ratified 2026-06-25, v0.16.0 update-arc grill):\r\n`get-string` resolves a set of **adapter-static** substitution keys inside a returned string value at **read time** (lazily, like file-backed strings): **`{adapter_dir}`** — the registry record's precise `source_dir` (the install dir; survives updates; the same dir bare-program resolution already uses) — and **`{adapter_name}`**. Session-scoped keys (`{id}`/`{session_id}`/…) are **not** available: `get-string` carries no session context today, and a `get-string --session-id` for session-scoped substitution is a deferred, larger change. The load-bearing invariant is preserved: **spt-core still never executes a string** — it substitutes and returns; the *adapter's own wrapper* executes the result. Canonical use: a harness hook dispatcher resolves its own packe…\n_Avoid_: session-scoped substitution through bare `get-string`; reading this as spt-core executing a string (it never does — the adapter wrapper executes the resolved value).\r\n\r\n**keyword hints** (ratified 2026-06-12 — core milestone A):\r\n<!-- [doc->REQ-MANIFEST-4] -->\r\nOnce-per-session usage/syntax hints, a first-class adapter feature: the manifest's `[hints]` section declares entries of `{keywords (literal default, regex opt-in), text}`; the adapter's user-prompt hook pipes the **full user message** to `spt api hint --session <id>` (stdin) and receives matched hint lines (`keyword hint for SPT adapter <name>: \"<kw>\"-->{text}`) for its context-injection channel. The daemon keeps a per-session seen-set — each hint fires **once per session** (a `/clear` mints a new session, naturally re-arming) — and emits at most **one hint per message**. **Tiebreak when a message matches multiple hints:** scan in declaration order and emit the FIRST match whose hint this session has not yet seen; if the declaration-order-first matching hin…\n_Avoid_: unconditional static context (that's the adapter's own preamble); firing per-message.\r\n\r\n**adapter update declaration** (manifest field):\r\n<!-- [doc->REQ-UPD-9] -->\r\nEach adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = \"user/repo\"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed …\n\r\n**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):\r\n<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->\r\nA `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS…\n<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->\r\nAn optional **`[update].message`** (avenue-agnostic) is a plain multi-line operator notice surfaced to stdout, markdown-rendered (the helpfmt prose path), **only when an update is actually applied** (the version changed) — never on a no-op. It is read from the newly-installed manifest with no `{key}` substitution; its use is to announce a post-update action (e.g. \"run `/reload-plugins` in any ongoing sessions\").\r\n\r\n**composite update — `[update.post]`** (ratified 2026-06-25, v0.16.0 update-arc grill; ADR-0029):\r\nAn optional **avenue-agnostic** post-step `{ command, self_verifies }` spt-core runs **after** the primary avenue resolves — in the same `spt adapter update` **and at `spt adapter add`** (install is the first update, so a fresh install conducts the post-step too; bug-#1 operator ruling, v0.19.0 — the eager-extract acquisition runs it post-registration, a delegated acquisition after the acquisition succeeds, and only the payload-less `file_pull` PENDING add defers it to the payload's arrival) — so one lever pulls the adapter `.spt` (`gh_release`) **and** runs a delegated reconcile (e.g. an adapter's `claude plugin update` cross-platform binary). It runs **foreground and bounded** (the subprocess-timeout hazard bound, 120s; never backgrounded — when the CLI re…\n\r\n**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.\r\n_Avoid_: calling the Psyche loop or an on-demand extractor a \"resident\" binary; \"restart the endpoint\" for what is a per-binary cycle.\r\n\r\n**session-invocation declaration** (manifest field, noted for spt-plugin parity):\r\nHow the harness spawns agent sessions, including Psyche and echo-commune sessions. For the rebuilt spt-plugin, Psyche and echo communes must migrate **off `claude -p`** (imminent Claude Code billing changes) to headless `claude` sessions (`--resume` for the Psyche). This is an adapter/manifest concern, not a core concern, but the parity milestone must carry it.\r\n\r\n### Manifest seams (outbound contract, detailed)\r\n\r\nGoverning principle: **SPT is not a harness.** Model choice, billing shape, harness-internal env, and harness-internal context are entirely the adapter's concern, expressed inside the adapter's own command templates. spt-core owns only the template *mechanism* (substitution keys), the substitution *values* it is responsible for, and the surrounding lifecycle. Env for the *endpoint binary itself* is auto-handled by spt-core/broker; env for the *agent running inside* that binary is the adapter's config (e.g. the CC plugin config).\r\n\r\n**spawn-session seam** — launch a new agent session on this node. Manifest provides: a command template; `cwd`/project; a `headless` flag (optional, default false — for the GUI's resume-of-compatible-adapters); a `resume` flag (optional); and the `commune` + `signoff` file directories relative to `cwd` (so the daemon knows where to watch). Substitution keys spt-core can supply: `{id}` and, optionally, a spt-core-generated valid session UUID (e.g. injected as `--session-id {uuid}`) so an adapter can skip the post-spawn seam. spt-core does **not** inject: harness-internal env (broker handles binary env; adapter handles in-session env), and **no initial-context handoff** (not needed at start — the agent is prompted for context once its session is up; the first …\n- **id resolution:** `id` is optional. With no id, spt-core reproduces today's no-id `/spt:live` behavior — run the lone live agent if that's all the project has; show a picker with proposed default IDs if the project has none; let the user choose if there are several.\r\n\r\n**post-spawn seam** — the just-launched binary calls an spt-core command on boot (via the adapter's SessionStart-equivalent hook) to bind itself. Needed because the harness's own session id usually isn't known until after the binary runs. Payload: the harness `session_id` (when binary-generated rather than spt-core-injected); the `parent_pid` (the stable session-binding anchor — see KNOWN-HAZARDS 2.1); an endpoint identity/type confirmation; optionally a local HTTP port the binary listens on (for HTTP-mode input delivery, below); and a **boot nonce** (a generation/boot discriminator so a respawn-after-crash bind can't be confused with a stale duplicate — guards KNOWN-HAZARDS 2.4). The call flips the perch from skeleton → live.\r\n\r\n**post-spawn is optional only under a strict commitment:** an adapter may forgo post-spawn *only* if it (a) injects the spt-core-generated session UUID at spawn AND (b) guarantees the launched-process pid IS the stable session-binding anchor (no wrapper-script / subprocess pid indirection). If either does not hold, post-spawn must fire to report `session_id` and/or `parent_pid`. UUID-injection alone suppresses only the `session_id` reporting, not the binding.\r\n\r\n**spawn-psyche seam** — two command templates: fresh-start and resume (the resume template includes `$session_id`). Both include `$psyche_prompt` — the revival essentials spt-core feeds the Psyche (timestamp, incoming event envelope). Everything else is the adapter's: model selection (in its template), and any harness-specific instructions the Psyche needs (Write-tool usage, commune dir) supplied as a static preamble before `$psyche_prompt` or as adapter SessionStart additionalContext. spt-core owns `$psyche_prompt` content; the adapter owns the rest.\r\n\r\n**history subsystem** (covers echo-commune source logs, resume briefs, and Shell logs) — two supported paths:\r\n- **Path A — adapter-owned logs.** Manifest declares a locate-template (keyed by `$session_id`) + a **normalize-command the adapter owns** that emits spt-core's expected normalized format. spt-core docs must teach adapter devs how to build a conformant parser.\r\n- **Path B — spt-core-native history store.** spt-core exposes a `history-log` command/API; the adapter writes its logs to spt-core in the native format and spt-core stores them. Rationale: spt-core needs its own log store for Shells anyway, and this simplifies integration for flexible/DIY harnesses.\r\n- The **echo-commune seam** is then just a command template (adapter picks the model) that consumes whichever history path is configured for the session.\r\n- **Why adapter-owned normalize over spt-core built-in parsers** (grounded in a Codex-CLI vs Claude-Code comparison): transcript formats diverge sharply and move fast. Claude Code = one flat JSONL per session, project-partitioned, locatable directly from the session id (`~/.claude/projects/<hash>/<id>.jsonl`). Codex = date-partitioned **rollout files** (`~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<id>.jsonl`) where the id is only a filename *substring* (must recursive-glob to locate), a **3-level tagged envelope** (`{timestamp,type,payload}` → tagged `ResponseItem` → tagged `ContentItem` with distinct `input_text`/`output_text`), tool calls as separate top-level items, a **second SQLite index that can desync from the files**, Limited/Extended persistence mode…\n- **Profile-relocated transcript roots — env-read capture** (ratified 2026-06-30, counter-38 field-bug grill; supersedes a rejected harness-specific `{config_dir}` proposal): a Path-A locate-template (and the `[digest].source` that reuses it) may reference a manifest-declared **`[env.<VAR>] direction = \"read\"`** var (e.g. `{CLAUDE_CONFIG_DIR}`). spt-core captures the **declared** read-vars — an explicit allowlist, never the whole env — from the session's launch environment at **bind** (the only point the env is present; the ephemeral `[digest]` extractor runs later in the daemon context where it is gone), persists them in the perch, and substitutes them into the locate-template + the extractor's env at digest time. The var's **fallback** is the `[env]` direc…\n- **`[digest]` mirrors history's two strategies — locate ownership** (ratified 2026-06-30, counter-38 W6 design-gate): like `[history]`, `[digest]` supports **`fetcher`** (the adapter's extractor **locates + reads + emits** normalized digest records; spt-core runs it bounded and consumes its stdout, doing **no** locate and **no** pre-read) alongside the original **`locate_normalize`** (`source` template → spt-core locates a **single** file + reads + pipes the bytes to the extractor as a pure stdin→stdout normalizer). The pre-read `locate_normalize` mode only works when the transcript is a **single fully-templatable path**; a **partitioned** layout — CC's project-slug subdir (`projects/<munge(cwd)>/<id>.jsonl`) or Codex's date-glob (`must recursive-glob to lo…\n\r\n**activity/idle detection** — **not** PTY-quiescence (insufficient: e.g. CC's AskUserQuestion stalls the PTY while holding stdin and needing nuanced input) and **not** a manifest-declared idle signal. Instead, the adapter calls spt-core activity/idle commands at the right moments (from its hooks); those commands manage activity/idle **sentinels inside the session perch**. The idle state lives in the perch, owned by spt-core via the commands API exposed to adapter devs.\r\n\r\n**inject-input seam** — message delivery into a running session. Configurable per activity-state (activity / idle / both); multiple methods, any combination:\r\n- PTY injection (with or without key/submit sequences) — spt-hosted topology;\r\n- adapter hooks calling spt-core poll commands;\r\n- an in-adapter-session child relay (à la CC's Monitor tool);\r\n- adapter manifest requesting HTTP POST delivery to the endpoint binary on a local port (shared via the post-spawn seam).\r\nNote: even spt-hosted sessions default to hook injection (or the adapter's equivalent) as the non-disruptive path **during activity**; some adapters prefer the in-session relay regardless of topology.\r\n\r\n**activity-gated delivery** — an inbound message routes by the receiver's activity sentinel (above). While the endpoint is **active**, the message spools for the receiver's own hook-poll to drain (non-disruptive — the *active window*). On **idle** (or an idle transition before a hook drains it), it delivers immediately — translation binary (spt-hosted) → relay-poll (either topology) → spool, in that fallback order (the *idle window*). The send-side axes below modulate which of these two windows a message is eligible for.\r\n\r\n**message delivery axes** — a sent message carries independent modifiers on orthogonal axes; it is **not** a single \"type\". The flag on each axis defaults to the unrestricted value:\r\n- **delivery window** (*when*) — **default** (both windows; delivers in whichever fires first) · **idle-only** (held for the idle window; delivered immediately if already idle) · **active-only** (active window only — the receiver's hook-poll; never wakes an idle agent). *active-only* is the renamed legacy **deferred** (the `deferred=1` spool column + `api poll --include-deferred` are its internal/adapter-facing names). Mutually exclusive.\r\n- **channel restriction** (*through what*) — **unrestricted** (any configured inject method) · **prefer-native** (the translation binary if one is running, else fall back to the standard methods) · **force-native** (the translation binary and nothing else — no fallback, no spool-to-another-method). Mutually exclusive; composes with the window. *\"Native\"* = the `[message-idle-translation-binary]` PTY channel.\r\n- **persistence** (*how long it waits*) — **durable** (default; spooled until delivered or TTL) · **ephemeral** (dropped if it cannot deliver in its accepted window — at the moment the window opens with no live carrier, or at TTL, whichever is first). Ephemeral is the **only** path permitted to drop silently (the REQ-HAZARD-IDLE-SILENT-NONDELIVERY carve-out); every non-ephemeral path spools and reports non-delivery. <!-- v0.15.0 PARTIAL (W3): ephemeral evaporation covers the spt-hosted-binary no-carrier-at-window leg (the idle-transition drain drops ephemeral rows the binary cannot take) + the TTL leg (purge). The harness-hosted relay \"window opens with no live *listener*\" leg is NOT yet delivered — it needs relay carrier-presence detection (same separate-co…\nWindow restricts *when* delivery is accepted, channel restricts *which method* carries it, persistence restricts *how long* it waits — they compose freely (e.g. `force-native` + `active-only` = the binary injects during the active window, never idle; `force-native` + `ephemeral` = binary-or-nothing).\r\n\r\n**message metadata (`json`)** — a sender may attach an opaque JSON metadata block (`--json-payload`), carried as a single attr-escaped `json=\"…\"` envelope attribute **alongside** (never replacing) the body. spt-core never interprets it — pure verbatim passthrough across every rail (spool / TCP / WAN / EVENT-PART), parsed only by the receiving adapter (its hooks and/or translation binary). Collision-proof by construction: the structured data lives **inside** the single `json` value, so it can never forge spt-core's control/identity attributes (`from`, `type`, …). Available to any sender — it confers no spt-core authority; what a custom field *means* is the receiving adapter's trust decision (the same posture as `from`-is-never-payload-trusted).\r\n\r\n**resume-session seam** — two distinct forms:\r\n- **fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> That command is **`spt api psyche-download <id> [--session-id <sid>]`**: it emits the durable resume brief (role → live-context → project-context, project resolved from the perch's bound cwd) to stdout for the adapter's SessionStart hook to inject as additional context, and APPENDS any **not-yet-synthesized** commune/signoff drop as a `<pending-commune>`/`<pending-signoff>` slice AFTER the durable tiers — closing the window where a just-dropped commune…\n- **continue-existing:** resume an existing harness session under the adapter (its native resume).\r\n\r\n**capability declaration** — which endpoint types a harness/node can host (a Pi node might host only Shells, never a LiveAgent). Static manifest list, consumed by the subnet registry so a node advertises its hostable types. Exact shape is design-open.\r\n\r\n**adapter-update seam** — file-pull or delegated-command (see Self-update). Locked.\r\n\r\nThere is no separate \"model/billing\" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model. The full manifest schema is `docs/MANIFEST.md`; key model-level facts from it:\r\n\r\n- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.\r\n- **A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github/<safe>/` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binaries is **self-contained**, needing no PATH placement. <!-- [doc->REQ-INSTALL-11] --> Applies to the `[digest]` extractor, the `[session.psyche_init]` runner, and the `adapter digest-proof` tool; the install dir is the registry record's `source_dir` (precise) for the daemon-resolved paths — the `[digest]` extractor and the daemon-h…\n- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.\r\n- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.\r\n- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs` — its own adapter, not a binary-swap). <!-- [doc->REQ-MANIFEST-6] --> A fallback **target is addressed as `<adapter>:<profile>`** (not just a bare adapter_name) and resolves through the one composite-addressing resolver (`registry::resolve_option`), so a fallback may select a shipped or local profile (`ccs`, `ccs:<profile>`) exactly as any other adapter-option read site does. *Contract only at M12-W3 — the addressing resolves; the node-wide setting + its rate-limit invocation belong to the consuming milestone (no reader exists yet, so no config field is added).* Adapte…\n- **Config knobs** (pulse period, echo-commune window, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override** — never per-adapter.\r\n- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> This includes the **checkpoint sentinel `!!checkpoint!!`** — the agent-checkpoint trigger an adapter embeds in a commune/signoff drop body (one bare token = checkpoint with default wake; a `!!checkpoint!! <text> !!checkpoint!!` pair makes the inter-marker text a custom wake directive). It is spt-core control metadata: spt-core STRIPS every occurrence (keeping the inter-marker text) before the drop body reaches agent context, at BOTH points it can — the resume `<pending-*>` presentation (pre-synthesis) and the durable tier write (post-synthesis) — so the marker never surfaces or …\n\r\n### Inbound `api` surface (detailed)\r\n\r\nAll commands below are `api`-prefixed (machinery-facing). Every `api` invocation **and** every manifest carries a unified **`adapter_name`** string (e.g. `claude-spt`) identifying the owning adapter. This is load-bearing: one daemon hosts endpoints from multiple adapters (`claude-spt`, `spt-codex`, `spt-pi`), so the daemon resolves an endpoint's manifest + seams (history normalize-command, inject method, update avenue) by its `adapter_name`; adapter-update ripples target by it; capability lookup and telemetry key on it.\r\n\r\n<!-- [doc->REQ-API-4] -->\r\n**Manifest resolution from `--adapter` (since v0.8.0).** `spt api <cmd> --adapter <name[:profile]>` resolves the registered adapter's manifest, `:profile` overlay, and install dir from the registry when `--manifest` is omitted — a registered adapter's `api` calls need only `--adapter`. `--manifest <path>` becomes an optional **override** (an unregistered or local-dev manifest): when present, the manifest loads from that file and the install dir is its parent directory; when absent, both come from the registry record (the install dir is the record's precise `source_dir`). An unregistered adapter with no `--manifest` degrades to no-manifest rather than failing.\r\n\r\n- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.\r\n- **`api listen`** — *long-running* relay/poll listener that an adapter-owned (harness-hosted) session owns as a child process; streams the daemon's events to the session's stdout. Distinct from the short-lived `api poll`. This is the heir to today's Monitor-bound `$LIVE start` poll loop.\r\n- **`api poll`** — short-lived drain of queued messages for a session (the hook-injection delivery path). `--include-deferred` optionally also drains deferred rows, for adapter flexibility (default excludes them — KNOWN-HAZARDS 1.4/4.4).\r\n- **`api state <busy|idle>`** — adapter reports session activity; writes the activity/idle sentinel in the session perch. By default `api state idle` also writes the **echo-commune gate sentinel** (`.more-done`-equiv — modern spt couples them); `--no-gate` suppresses that coupling, and a standalone **`api echo-gate <set|clear>`** gives granular adapters explicit control over when echo communes may fire, independent of idle.\r\n- **`api worker-start`** / **`api worker-stop`** — Worker (subagent) perch create/teardown under the parent (nested, registry-tracked).\r\n- **`api worker-poll`** — a Worker (subagent) receives its queued messages (inbound from Self or sibling Workers).\r\n- **`api boundary <clear|compact>`** — context-boundary report; **carries the new `session_id`** (it rotates on `/clear` or `/compact`), so the daemon rebinds the perch to the new session id while keeping the stable identity + `parent_pid` anchor. Authors a **Self-resume commune** (resume the Self session → commune file-drop) rather than a background echo — strong live-context signal at the boundary (see `docs/CONTEXT-MEMORY.md`). **Rotation credential** (ADR-0032): the proof of association for this one verb belongs to the **departed** session (its sid, or the perch token) — the new sid is the *payload*, never the *proof* — so adapters persist the current sid across the rotation (endpoint-keyed adapter state, NOT per-session env) and present it; the design-t…\n- **`api session-end`** — session stop/crash report → soft teardown by default (preserve perch + spool + tracked history for recovery — KNOWN-HAZARDS 6.2). **`--erase`** instead hard-wipes the perch and tracked history (for ephemeral/secondary adapters that act as robust agent-spawned-agent surfaces).\r\n\r\n**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running host would let the daemon re-create or re-host mid-purge); **`--force`** stops it first (→ the daemon reconcile un-hosts it and reaps its Psyche) and then purges. **`--yes`** skips the interactive confirm (the CI path); purge refuses removing the **caller's own run…\n_Avoid_: consent-gating it (it is intentionally ungated, for CI); treating it as a sync/remote op (local-only); a soft variant (purge is always the hard, full wipe — soft teardown is `endpoint stop`).\r\n<!-- [doc->REQ-ENDPOINT-PURGE] -->\r\n\r\n- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.\r\n- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).\r\n- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.\r\n\r\n**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike.\r\n\r\n### Startup flows (the two topologies)\r\n\r\n**Adapters never resolve `$SPT_HOME`.** spt-core install registers its binary directory on the system-wide PATH, so adapters call `spt api …` on any OS without path math. All harness↔daemon bridging goes through `spt api` commands (the daemon is always running, or auto-started — below), so there is **no adapter-written file** in the bind path.\r\n\r\n**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent).** Key constraint: the SPT *live agent* does not exist until the agent invokes start — the `live_id` isn't chosen at session boot, and `$LIVE start` is itself invoked *behind the Monitor tool*, so it becomes the long-running relay. So binding cannot happen at SessionStart directly. A **seed record** (daemon-held, in-memory — not a file) bridges the gap:\r\n1. The harness's SessionStart hook calls **`spt api seed --pid <parent_pid> --session-id <sid> [cwd]`**. The daemon records an ephemeral in-memory **seed entry** keyed by `parent_pid` — the session details the spt-hosted topology would share directly, minus the not-yet-chosen `live_id`. The seed is **adapter-agnostic**: it carries no `adapter_name`. <!-- [doc->REQ-START-5] --> *Which* adapter/profile a session belongs to is resolved later, at bind, as a read against the live registry (below) — so one SessionStart hook seeds correctly no matter which harness adapters are installed, and an `adapter add` after the seed is never missed. In-memory (not a file) avoids drive churn and the `$SPT_HOME` resolution nuisance; seeds are consumed within seconds, so persis…\n2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. It self-discovers its `parent_pid`, the daemon matches the seed entry by that pid (validated against `session_id` to defeat PID-recycling — KNOWN-HAZARDS 5.1), **resolves the owning adapter/profile** (the bind-time resolution below), creates/revives the perch binding `live_id` ↔ session details, then enters the long-running relay loop streaming events to stdout.\r\n3. The always-on daemon holds the perch, spool, registry, and daemon-spawns the Psyche (via the spawn-psyche seam) — no separate wrapper. The relay is purely the delivery pipe.\r\n   - Seed entry refreshed on each SessionStart (keeps `session_id` current across `/clear`, since `parent_pid` is stable while the harness process persists). If a harness has no SessionStart-equiv, `start` may carry the details directly as args — the seed is the preferred convenience, not the only path.\r\n   - The same seed + bind-time resolution serves a **ReadyAgent** bringup (`$SPT ready`/poll), not just a LiveAgent — a harness-hosted ready agent is seeded and resolved identically (it just binds a poll listener, no Psyche).\r\n\r\n**Bind-time adapter/profile resolution (ADR-0021).** Because the seed is adapter-agnostic, `listen`/`poll` resolve the owning adapter/profile when they bind, as a pure read — never a seed-time snapshot that could drift. `--adapter <name[:profile]>` is an **optional override** on the `api` group (an explicit choice for adapter dev/iteration); omitted, resolution runs:\r\n1. the seed's `parent_pid` → that process's **executable basename** (case-insensitive, `.exe`-stripped);\r\n2. **candidate adapters** = registered `kind=\"harness\"` adapters whose **`host_binaries`** (the manifest match-key) contains that basename; <!-- [doc->REQ-MANIFEST-8] -->\r\n3. **profile**: the durable **active-profile pointer** (`spt adapter use <adapter>[:profile]` writes it; one default per `host_binary`) wins; unset → the freshest candidate adapter by `registered_at_ms`, base profile (a specific profile is only ever chosen by the pointer), name-ascending on ties; <!-- [doc->REQ-INSTALL-12] -->\r\n4. zero candidates → a friendly error naming the binary and the `--adapter` escape. The pointer is a standing user preference (durable on disk, never auto-written by install/update); the seed is ephemeral — see ADR-0021.\r\n\r\n**Daemon auto-start:** the daemon is per-machine always-on (OS-service registered), but any `spt api` invocation that needs it will **start it if absent** (fresh boot, crash, never-installed-as-service). `$SPT listen` for the first SPT session on a machine thus transparently spins up the daemon. Ensure-running lives in the `api` layer generally; `listen` is the reliable anchor.\r\n\r\n**spt-hosted (terminal wrapper / GUI launcher; the daemon launches the binary into a broker PTY):**\r\n1. The frontend/CLI launches the agent: the daemon runs the **spawn-session** command template into a broker-held PTY.\r\n2. The binary boots and fires **`api bind`** (or skips it under the strict UUID-injection + stable-pid commitment). **No catalyst/seed file** — the daemon is the launcher, already holds a direct channel (it spawned the process and owns the PTY), so a file round-trip would only add drive churn for no benefit.\r\n3. The daemon delivers events; method is **manifest-configurable per activity-state** — direct PTY injection, or a relay even here (some adapters prefer a relay over PTY injection for idle delivery), or HTTP. During *activity*, delivery still defaults to the non-disruptive hook-injection path, not raw PTY writes.\r\n4. Psyche is daemon-spawned, same as above.\r\n\r\nSo the old `$LIVE start` splits by topology: harness-hosted = SessionStart writes an adapter-agnostic seed → `$SPT listen <id>` consumes seed (by `parent_pid`) + resolves adapter/profile (ADR-0021) + binds + relays — legacy parity (`$LIVE start <id>` → `$SPT listen <id>`, no mandatory `--adapter`); spt-hosted = daemon spawn-session + `api bind` (direct, no file). The asymmetry is the file-bridge-only-when-no-direct-channel principle.\r\n\r\n**Env-var aliases:** adapters inject clean env-var aliases for in-session invocation (heirs to today's `$OWL`/`$LIVE`), e.g. **`$SPT` = `spt api`** so a Monitor-bound call reads `$SPT listen <id>`. spt-core supplies the subcommands; the adapter supplies the env aliases (manifest philosophy).\r\n\r\n### Endpoint types\r\n\r\nEach perch advertises an **endpoint type** — a tag that says what shape of entity lives at that perch and what operations it accepts. The set of day-one types:\r\n\r\n**ReadyAgent**:\r\nMinimal SPT participant — a perch + a poll listener, no Psyche, no live-agent wrapper. Direct heir to the sister project's \"ready agent\".\r\n\r\n**LiveAgent**:\r\nA Self with a Psyche companion. Composite logical actor; addressable as one ID, but its component perches (the Self's, the Psyche's) live independently. Direct heir to the sister project's \"live agent\".\r\n\r\n**Psyche**:\r\nThe Psyche companion's own perch, distinct from its paired LiveAgent's perch. First-class endpoint type so messages addressed to a LiveAgent's Psyche route directly without ambiguity. **A Psyche is a bounded per-event turn, not a resident process (since v0.25.0).** Each psyche-relevant event (a pulse fire, a commune/signoff drop, a session-custody transition) runs **exactly one** bounded turn through the psyche role template, spawned by the daemon, which exits at turn end — there is no long-lived psyche loop or psyche pid between events. <!-- [doc->REQ-PSYCHE-EPHEMERAL-DRIVER] --> **Liveness = turns succeed** — never a PID or a resident-process check. A Self perch is online-and-hosted whether or not any psyche turn is in flight; a psyche turn failure of any …\n\r\n*I/O & trust boundary (ADR-0012):* the Psyche is a **sandboxed** actor — it may read and write files but **cannot send messages or reach the network itself**. Its inbound context arrives two ways: events/messages the daemon hands it, and **commune/signoff file-drops** (Self → daemon → Psyche; the *Summarizer* authors the commune delta). Its **sole outbound** is **reply/notify intents** the daemon relays as its **outbound proxy** — emitted as `<EVENT type=\"reply\">`/`<EVENT type=\"notify\">` (the shared envelope grammar). A *reply* reaches **only the sender it answers**; a *notify* reaches **only the agent's own user** — the Psyche carries no target and cannot address arbitrary endpoints (the daemon strips/re-stamps `from=` before relaying).\r\n\r\n*Psyche-host health — harness-reachable failure signal (v0.8.1, REQ-HAZARD-LIVEHOST-BOOT-RACE):* a LiveAgent's `status=online` is daemon-authoritative liveness and **stays authoritative** — but it does not by itself prove the daemon hosted a Psyche. When the brain's live-host reconcile fails to spawn the Psyche (e.g. the adapter's psyche binary is absent from its install dir, or the net-less boot-race starves the host), that failure was previously **silent** — only an `eprintln!` on the brain's invisible stderr, while a harness (and a human via `spt endpoint list` / `whoami`) reads **perch state**, never brain stderr. The Self perch's `info.json` therefore carries an additive, N-1-safe `psyche_host_error` field (`{reason, ts, attempts}`): a **current-state**…\n\r\n**Summarizer**:\r\nThe ephemeral, cheap model that builds a **commune delta** from a Self's recent turns and feeds it *into* the **Psyche** as inbound context. A distinct actor from the Psyche — different (cheaper) model, fire-and-forget, **no perch** (not an endpoint type). It authors *commune* deltas only, **never** *reply*/*notify*.\r\n_Avoid_: conflating with the Psyche; \"echo-commune model\".\r\n\r\n**Worker**:\r\nA subagent's perch under a parent LiveAgent. Created on subagent start, torn down on subagent stop. Replaces today's \"working perch\" concept; first-class type so cross-communication between a Self and its workers (and worker↔worker) is addressable.\r\n\r\n**SptNode**:\r\nA machine's participation in an SPT subnet, identified by an Ed25519 public key generated on first run. First-class so networking primitives can address nodes directly as message targets, not only as transport peers. The node identity and network endpoint are hosted by the machine's `spt-daemon` (see Networking), not a separate process.\r\n\r\n<!-- [doc->REQ-EP-6] -->\r\n**Gateway** (concept ratified 2026-06-11; registered via the open type system, first instance downstream):\r\nA **human-backed endpoint** — a user's specialized window into the subnet from a device or surface with no conventional-harness compatibility. Nothing LLM-shaped runs there; the intelligence at the endpoint is the **user**. Addressable like any endpoint (receives digests/messages, sends via the normal verbs) and may **own Shells** (it is an owning endpoint — see §Shell model). Distinct from a Shell: a Shell is *driven from elsewhere*; a Gateway *originates* interaction. No `tracked/` mind, no Psyche (LiveAgent affordances). First instance: the `spt-lecturn` adapter's Playdate endpoint (own repo).\r\n\r\n<!-- [doc->REQ-MSG-5] -->\r\nA message sent from a Gateway carries **the user's authority** — it *is* the user speaking through a device — and is delivered typed **`user-msg`** (ratified 2026-06-12) so receiving agents weight it as user instruction, not peer-agent chatter. The type is **identity-gated, never payload-trusted** (the KH 7.3/7.5 posture): the daemon permits `user-msg` only from user-backed origins (a Gateway endpoint, the local user's own CLI) and re-stamps an agent-family sender's `user-msg` down to plain `msg` — authority comes from who you are, not what you wrote.\r\n\r\n<!-- [doc->REQ-MSG-6] -->\r\n_Implemented posture_: the **local** user-backed origins are honored end-to-end — a locally-hosted Gateway endpoint (info.json `state=\"gateway\"`) and the local user's CLI (M9-T4/T5). The **cross-node WAN** path is being completed (trust posture **ratified 2026-06-13**): the **subnet membership boundary is the trust boundary**. A subnet is a collection of machines the user already trusts, so a `user-msg` arriving over the subnet from a **Gateway-typed** origin is honored as the user's authority; the daemon does **not** defend against a subnet member *forging* the Gateway type — an in-subnet compromise is out of scope by construction (if the subnet is breached at all, the trust model is already void). The origin's type is read from its advertised registry **`e…\n\r\nA Gateway endpoint binary is revived by **existing machinery only** (settled 2026-06-12, two corrections deep): while running, the bridged device's link liveness drives ordinary **instance state** (sustained device silence → dormant; device contact → active — the driver-attach rule). Across a node restart, revival rides a **co-located shell's wake-watcher** — the Gateway typically owns a shell instance on its own gateway host; that shell's offline wake-watcher (one of the two classes of third-party binary spt-core boot-launches — the other is the [[AlwaysOnEndpoint]] resident binary) holds the device-contact surface and fires the standard **wake resolution** (\"owner suspended → revive the owner\"). No Gateway-manifest watcher, no autostart flag, no new mechan…\n_Avoid_: calling a Gateway a Shell or an agent; \"console\", \"remote\".\r\n\r\n**PresenceChannel** (broker endpoint — concept locked, impl deferred past v1):\r\nA *broker* endpoint, not an interaction surface. Job: (1) **presence resolution** — track which node + endpoint the user most recently interacted with; (2) **shell brokering** — locate/instantiate the right Shell on that node and relay between the agent and the user. An agent \"just knows how to reach the user\" by firing at its PresenceChannel; the channel figures out the rest. Also a durable, **shell-agnostic 2-way thread**: messages persist in the channel, not in any one Shell, so the user can be sent a message via a phone messaging-Shell and surface/continue that same agent conversation later at a GameRobot Shell. Shells are interchangeable I/O windows onto the channel's thread.\r\n\r\nThree interaction styles:\r\n- **dispatch** — fire-and-forget: \"reach the user with this payload\"; channel delivers via the best available Shell.\r\n- **bind** — sustained drive: \"give me a Shell of capability X\"; channel instantiates and the agent drives it directly until teardown. Supports operating a *specific* Shell regardless of where the user currently is (agent transience).\r\n- **thread** — the persistent conversation that floats across Shells; the user can pick it up from any Shell, and 2-way payloads (text/audio/image/video, subject to the Shell's supported types) flow both directions.\r\n\r\nPresence datum: `(last_active_node, last_active_endpoint, timestamp)`. The `last_active_endpoint` field lets an agent choose between messaging that specific endpoint vs. driving a parallel instance of itself.\r\n\r\n**AlwaysOnEndpoint** (always-on endpoint; concept ratified 2026-06-21 — core kind, first instance downstream `spt-discord`):\r\nA **resident, addressable endpoint that hosts no mind.** Its binary is daemon-**supervised continuously and runs independent of any agent's liveness** (up even when zero agents are online) — unlike an *agent endpoint* (a hosted mind with a Psyche + `tracked/` context) and unlike a **Shell** (single-owner, *driven*). It is **two-way addressable**: agents message it (to drive whatever external surface it fronts) and it messages out — notably it may call `endpoint wake <id>` to draw an offline agent online (wake authorization is **target-side**, so no special caller right is needed — see the wake-watcher/sleep-wake model). Declared by an adapter's **`[always-on]`** manifest section; the supervised binary is **one per adapter-option** (`<adapter>[:profile]`), an…\n_Avoid_: calling it a Shell (owner-less + not driven) or an agent (no mind); \"service\" alone (it is addressable, not faceless); a sleep/wake resting model (it does not rest).\r\n\r\n**instance state (active / dormant / suspended / offline)**:\r\nThe four liveness states a per-endpoint registry row (`registry::Status`) advertises across the subnet. The **active/dormant pair is the multi-instance routing differentiator**: an endpoint may run on several nodes at once (cross-node context sync) — the **active** instance is the bare-`id` routing target, its live siblings are **dormant**. The canonical meaning (the resting state machine, `resting.rs`, implements active/dormant/suspended; offline is registry-only):\r","truncated":true,"truncatedBy":"lines","totalLines":883,"totalBytes":69553,"outputLines":283,"outputBytes":69553,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"# spt-core\r\n\r\n**Platform scope:** Windows + Linux for v1. macOS is out (no test machine available) but kept structurally easy — `portable-pty` and Iroh both support it, so macOS is a later test/CI-budget decision, not a re-architecture.\r\n\r\n**Legacy migration:** it should be possible — ideally *automatic* — for a user to migrate an existing `claude_skill_owl` (modern SPT) install to spt-core (identity, agents, tracked Psyche context). Exact mechanism deferred to design; the commitment is that migration is a first-class supported path, not a manual rebuild.\r\n\r\nHarness-independent core for the SPT ecosystem. Provides inter-agent messaging, live-agent lifecycle, terminal wrapping, self-update, and networking primitives — as both a Rust library workspace and a canonical reference binary. Designed so any agent runtime (Claude Code, Codex, Cursor, headless, future harnesses) can interface with the SPT ecosystem either by shelling out to the binary or by linking the crates directly.\r\n\r\nSuccessor to `claude_skill_owl` (today's \"modern SPT\"), which is being rebuilt as `spt-core` to untether the system from Claude Code and lift it to a general-purpose agent-ecosystem core.\r\n\r\n## Language\r\n\r\n**spt-core**:\r\nThe system. Canonical name. The Rust workspace and the umbrella project.\r\n\r\n**spt.exe / spt** (canonical binary):\r\nThe reference binary built from the workspace. Replaces today's `owl.exe`. Most external integrations (plugins, hooks, scripts in other harnesses) interact with spt-core *only* through this binary — fire-and-forget subcommands, long-running listeners under a parent harness's process supervisor, etc. Unix builds use the same name without `.exe`.\r\n\r\n**library workspace**:\r\nThe set of Rust crates that compose spt-core. Consumers that want a deeper integration than shelling out to `spt.exe` link these crates directly. The reference binary is itself a consumer of the workspace. The expected non-binary consumers are future first-party services that link Rust directly.\r\n\r\n**spt plugin** (separate downstream project — NOT an spt-core deliverable):\r\nA rebuilt version of today's Claude Code `spt` plugin. It is the **first consumer** built *atop* spt-core and the **acceptance proof** of spt-core v1 (it reaches feature parity with modern SPT while delegating all core functionality to spt-core, primarily via `spt.exe`, with deeper hooks where useful) — but it **lives and builds in its own repository, outside spt-core**. It is a Claude-Code-specific *adapter*: it holds the Claude Code conventions (hooks, slash-commands, skill/plugin layout, `claude` session-invocation). **spt-core itself contains zero Claude Code conventions** — only the harness-agnostic contract the plugin binds to. The only adapter-shaped artifact ever in this repo is a generic mock/test adapter exercising the manifest + `api` contract (PR…\n\r\n**Pi** (disambiguation — two meanings, never conflate):\r\n(1) **Pi, the coding agent/harness** (`badlogic/pi-mono`) — a harness example alongside Claude Code and Codex; this is the meaning in user-facing harness lists. (2) **Pi-class node** — Raspberry-Pi-class low-power hardware hosting a Shell-only or headless SPT node; an incidental hardware descriptor, never an explicit product example. Public-facing docs must disambiguate or avoid the bare word.\r\n_Avoid_: bare \"Pi node\" when the harness is meant.\r\n\r\n**spt-daemon** (per-machine supervisor):\r\nThe single always-on, one-per-machine logical supervisor. Owns the PTYs for all hosted sessions, the node's network identity + WAN endpoint, the subnet registry, all spools, **all poll-listener logic, and all Psyche/pulse loops** — everything is consolidated here (no separate poll-listener or Psyche-wrapper processes; listeners already touch sessions directly under capsule/idle, and Psyche wrappers already invoke harness binaries directly, so they belong in the one supervisor). Collapses what the sister project planned as a separate `spt-node` daemon into one process — see Networking. The `spt-node` separate-deliverable concept is retired.\r\n\r\nInternally the logical daemon is split into two implementation layers for seamless self-update (see Self-update):\r\n- **broker** (stable \"kernel\") — holds *only* the un-transferable, must-not-die resources: PTY master fds, the spawned harness child processes, and listening network sockets. Minimal, dumb, versioned local IPC. Almost never updates.\r\n- **daemon brain** (\"userspace\") — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk state and re-attaches to the broker's held handles.\r\n\r\nLogical addressing is unchanged — still one per-machine `spt-daemon`; the broker is an internal layer, not separately addressable. There is exactly **one broker per machine** (per `SPT_HOME`) — *not* one per endpoint: a single broker holds every hosted endpoint's resources, and it is present whenever the daemon runs, even with zero endpoints online (the bare-daemon case). It is therefore the always-present per-machine layer, which is why the single-daemon lock + liveness anchor belong to it.\r\n\r\n**in-session relay**:\r\nA thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle delivery into an spt-hosted PTY goes through an opt-in adapter **translation binary** (`[message-idle-translation-binary]`, ADR-0022): a pure stdin→stdout filter spt-core lifecycle-manages — it reads the `<EVENT>` feed on stdin, emits keystroke-commands (`{key}`/`{…\n\r\n### Deliverable shape\r\n\r\nspt-core ships **both** a library workspace and a canonical binary:\r\n\r\n- **Library crates** — the deeper integration path. Used by future first-party services that link Rust directly.\r\n- **`spt.exe` / `spt`** — the canonical binary, built from the workspace. The primary integration path for harness plugins and external tooling, which mostly fire it as a subprocess at various surfaces (one-shot commands, poll listeners under a Monitor-tool-equivalent, hook tap-ins).\r\n\r\nBoth surfaces are first-class. Wire-protocol parity between them is a versioning concern from day one (a non-Rust client speaking to `spt.exe` and a Rust client linking the crates must see the same observable behavior).\r\n\r\n## Runtime model\r\n\r\nspt-core is harness-independent: it does not know about Claude Code, Codex, Cursor, or any other agent runtime. All harness-specific surfaces (how to invoke an agent session, fetch conversation history for an echo commune, detect activity/idleness, etc.) are abstracted behind a runtime layer that consumers supply.\r\n\r\n**AgentRuntime** (Rust trait, implementation detail):\r\nThe internal Rust abstraction over a harness. Anything spt-core needs to do *to* or *with* an agent goes through this trait. Most consumers never see it directly — they configure spt-core via a manifest, and spt-core's default `ManifestRuntime` implementation executes against the manifest.\r\n\r\n**harness contract** (umbrella term):\r\nThe full surface a harness binds to in order to participate in the spt-core ecosystem. Has two equally-important halves: the **runtime manifest** (outbound — how spt-core drives the harness) and the **subcommand surface** (inbound — how the harness reports events back to spt-core). A harness implementation is one TOML/YAML manifest + a binding from the harness's own hook system into `spt.exe <subcommand>` calls.\r\n\r\n**runtime manifest** (outbound half of the harness contract):\r\nA declarative configuration file (TOML/YAML — schema TBD) that tells spt-core how to drive a specific harness. Declares: how to invoke an agent session, how to look up conversation history for an echo commune, how to spawn/resume a Psyche-equivalent, which binary or command implements each harness-side operation, and which endpoint types this harness supports. spt-core is the actor for each of these; the manifest tells it what to do.\r\n\r\nExample shape (illustrative): `spt.exe --manifest spt-plugin.toml live start <id>`. A harness like the planned spt plugin wraps this invocation into the `$LIVE` / `$OWL` environment variables it injects into its sessions, so harness-internal callers continue to invoke `$LIVE` / `$OWL` unchanged.\r\n\r\n**subcommand surface** (inbound half of the harness contract):\r\nThe stable set of `spt.exe <subcommand>` entry points that harnesses bind their own hook systems to. When the harness's runtime emits an event (subagent started, tool just invoked, user typed `/clear`, session crashed), the harness's hook fires a short-lived `spt.exe <subcommand>` invocation that mutates on-disk SPT state (perch registry, spool, etc.). spt-core publishes this surface; harnesses author the bindings.\r\n\r\n**Naming convention:** these inbound, machinery-facing commands are prefixed **`api `/`api-`** (e.g. `spt api bind`, `spt api state`) to distinguish them from the agent-facing verbs an agent invokes directly (`send`, `ring`, `ready`, …). The `api` namespace is the harness/adapter commands-API; the unprefixed namespace is the agent surface.\r\n\r\nTogether: manifest + subcommand surface = the complete harness API. A sidecar-style long-running adapter process speaking a wire protocol is explicitly **deferred** as a possible v2 alternative for harnesses that outgrow the manifest+hooks shape (e.g. need streaming or in-memory state across events). Not built day-one.\r\n\r\n**adapter manifest header** (`adapter_name` + version compat):\r\nEvery manifest declares a unified **`adapter_name`** (e.g. `claude-spt`), carried on every `api` invocation too. It is load-bearing: one daemon hosts endpoints from multiple adapters, so it resolves an endpoint's manifest + seams by `adapter_name`; adapter-update ripples target by it; capability/manifest lookup and telemetry key on it. The header also declares the adapter's own version and a **`min_spt_core_version`** — the minimum spt-core the adapter requires. This declaration must be **readable before an adapter update is applied** (it lives in the manifest header / a small metadata file fetched first), so spt-core can verify compatibility / expected supported features *before* committing the update. If installed spt-core < the adapter's `min_spt_core_ver…\n\r\n<!-- [doc->REQ-MANIFEST-2] -->\r\n**adapter profile** (ratified 2026-06-11, Gateway grill; future spt-core milestone — first beneficiaries `spt-claude-code` and the usbip shell):\r\nA named **sparse overlay** on its parent adapter manifest. Merge semantics are **leaf-replace**: a profile key replaces the whole value at that path (arrays included — never spliced or appended). The merged result is a complete manifest, and the profile behaves as a distinct adapter option everywhere: canonical addressing is the composite **`<adapter>:<profile>`** (`claude-spt:work`, `spt-usbip-driver:hid-only`) in every place a bare `adapter_name` rides today (perch `info.json`, capability resolution, `api` invocations, `spt adapter list`); the bare name = the parent unmodified. **Two sources, one semantics:** a **shipped profile** is declared inside the parent manifest by the adapter dev and updates as one unit with it; a **local profile** is a node-local …\n_Avoid_: \"manifest fork\", \"child adapter\", per-profile versioning.\r\n\r\n**adapter strings** (ratified 2026-06-11, Gateway grill):\r\n<!-- [doc->REQ-MANIFEST-3] -->\r\nA `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** p…\n<!-- [doc->REQ-MANIFEST-5] -->\r\n**File-backed strings** (M12-W3): a `[strings]` value MAY be a **file pointer** instead of an inline literal — a value-position table with **exactly one** key `file`: `skill = { file = \"skill.md\" }`. `get-string` resolves it to the file's **contents** (so large bodies — skill-instructions, hint text — stay out of the manifest). The exactly-one-key rule is the disambiguation: any other table shape stays an opaque nested strings tree (existing trees untouched), and `{ file = … }` is reserved as the pointer form (it can't double as inline data). Files live in the adapter's per-adapter aux dir **`adapters/<adapter>/strings/`** (sibling of `profiles/`), referenced by a path relative to it that **must stay inside that dir** (HAZARD-class containment: `..` traversa…\n_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); \"adapter KV store\" as a separate registry; putting user files in the adapter-shipped `strings/` dir (clobbered by updates — use a local profile).\r\n\r\n**manifest substitution in `[strings]`** (ratified 2026-06-25, v0.16.0 update-arc grill):\r\n`get-string` resolves a set of **adapter-static** substitution keys inside a returned string value at **read time** (lazily, like file-backed strings): **`{adapter_dir}`** — the registry record's precise `source_dir` (the install dir; survives updates; the same dir bare-program resolution already uses) — and **`{adapter_name}`**. Session-scoped keys (`{id}`/`{session_id}`/…) are **not** available: `get-string` carries no session context today, and a `get-string --session-id` for session-scoped substitution is a deferred, larger change. The load-bearing invariant is preserved: **spt-core still never executes a string** — it substitutes and returns; the *adapter's own wrapper* executes the result. Canonical use: a harness hook dispatcher resolves its own packe…\n_Avoid_: session-scoped substitution through bare `get-string`; reading this as spt-core executing a string (it never does — the adapter wrapper executes the resolved value).\r\n\r\n**keyword hints** (ratified 2026-06-12 — core milestone A):\r\n<!-- [doc->REQ-MANIFEST-4] -->\r\nOnce-per-session usage/syntax hints, a first-class adapter feature: the manifest's `[hints]` section declares entries of `{keywords (literal default, regex opt-in), text}`; the adapter's user-prompt hook pipes the **full user message** to `spt api hint --session <id>` (stdin) and receives matched hint lines (`keyword hint for SPT adapter <name>: \"<kw>\"-->{text}`) for its context-injection channel. The daemon keeps a per-session seen-set — each hint fires **once per session** (a `/clear` mints a new session, naturally re-arming) — and emits at most **one hint per message**. **Tiebreak when a message matches multiple hints:** scan in declaration order and emit the FIRST match whose hint this session has not yet seen; if the declaration-order-first matching hin…\n_Avoid_: unconditional static context (that's the adapter's own preamble); firing per-message.\r\n\r\n**adapter update declaration** (manifest field):\r\n<!-- [doc->REQ-UPD-9] -->\r\nEach adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = \"user/repo\"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed …\n\r\n**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):\r\n<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->\r\nA `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS…\n<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->\r\nAn optional **`[update].message`** (avenue-agnostic) is a plain multi-line operator notice surfaced to stdout, markdown-rendered (the helpfmt prose path), **only when an update is actually applied** (the version changed) — never on a no-op. It is read from the newly-installed manifest with no `{key}` substitution; its use is to announce a post-update action (e.g. \"run `/reload-plugins` in any ongoing sessions\").\r\n\r\n**composite update — `[update.post]`** (ratified 2026-06-25, v0.16.0 update-arc grill; ADR-0029):\r\nAn optional **avenue-agnostic** post-step `{ command, self_verifies }` spt-core runs **after** the primary avenue resolves — in the same `spt adapter update` **and at `spt adapter add`** (install is the first update, so a fresh install conducts the post-step too; bug-#1 operator ruling, v0.19.0 — the eager-extract acquisition runs it post-registration, a delegated acquisition after the acquisition succeeds, and only the payload-less `file_pull` PENDING add defers it to the payload's arrival) — so one lever pulls the adapter `.spt` (`gh_release`) **and** runs a delegated reconcile (e.g. an adapter's `claude plugin update` cross-platform binary). It runs **foreground and bounded** (the subprocess-timeout hazard bound, 120s; never backgrounded — when the CLI re…\n\r\n**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.\r\n_Avoid_: calling the Psyche loop or an on-demand extractor a \"resident\" binary; \"restart the endpoint\" for what is a per-binary cycle.\r\n\r\n**session-invocation declaration** (manifest field, noted for spt-plugin parity):\r\nHow the harness spawns agent sessions, including Psyche and echo-commune sessions. For the rebuilt spt-plugin, Psyche and echo communes must migrate **off `claude -p`** (imminent Claude Code billing changes) to headless `claude` sessions (`--resume` for the Psyche). This is an adapter/manifest concern, not a core concern, but the parity milestone must carry it.\r\n\r\n### Manifest seams (outbound contract, detailed)\r\n\r\nGoverning principle: **SPT is not a harness.** Model choice, billing shape, harness-internal env, and harness-internal context are entirely the adapter's concern, expressed inside the adapter's own command templates. spt-core owns only the template *mechanism* (substitution keys), the substitution *values* it is responsible for, and the surrounding lifecycle. Env for the *endpoint binary itself* is auto-handled by spt-core/broker; env for the *agent running inside* that binary is the adapter's config (e.g. the CC plugin config).\r\n\r\n**spawn-session seam** — launch a new agent session on this node. Manifest provides: a command template; `cwd`/project; a `headless` flag (optional, default false — for the GUI's resume-of-compatible-adapters); a `resume` flag (optional); and the `commune` + `signoff` file directories relative to `cwd` (so the daemon knows where to watch). Substitution keys spt-core can supply: `{id}` and, optionally, a spt-core-generated valid session UUID (e.g. injected as `--session-id {uuid}`) so an adapter can skip the post-spawn seam. spt-core does **not** inject: harness-internal env (broker handles binary env; adapter handles in-session env), and **no initial-context handoff** (not needed at start — the agent is prompted for context once its session is up; the first …\n- **id resolution:** `id` is optional. With no id, spt-core reproduces today's no-id `/spt:live` behavior — run the lone live agent if that's all the project has; show a picker with proposed default IDs if the project has none; let the user choose if there are several.\r\n\r\n**post-spawn seam** — the just-launched binary calls an spt-core command on boot (via the adapter's SessionStart-equivalent hook) to bind itself. Needed because the harness's own session id usually isn't known until after the binary runs. Payload: the harness `session_id` (when binary-generated rather than spt-core-injected); the `parent_pid` (the stable session-binding anchor — see KNOWN-HAZARDS 2.1); an endpoint identity/type confirmation; optionally a local HTTP port the binary listens on (for HTTP-mode input delivery, below); and a **boot nonce** (a generation/boot discriminator so a respawn-after-crash bind can't be confused with a stale duplicate — guards KNOWN-HAZARDS 2.4). The call flips the perch from skeleton → live.\r\n\r\n**post-spawn is optional only under a strict commitment:** an adapter may forgo post-spawn *only* if it (a) injects the spt-core-generated session UUID at spawn AND (b) guarantees the launched-process pid IS the stable session-binding anchor (no wrapper-script / subprocess pid indirection). If either does not hold, post-spawn must fire to report `session_id` and/or `parent_pid`. UUID-injection alone suppresses only the `session_id` reporting, not the binding.\r\n\r\n**spawn-psyche seam** — two command templates: fresh-start and resume (the resume template includes `$session_id`). Both include `$psyche_prompt` — the revival essentials spt-core feeds the Psyche (timestamp, incoming event envelope). Everything else is the adapter's: model selection (in its template), and any harness-specific instructions the Psyche needs (Write-tool usage, commune dir) supplied as a static preamble before `$psyche_prompt` or as adapter SessionStart additionalContext. spt-core owns `$psyche_prompt` content; the adapter owns the rest.\r\n\r\n**history subsystem** (covers echo-commune source logs, resume briefs, and Shell logs) — two supported paths:\r\n- **Path A — adapter-owned logs.** Manifest declares a locate-template (keyed by `$session_id`) + a **normalize-command the adapter owns** that emits spt-core's expected normalized format. spt-core docs must teach adapter devs how to build a conformant parser.\r\n- **Path B — spt-core-native history store.** spt-core exposes a `history-log` command/API; the adapter writes its logs to spt-core in the native format and spt-core stores them. Rationale: spt-core needs its own log store for Shells anyway, and this simplifies integration for flexible/DIY harnesses.\r\n- The **echo-commune seam** is then just a command template (adapter picks the model) that consumes whichever history path is configured for the session.\r\n- **Why adapter-owned normalize over spt-core built-in parsers** (grounded in a Codex-CLI vs Claude-Code comparison): transcript formats diverge sharply and move fast. Claude Code = one flat JSONL per session, project-partitioned, locatable directly from the session id (`~/.claude/projects/<hash>/<id>.jsonl`). Codex = date-partitioned **rollout files** (`~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<id>.jsonl`) where the id is only a filename *substring* (must recursive-glob to locate), a **3-level tagged envelope** (`{timestamp,type,payload}` → tagged `ResponseItem` → tagged `ContentItem` with distinct `input_text`/`output_text`), tool calls as separate top-level items, a **second SQLite index that can desync from the files**, Limited/Extended persistence mode…\n- **Profile-relocated transcript roots — env-read capture** (ratified 2026-06-30, counter-38 field-bug grill; supersedes a rejected harness-specific `{config_dir}` proposal): a Path-A locate-template (and the `[digest].source` that reuses it) may reference a manifest-declared **`[env.<VAR>] direction = \"read\"`** var (e.g. `{CLAUDE_CONFIG_DIR}`). spt-core captures the **declared** read-vars — an explicit allowlist, never the whole env — from the session's launch environment at **bind** (the only point the env is present; the ephemeral `[digest]` extractor runs later in the daemon context where it is gone), persists them in the perch, and substitutes them into the locate-template + the extractor's env at digest time. The var's **fallback** is the `[env]` direc…\n- **`[digest]` mirrors history's two strategies — locate ownership** (ratified 2026-06-30, counter-38 W6 design-gate): like `[history]`, `[digest]` supports **`fetcher`** (the adapter's extractor **locates + reads + emits** normalized digest records; spt-core runs it bounded and consumes its stdout, doing **no** locate and **no** pre-read) alongside the original **`locate_normalize`** (`source` template → spt-core locates a **single** file + reads + pipes the bytes to the extractor as a pure stdin→stdout normalizer). The pre-read `locate_normalize` mode only works when the transcript is a **single fully-templatable path**; a **partitioned** layout — CC's project-slug subdir (`projects/<munge(cwd)>/<id>.jsonl`) or Codex's date-glob (`must recursive-glob to lo…\n\r\n**activity/idle detection** — **not** PTY-quiescence (insufficient: e.g. CC's AskUserQuestion stalls the PTY while holding stdin and needing nuanced input) and **not** a manifest-declared idle signal. Instead, the adapter calls spt-core activity/idle commands at the right moments (from its hooks); those commands manage activity/idle **sentinels inside the session perch**. The idle state lives in the perch, owned by spt-core via the commands API exposed to adapter devs.\r\n\r\n**inject-input seam** — message delivery into a running session. Configurable per activity-state (activity / idle / both); multiple methods, any combination:\r\n- PTY injection (with or without key/submit sequences) — spt-hosted topology;\r\n- adapter hooks calling spt-core poll commands;\r\n- an in-adapter-session child relay (à la CC's Monitor tool);\r\n- adapter manifest requesting HTTP POST delivery to the endpoint binary on a local port (shared via the post-spawn seam).\r\nNote: even spt-hosted sessions default to hook injection (or the adapter's equivalent) as the non-disruptive path **during activity**; some adapters prefer the in-session relay regardless of topology.\r\n\r\n**activity-gated delivery** — an inbound message routes by the receiver's activity sentinel (above). While the endpoint is **active**, the message spools for the receiver's own hook-poll to drain (non-disruptive — the *active window*). On **idle** (or an idle transition before a hook drains it), it delivers immediately — translation binary (spt-hosted) → relay-poll (either topology) → spool, in that fallback order (the *idle window*). The send-side axes below modulate which of these two windows a message is eligible for.\r\n\r\n**message delivery axes** — a sent message carries independent modifiers on orthogonal axes; it is **not** a single \"type\". The flag on each axis defaults to the unrestricted value:\r\n- **delivery window** (*when*) — **default** (both windows; delivers in whichever fires first) · **idle-only** (held for the idle window; delivered immediately if already idle) · **active-only** (active window only — the receiver's hook-poll; never wakes an idle agent). *active-only* is the renamed legacy **deferred** (the `deferred=1` spool column + `api poll --include-deferred` are its internal/adapter-facing names). Mutually exclusive.\r\n- **channel restriction** (*through what*) — **unrestricted** (any configured inject method) · **prefer-native** (the translation binary if one is running, else fall back to the standard methods) · **force-native** (the translation binary and nothing else — no fallback, no spool-to-another-method). Mutually exclusive; composes with the window. *\"Native\"* = the `[message-idle-translation-binary]` PTY channel.\r\n- **persistence** (*how long it waits*) — **durable** (default; spooled until delivered or TTL) · **ephemeral** (dropped if it cannot deliver in its accepted window — at the moment the window opens with no live carrier, or at TTL, whichever is first). Ephemeral is the **only** path permitted to drop silently (the REQ-HAZARD-IDLE-SILENT-NONDELIVERY carve-out); every non-ephemeral path spools and reports non-delivery. <!-- v0.15.0 PARTIAL (W3): ephemeral evaporation covers the spt-hosted-binary no-carrier-at-window leg (the idle-transition drain drops ephemeral rows the binary cannot take) + the TTL leg (purge). The harness-hosted relay \"window opens with no live *listener*\" leg is NOT yet delivered — it needs relay carrier-presence detection (same separate-co…\nWindow restricts *when* delivery is accepted, channel restricts *which method* carries it, persistence restricts *how long* it waits — they compose freely (e.g. `force-native` + `active-only` = the binary injects during the active window, never idle; `force-native` + `ephemeral` = binary-or-nothing).\r\n\r\n**message metadata (`json`)** — a sender may attach an opaque JSON metadata block (`--json-payload`), carried as a single attr-escaped `json=\"…\"` envelope attribute **alongside** (never replacing) the body. spt-core never interprets it — pure verbatim passthrough across every rail (spool / TCP / WAN / EVENT-PART), parsed only by the receiving adapter (its hooks and/or translation binary). Collision-proof by construction: the structured data lives **inside** the single `json` value, so it can never forge spt-core's control/identity attributes (`from`, `type`, …). Available to any sender — it confers no spt-core authority; what a custom field *means* is the receiving adapter's trust decision (the same posture as `from`-is-never-payload-trusted).\r\n\r\n**resume-session seam** — two distinct forms:\r\n- **fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> That command is **`spt api psyche-download <id> [--session-id <sid>]`**: it emits the durable resume brief (role → live-context → project-context, project resolved from the perch's bound cwd) to stdout for the adapter's SessionStart hook to inject as additional context, and APPENDS any **not-yet-synthesized** commune/signoff drop as a `<pending-commune>`/`<pending-signoff>` slice AFTER the durable tiers — closing the window where a just-dropped commune…\n- **continue-existing:** resume an existing harness session under the adapter (its native resume).\r\n\r\n**capability declaration** — which endpoint types a harness/node can host (a Pi node might host only Shells, never a LiveAgent). Static manifest list, consumed by the subnet registry so a node advertises its hostable types. Exact shape is design-open.\r\n\r\n**adapter-update seam** — file-pull or delegated-command (see Self-update). Locked.\r\n\r\nThere is no separate \"model/billing\" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model. The full manifest schema is `docs/MANIFEST.md`; key model-level facts from it:\r\n\r\n- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.\r\n- **A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github/<safe>/` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binaries is **self-contained**, needing no PATH placement. <!-- [doc->REQ-INSTALL-11] --> Applies to the `[digest]` extractor, the `[session.psyche_init]` runner, and the `adapter digest-proof` tool; the install dir is the registry record's `source_dir` (precise) for the daemon-resolved paths — the `[digest]` extractor and the daemon-h…\n- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.\r\n- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.\r\n- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs` — its own adapter, not a binary-swap). <!-- [doc->REQ-MANIFEST-6] --> A fallback **target is addressed as `<adapter>:<profile>`** (not just a bare adapter_name) and resolves through the one composite-addressing resolver (`registry::resolve_option`), so a fallback may select a shipped or local profile (`ccs`, `ccs:<profile>`) exactly as any other adapter-option read site does. *Contract only at M12-W3 — the addressing resolves; the node-wide setting + its rate-limit invocation belong to the consuming milestone (no reader exists yet, so no config field is added).* Adapte…\n- **Config knobs** (pulse period, echo-commune window, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override** — never per-adapter.\r\n- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> This includes the **checkpoint sentinel `!!checkpoint!!`** — the agent-checkpoint trigger an adapter embeds in a commune/signoff drop body (one bare token = checkpoint with default wake; a `!!checkpoint!! <text> !!checkpoint!!` pair makes the inter-marker text a custom wake directive). It is spt-core control metadata: spt-core STRIPS every occurrence (keeping the inter-marker text) before the drop body reaches agent context, at BOTH points it can — the resume `<pending-*>` presentation (pre-synthesis) and the durable tier write (post-synthesis) — so the marker never surfaces or …\n\r\n### Inbound `api` surface (detailed)\r\n\r\nAll commands below are `api`-prefixed (machinery-facing). Every `api` invocation **and** every manifest carries a unified **`adapter_name`** string (e.g. `claude-spt`) identifying the owning adapter. This is load-bearing: one daemon hosts endpoints from multiple adapters (`claude-spt`, `spt-codex`, `spt-pi`), so the daemon resolves an endpoint's manifest + seams (history normalize-command, inject method, update avenue) by its `adapter_name`; adapter-update ripples target by it; capability lookup and telemetry key on it.\r\n\r\n<!-- [doc->REQ-API-4] -->\r\n**Manifest resolution from `--adapter` (since v0.8.0).** `spt api <cmd> --adapter <name[:profile]>` resolves the registered adapter's manifest, `:profile` overlay, and install dir from the registry when `--manifest` is omitted — a registered adapter's `api` calls need only `--adapter`. `--manifest <path>` becomes an optional **override** (an unregistered or local-dev manifest): when present, the manifest loads from that file and the install dir is its parent directory; when absent, both come from the registry record (the install dir is the record's precise `source_dir`). An unregistered adapter with no `--manifest` degrades to no-manifest rather than failing.\r\n\r\n- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.\r\n- **`api listen`** — *long-running* relay/poll listener that an adapter-owned (harness-hosted) session owns as a child process; streams the daemon's events to the session's stdout. Distinct from the short-lived `api poll`. This is the heir to today's Monitor-bound `$LIVE start` poll loop.\r\n- **`api poll`** — short-lived drain of queued messages for a session (the hook-injection delivery path). `--include-deferred` optionally also drains deferred rows, for adapter flexibility (default excludes them — KNOWN-HAZARDS 1.4/4.4).\r\n- **`api state <busy|idle>`** — adapter reports session activity; writes the activity/idle sentinel in the session perch. By default `api state idle` also writes the **echo-commune gate sentinel** (`.more-done`-equiv — modern spt couples them); `--no-gate` suppresses that coupling, and a standalone **`api echo-gate <set|clear>`** gives granular adapters explicit control over when echo communes may fire, independent of idle.\r\n- **`api worker-start`** / **`api worker-stop`** — Worker (subagent) perch create/teardown under the parent (nested, registry-tracked).\r\n- **`api worker-poll`** — a Worker (subagent) receives its queued messages (inbound from Self or sibling Workers).\r\n- **`api boundary <clear|compact>`** — context-boundary report; **carries the new `session_id`** (it rotates on `/clear` or `/compact`), so the daemon rebinds the perch to the new session id while keeping the stable identity + `parent_pid` anchor. Authors a **Self-resume commune** (resume the Self session → commune file-drop) rather than a background echo — strong live-context signal at the boundary (see `docs/CONTEXT-MEMORY.md`). **Rotation credential** (ADR-0032): the proof of association for this one verb belongs to the **departed** session (its sid, or the perch token) — the new sid is the *payload*, never the *proof* — so adapters persist the current sid across the rotation (endpoint-keyed adapter state, NOT per-session env) and present it; the design-t…\n- **`api session-end`** — session stop/crash report → soft teardown by default (preserve perch + spool + tracked history for recovery — KNOWN-HAZARDS 6.2). **`--erase`** instead hard-wipes the perch and tracked history (for ephemeral/secondary adapters that act as robust agent-spawned-agent surfaces).\r\n\r\n**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running host would let the daemon re-create or re-host mid-purge); **`--force`** stops it first (→ the daemon reconcile un-hosts it and reaps its Psyche) and then purges. **`--yes`** skips the interactive confirm (the CI path); purge refuses removing the **caller's own run…\n_Avoid_: consent-gating it (it is intentionally ungated, for CI); treating it as a sync/remote op (local-only); a soft variant (purge is always the hard, full wipe — soft teardown is `endpoint stop`).\r\n<!-- [doc->REQ-ENDPOINT-PURGE] -->\r\n\r\n- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.\r\n- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).\r\n- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.\r\n\r\n**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike.\r\n\r\n### Startup flows (the two topologies)\r\n\r\n**Adapters never resolve `$SPT_HOME`.** spt-core install registers its binary directory on the system-wide PATH, so adapters call `spt api …` on any OS without path math. All harness↔daemon bridging goes through `spt api` commands (the daemon is always running, or auto-started — below), so there is **no adapter-written file** in the bind path.\r\n\r\n**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent).** Key constraint: the SPT *live agent* does not exist until the agent invokes start — the `live_id` isn't chosen at session boot, and `$LIVE start` is itself invoked *behind the Monitor tool*, so it becomes the long-running relay. So binding cannot happen at SessionStart directly. A **seed record** (daemon-held, in-memory — not a file) bridges the gap:\r\n1. The harness's SessionStart hook calls **`spt api seed --pid <parent_pid> --session-id <sid> [cwd]`**. The daemon records an ephemeral in-memory **seed entry** keyed by `parent_pid` — the session details the spt-hosted topology would share directly, minus the not-yet-chosen `live_id`. The seed is **adapter-agnostic**: it carries no `adapter_name`. <!-- [doc->REQ-START-5] --> *Which* adapter/profile a session belongs to is resolved later, at bind, as a read against the live registry (below) — so one SessionStart hook seeds correctly no matter which harness adapters are installed, and an `adapter add` after the seed is never missed. In-memory (not a file) avoids drive churn and the `$SPT_HOME` resolution nuisance; seeds are consumed within seconds, so persis…\n2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. It self-discovers its `parent_pid`, the daemon matches the seed entry by that pid (validated against `session_id` to defeat PID-recycling — KNOWN-HAZARDS 5.1), **resolves the owning adapter/profile** (the bind-time resolution below), creates/revives the perch binding `live_id` ↔ session details, then enters the long-running relay loop streaming events to stdout.\r\n3. The always-on daemon holds the perch, spool, registry, and daemon-spawns the Psyche (via the spawn-psyche seam) — no separate wrapper. The relay is purely the delivery pipe.\r\n   - Seed entry refreshed on each SessionStart (keeps `session_id` current across `/clear`, since `parent_pid` is stable while the harness process persists). If a harness has no SessionStart-equiv, `start` may carry the details directly as args — the seed is the preferred convenience, not the only path.\r\n   - The same seed + bind-time resolution serves a **ReadyAgent** bringup (`$SPT ready`/poll), not just a LiveAgent — a harness-hosted ready agent is seeded and resolved identically (it just binds a poll listener, no Psyche).\r\n\r\n**Bind-time adapter/profile resolution (ADR-0021).** Because the seed is adapter-agnostic, `listen`/`poll` resolve the owning adapter/profile when they bind, as a pure read — never a seed-time snapshot that could drift. `--adapter <name[:profile]>` is an **optional override** on the `api` group (an explicit choice for adapter dev/iteration); omitted, resolution runs:\r\n1. the seed's `parent_pid` → that process's **executable basename** (case-insensitive, `.exe`-stripped);\r\n2. **candidate adapters** = registered `kind=\"harness\"` adapters whose **`host_binaries`** (the manifest match-key) contains that basename; <!-- [doc->REQ-MANIFEST-8] -->\r\n3. **profile**: the durable **active-profile pointer** (`spt adapter use <adapter>[:profile]` writes it; one default per `host_binary`) wins; unset → the freshest candidate adapter by `registered_at_ms`, base profile (a specific profile is only ever chosen by the pointer), name-ascending on ties; <!-- [doc->REQ-INSTALL-12] -->\r\n4. zero candidates → a friendly error naming the binary and the `--adapter` escape. The pointer is a standing user preference (durable on disk, never auto-written by install/update); the seed is ephemeral — see ADR-0021.\r\n\r\n**Daemon auto-start:** the daemon is per-machine always-on (OS-service registered), but any `spt api` invocation that needs it will **start it if absent** (fresh boot, crash, never-installed-as-service). `$SPT listen` for the first SPT session on a machine thus transparently spins up the daemon. Ensure-running lives in the `api` layer generally; `listen` is the reliable anchor.\r\n\r\n**spt-hosted (terminal wrapper / GUI launcher; the daemon launches the binary into a broker PTY):**\r\n1. The frontend/CLI launches the agent: the daemon runs the **spawn-session** command template into a broker-held PTY.\r\n2. The binary boots and fires **`api bind`** (or skips it under the strict UUID-injection + stable-pid commitment). **No catalyst/seed file** — the daemon is the launcher, already holds a direct channel (it spawned the process and owns the PTY), so a file round-trip would only add drive churn for no benefit.\r\n3. The daemon delivers events; method is **manifest-configurable per activity-state** — direct PTY injection, or a relay even here (some adapters prefer a relay over PTY injection for idle delivery), or HTTP. During *activity*, delivery still defaults to the non-disruptive hook-injection path, not raw PTY writes.\r\n4. Psyche is daemon-spawned, same as above.\r\n\r\nSo the old `$LIVE start` splits by topology: harness-hosted = SessionStart writes an adapter-agnostic seed → `$SPT listen <id>` consumes seed (by `parent_pid`) + resolves adapter/profile (ADR-0021) + binds + relays — legacy parity (`$LIVE start <id>` → `$SPT listen <id>`, no mandatory `--adapter`); spt-hosted = daemon spawn-session + `api bind` (direct, no file). The asymmetry is the file-bridge-only-when-no-direct-channel principle.\r\n\r\n**Env-var aliases:** adapters inject clean env-var aliases for in-session invocation (heirs to today's `$OWL`/`$LIVE`), e.g. **`$SPT` = `spt api`** so a Monitor-bound call reads `$SPT listen <id>`. spt-core supplies the subcommands; the adapter supplies the env aliases (manifest philosophy).\r\n\r\n### Endpoint types\r\n\r\nEach perch advertises an **endpoint type** — a tag that says what shape of entity lives at that perch and what operations it accepts. The set of day-one types:\r\n\r\n**ReadyAgent**:\r\nMinimal SPT participant — a perch + a poll listener, no Psyche, no live-agent wrapper. Direct heir to the sister project's \"ready agent\".\r\n\r\n**LiveAgent**:\r\nA Self with a Psyche companion. Composite logical actor; addressable as one ID, but its component perches (the Self's, the Psyche's) live independently. Direct heir to the sister project's \"live agent\".\r\n\r\n**Psyche**:\r\nThe Psyche companion's own perch, distinct from its paired LiveAgent's perch. First-class endpoint type so messages addressed to a LiveAgent's Psyche route directly without ambiguity. **A Psyche is a bounded per-event turn, not a resident process (since v0.25.0).** Each psyche-relevant event (a pulse fire, a commune/signoff drop, a session-custody transition) runs **exactly one** bounded turn through the psyche role template, spawned by the daemon, which exits at turn end — there is no long-lived psyche loop or psyche pid between events. <!-- [doc->REQ-PSYCHE-EPHEMERAL-DRIVER] --> **Liveness = turns succeed** — never a PID or a resident-process check. A Self perch is online-and-hosted whether or not any psyche turn is in flight; a psyche turn failure of any …\n\r\n*I/O & trust boundary (ADR-0012):* the Psyche is a **sandboxed** actor — it may read and write files but **cannot send messages or reach the network itself**. Its inbound context arrives two ways: events/messages the daemon hands it, and **commune/signoff file-drops** (Self → daemon → Psyche; the *Summarizer* authors the commune delta). Its **sole outbound** is **reply/notify intents** the daemon relays as its **outbound proxy** — emitted as `<EVENT type=\"reply\">`/`<EVENT type=\"notify\">` (the shared envelope grammar). A *reply* reaches **only the sender it answers**; a *notify* reaches **only the agent's own user** — the Psyche carries no target and cannot address arbitrary endpoints (the daemon strips/re-stamps `from=` before relaying).\r\n\r\n*Psyche-host health — harness-reachable failure signal (v0.8.1, REQ-HAZARD-LIVEHOST-BOOT-RACE):* a LiveAgent's `status=online` is daemon-authoritative liveness and **stays authoritative** — but it does not by itself prove the daemon hosted a Psyche. When the brain's live-host reconcile fails to spawn the Psyche (e.g. the adapter's psyche binary is absent from its install dir, or the net-less boot-race starves the host), that failure was previously **silent** — only an `eprintln!` on the brain's invisible stderr, while a harness (and a human via `spt endpoint list` / `whoami`) reads **perch state**, never brain stderr. The Self perch's `info.json` therefore carries an additive, N-1-safe `psyche_host_error` field (`{reason, ts, attempts}`): a **current-state**…\n\r\n**Summarizer**:\r\nThe ephemeral, cheap model that builds a **commune delta** from a Self's recent turns and feeds it *into* the **Psyche** as inbound context. A distinct actor from the Psyche — different (cheaper) model, fire-and-forget, **no perch** (not an endpoint type). It authors *commune* deltas only, **never** *reply*/*notify*.\r\n_Avoid_: conflating with the Psyche; \"echo-commune model\".\r\n\r\n**Worker**:\r\nA subagent's perch under a parent LiveAgent. Created on subagent start, torn down on subagent stop. Replaces today's \"working perch\" concept; first-class type so cross-communication between a Self and its workers (and worker↔worker) is addressable.\r\n\r\n**SptNode**:\r\nA machine's participation in an SPT subnet, identified by an Ed25519 public key generated on first run. First-class so networking primitives can address nodes directly as message targets, not only as transport peers. The node identity and network endpoint are hosted by the machine's `spt-daemon` (see Networking), not a separate process.\r\n\r\n<!-- [doc->REQ-EP-6] -->\r\n**Gateway** (concept ratified 2026-06-11; registered via the open type system, first instance downstream):\r\nA **human-backed endpoint** — a user's specialized window into the subnet from a device or surface with no conventional-harness compatibility. Nothing LLM-shaped runs there; the intelligence at the endpoint is the **user**. Addressable like any endpoint (receives digests/messages, sends via the normal verbs) and may **own Shells** (it is an owning endpoint — see §Shell model). Distinct from a Shell: a Shell is *driven from elsewhere*; a Gateway *originates* interaction. No `tracked/` mind, no Psyche (LiveAgent affordances). First instance: the `spt-lecturn` adapter's Playdate endpoint (own repo).\r\n\r\n<!-- [doc->REQ-MSG-5] -->\r\nA message sent from a Gateway carries **the user's authority** — it *is* the user speaking through a device — and is delivered typed **`user-msg`** (ratified 2026-06-12) so receiving agents weight it as user instruction, not peer-agent chatter. The type is **identity-gated, never payload-trusted** (the KH 7.3/7.5 posture): the daemon permits `user-msg` only from user-backed origins (a Gateway endpoint, the local user's own CLI) and re-stamps an agent-family sender's `user-msg` down to plain `msg` — authority comes from who you are, not what you wrote.\r\n\r\n<!-- [doc->REQ-MSG-6] -->\r\n_Implemented posture_: the **local** user-backed origins are honored end-to-end — a locally-hosted Gateway endpoint (info.json `state=\"gateway\"`) and the local user's CLI (M9-T4/T5). The **cross-node WAN** path is being completed (trust posture **ratified 2026-06-13**): the **subnet membership boundary is the trust boundary**. A subnet is a collection of machines the user already trusts, so a `user-msg` arriving over the subnet from a **Gateway-typed** origin is honored as the user's authority; the daemon does **not** defend against a subnet member *forging* the Gateway type — an in-subnet compromise is out of scope by construction (if the subnet is breached at all, the trust model is already void). The origin's type is read from its advertised registry **`e…\n\r\nA Gateway endpoint binary is revived by **existing machinery only** (settled 2026-06-12, two corrections deep): while running, the bridged device's link liveness drives ordinary **instance state** (sustained device silence → dormant; device contact → active — the driver-attach rule). Across a node restart, revival rides a **co-located shell's wake-watcher** — the Gateway typically owns a shell instance on its own gateway host; that shell's offline wake-watcher (one of the two classes of third-party binary spt-core boot-launches — the other is the [[AlwaysOnEndpoint]] resident binary) holds the device-contact surface and fires the standard **wake resolution** (\"owner suspended → revive the owner\"). No Gateway-manifest watcher, no autostart flag, no new mechan…\n_Avoid_: calling a Gateway a Shell or an agent; \"console\", \"remote\".\r\n\r\n**PresenceChannel** (broker endpoint — concept locked, impl deferred past v1):\r\nA *broker* endpoint, not an interaction surface. Job: (1) **presence resolution** — track which node + endpoint the user most recently interacted with; (2) **shell brokering** — locate/instantiate the right Shell on that node and relay between the agent and the user. An agent \"just knows how to reach the user\" by firing at its PresenceChannel; the channel figures out the rest. Also a durable, **shell-agnostic 2-way thread**: messages persist in the channel, not in any one Shell, so the user can be sent a message via a phone messaging-Shell and surface/continue that same agent conversation later at a GameRobot Shell. Shells are interchangeable I/O windows onto the channel's thread.\r\n\r\nThree interaction styles:\r\n- **dispatch** — fire-and-forget: \"reach the user with this payload\"; channel delivers via the best available Shell.\r\n- **bind** — sustained drive: \"give me a Shell of capability X\"; channel instantiates and the agent drives it directly until teardown. Supports operating a *specific* Shell regardless of where the user currently is (agent transience).\r\n- **thread** — the persistent conversation that floats across Shells; the user can pick it up from any Shell, and 2-way payloads (text/audio/image/video, subject to the Shell's supported types) flow both directions.\r\n\r\nPresence datum: `(last_active_node, last_active_endpoint, timestamp)`. The `last_active_endpoint` field lets an agent choose between messaging that specific endpoint vs. driving a parallel instance of itself.\r\n\r\n**AlwaysOnEndpoint** (always-on endpoint; concept ratified 2026-06-21 — core kind, first instance downstream `spt-discord`):\r\nA **resident, addressable endpoint that hosts no mind.** Its binary is daemon-**supervised continuously and runs independent of any agent's liveness** (up even when zero agents are online) — unlike an *agent endpoint* (a hosted mind with a Psyche + `tracked/` context) and unlike a **Shell** (single-owner, *driven*). It is **two-way addressable**: agents message it (to drive whatever external surface it fronts) and it messages out — notably it may call `endpoint wake <id>` to draw an offline agent online (wake authorization is **target-side**, so no special caller right is needed — see the wake-watcher/sleep-wake model). Declared by an adapter's **`[always-on]`** manifest section; the supervised binary is **one per adapter-option** (`<adapter>[:profile]`), an…\n_Avoid_: calling it a Shell (owner-less + not driven) or an agent (no mind); \"service\" alone (it is addressable, not faceless); a sleep/wake resting model (it does not rest).\r\n\r\n**instance state (active / dormant / suspended / offline)**:\r\nThe four liveness states a per-endpoint registry row (`registry::Status`) advertises across the subnet. The **active/dormant pair is the multi-instance routing differentiator**: an endpoint may run on several nodes at once (cross-node context sync) — the **active** instance is the bare-`id` routing target, its live siblings are **dormant**. The canonical meaning (the resting state machine, `resting.rs`, implements active/dormant/suspended; offline is registry-only):\r\n…\n\n…\n\n…\nThe CLI sibling of the frontend's guided resume — one command that lists endpoints **grouped by locality, most-recently-used within each group**: `on-node / current-project → on-node / other-project → off-node`, mirroring the *resolution policy*'s local-first preference. Selection **chains conditionally**: a **running** instance → attach/tap-in (no adapter step — already live under one); a **non-running** endpoint → into the **adapter selector** (*adapter selection*: history head = default → prior adapters → \"choose a different adapter\") → *home subnet* / other creation prompts as needed → launch; a **\"+ new endpoint\"** entry → the full creation flow. Off-node picks respect the reach + consent gates (remote-drive of your own running instance is ungated; a co…","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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,null,294,null,461,null,882]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\CONTEXT.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":883,"totalBytes":69553,"outputLines":283,"outputBytes":69553,"shownRange":{"start":1,"end":283},"nextOffset":284},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195752997}}
{"type":"message","id":"64169b6b","parentId":"61d72253","timestamp":"2026-07-16T09:55:53.000Z","message":{"role":"toolResult","toolCallId":"call_dcCMiQIhh8QexYXiedVEE8yG|fc_008390f508d1d09b016a58aaa8e484819aae0e765a82272612","toolName":"grep","content":[{"type":"text","text":"# crates/spt/\n\n## src/\n### cli.rs\n 184|    /// the degenerate single-node case of the cross-node attach (one pump,\n*185|    /// loopback peer). Detach with the **ctrl-b** prefix then `d`\n 186|    /// (`ctrl-b ctrl-b` sends a literal ctrl-b); detaching leaves the session\n 187|    /// running on the broker. `--view` watches read-only.\n 188|    Rc {\n...\n 1128|    /// within one pump cadence; `spt subnet attach` reverses it.\n*1129|    Detach {\n 1130|        /// The held subnet to stop serving.\n 1131|        name: String,\n 1132|        /// Also persist as the startup default (survives daemon restarts).\n...\n 1330|                }) => cmd_subnet_join(name, code, verbose),\n*1331|                Some(SubnetCmd::Detach { name, save }) => cmd_subnet_attachment(&name, true, save),\n 1332|                Some(SubnetCmd::Attach { name, save }) => cmd_subnet_attachment(&name, false, save),\n 1333|                Some(SubnetCmd::Leave { name }) => cmd_subnet_leave(&name),\n 1334|                Some(SubnetCmd::Prune { node }) => cmd_subnet_prune(&node),\n...\n 1714|/// makes `--view`/`--take` mutually exclusive, so at most one is set.\n*1715|fn rc_intent(view: bool, take: bool) -> spt_net::net::attach::AttachIntent {\n*1716|    use spt_net::net::attach::AttachIntent;\n 1717|    if view {\n*1718|        AttachIntent::Viewer\n 1719|    } else if take {\n*1720|        AttachIntent::Take\n 1721|    } else {\n*1722|        AttachIntent::Control\n 1723|    }\n 1724|}\n 1725|\n...\n 16931|            subnet_cmd(&[\"spt\", \"subnet\", \"detach\", \"home\"]),\n*16932|            Some(SubnetCmd::Detach { name, save: false }) if name == \"home\"\n 16933|        ));\n 16934|        assert!(matches!(\n 16935|            subnet_cmd(&[\"spt\", \"subnet\", \"attach\", \"home\", \"--save\"]),\n### rc.rs\n 12|//!\n*13|//! **Detach never kills the session.** The broker owns the PTY; closing our send\n 14|//! side ends the viewport with the session still running (`serve_attach` →\n 15|//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS\n 16|//! attach-lifecycle invariant).\n...\n 32|use spt_daemon::msg::decode_bytes;\n*33|use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\n 34|\n 35|/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the\n 36|/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one\n...\n 50|    /// The operator hit the detach keybind — end the viewport, leave the session.\n*51|    Detach,\n 52|}\n 53|\n 54|/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET\n...\n 610|    /// The operator hit `ctrl-b d` — end the viewport.\n*611|    Detach,\n 612|    /// Bytes to forward to the session (empty ⇒ nothing to send).\n 613|    Forward(Vec<u8>),\n 614|    /// Saw the lone `ctrl-b` prefix — armed; nothing to forward yet.\n...\n 619|/// [`parse_stdin_chunk`] (the byte path) so both inputs share IDENTICAL detach\n*620|/// semantics (doyle ruling): Ctrl+B arms; armed + plain `'d'` ⇒ Detach; armed +\n 621|/// Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` then the translated event. The\n 622|/// detach key is PLAIN `'d'` only (a Ctrl+D stays prefix + `0x04`, matching the byte\n 623|/// SM's bare-`DETACH_KEY`). Pure given `armed`, so it unit-tests without a console.\n...\n 632|        if ke.code == KeyCode::Char('d') && ke.modifiers.is_empty() {\n*633|            return KeyAction::Detach;\n 634|        }\n 635|        if is_ctrl_b {\n 636|            return KeyAction::Forward(vec![DETACH_PREFIX]); // literal ctrl-b\n...\n 962|/// raw byte-read path: Unix already delivers proper VT, and the e2e byte-injection\n*963|/// relies on it. Detach is the `ctrl-b d` prefix on BOTH paths.\n 964|fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {\n 965|    #[cfg(windows)]\n 966|    {\n...\n 1000|            if parsed.detach {\n*1001|                let _ = tx.send(StdinMsg::Detach);\n 1002|                return;\n 1003|            }\n 1004|        }\n...\n 1051|                    match key_event_step(&mut armed, ke) {\n*1052|                        KeyAction::Detach => {\n*1053|                            let _ = tx.send(StdinMsg::Detach);\n 1054|                            return;\n 1055|                        }\n 1056|                        KeyAction::Forward(bytes) => {\n...\n 1081|// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\n*1082|pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n 1083|    run_attach_inner(endpoint_id, intent, false)\n 1084|}\n 1085|\n...\n 1097|    endpoint_id: &str,\n*1098|    intent: AttachIntent,\n 1099|) -> Result<(), String> {\n 1100|    run_attach_inner(endpoint_id, intent, true)\n 1101|}\n...\n 1104|    endpoint_id: &str,\n*1105|    intent: AttachIntent,\n 1106|    session_confirmed: bool,\n 1107|) -> Result<(), String> {\n*1108|    let view = intent == AttachIntent::Viewer;\n 1109|    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\n 1110|    // an endpoint a DIFFERENT node already controls gets actionable guidance here\n 1111|    // (the broker would also refuse on the wire — this is the friendly path). A\n 1112|    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\n*1113|    if intent == AttachIntent::Control {\n 1114|        if let Some(node) = current_driver(endpoint_id) {\n 1115|            println!(\"'{endpoint_id}' is controlled by {node}.\");\n 1116|            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\n...\n 1388|// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\n*1389|fn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n 1390|    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n 1391|    // launcher. It must NEVER auto-start the broker — the historical\n 1392|    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n...\n 1520|    endpoint_id: &str,\n*1521|    intent: AttachIntent,\n 1522|    view: bool,\n 1523|    mut est: Established,\n 1524|    rx: Option<&mpsc::Receiver<StdinMsg>>,\n...\n 1591|                while let Ok(msg) = rx.try_recv() {\n*1592|                    if matches!(msg, StdinMsg::Detach) {\n 1593|                        detached = true;\n 1594|                    }\n 1595|                }\n...\n 1731|    /// conn dropped (node bounce, network cut), NOT an operator detach (that\n*1732|    /// returns via the stdin `Detach` path directly) and NOT a session end (an\n 1733|    /// `Exit` record precedes a session-death EOF). Pre-B-3 this was mislabeled\n 1734|    /// \"detached — still running\"; it is the SEVERED case the reconnect loop\n 1735|    /// re-drives. [impl->REQ-RC-RECONNECT]\n...\n 1904|                    }\n*1905|                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\n 1906|                    Err(mpsc::TryRecvError::Empty) => break,\n 1907|                    Err(mpsc::TryRecvError::Disconnected) => {\n 1908|                        return Ok(detach(brain, stream_id))\n\n### picker/\n#### model.rs\n 779|        id: String,\n*780|        intent: spt_net::net::attach::AttachIntent,\n 781|    },\n 782|    /// Write/update a `spt-<id>` shortcut, then stay (re-enter the loop).\n 783|    Shortcut {\n...\n 1343|        };\n*1344|        use spt_net::net::attach::AttachIntent;\n 1345|        match opt {\n 1346|            // An online endpoint's PTY is live → attach the rc pump; an offline\n 1347|            // one is brought up then ATTACHED by default (W4 UX: Start-now lands you\n...\n 1351|            ConfirmOption::Attach | ConfirmOption::Start => Some(if online {\n*1352|                Outcome::Attach { id: ep.id.clone(), intent: AttachIntent::Control }\n 1353|            } else {\n 1354|                bringup(RunMode::Attach)\n 1355|            }),\n 1356|            ConfirmOption::View => Some(if online {\n*1357|                Outcome::Attach { id: ep.id.clone(), intent: AttachIntent::Viewer }\n 1358|            } else {\n 1359|                bringup(RunMode::View)\n 1360|            }),\n...\n 1363|                id: ep.id.clone(),\n*1364|                intent: AttachIntent::Take,\n 1365|            }),\n 1366|            // C-2: wake a REMOTE suspended endpoint through its owning node's rest\n 1367|            // edge — carries the RAW node hex (node_key) so the dispatch qualifies\n...\n 2558|        m.enter_pick();\n*2559|        use spt_net::net::attach::AttachIntent;\n 2560|        assert_eq!(\n 2561|            m.confirm_terminal(ConfirmOption::Attach),\n*2562|            Some(Outcome::Attach { id: \"live\".into(), intent: AttachIntent::Control })\n 2563|        );\n 2564|        assert_eq!(\n 2565|            m.confirm_terminal(ConfirmOption::View),\n*2566|            Some(Outcome::Attach { id: \"live\".into(), intent: AttachIntent::Viewer })\n 2567|        );\n 2568|        match m.confirm_terminal(ConfirmOption::Shortcut).unwrap() {\n 2569|            Outcome::Shortcut { adapter, id, .. } => {\n\n## tests/\n### resume_no_control_steal_e2e.rs\n 13|//! `resume_sessions()` at boot, which re-attached EVERY session `KIND_SESSIONS` returns as\n*14|//! `subscribe_with(AttachIntent::Control, by:None)`. The docstring's \"a None identity never\n 15|//! displaces (falls back to viewer)\" is a MYTH for FREE / SAME-LOCAL-IDENTITY slots:\n 16|//! `resolve_subscribe` (broker.rs) stall-evicts first, then `become_controller` when the\n 17|//! slot is free OR the incumbent is also local (`None == None`) — viewer-fallback fires\n...\n 48|//!\n*49|//! `resume_sessions` (still used by DRAINING callers) re-attaches as **`AttachIntent::Viewer`**\n 50|//! (a viewer never owns `driven_by` and is never stall-evicted). The RED→GREEN discriminator\n 51|//! this rig asserts is\n 52|//! the STEAL itself — the deterministic ROOT that CAUSES both A and B — observed as OUTPUT\n...\n 483|    // swap — each A_i went on receiving output after the respawn. RED (resume re-attaches\n*484|    // AttachIntent::Control): resolve_subscribe same_identity(None==None) become_controller'd\n 485|    // each slot, dropping A_i → its tally froze → gained==0. GREEN (Viewer): A_i untouched.\n 486|    // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n 487|    for (i, g) in gained.iter().enumerate() {"}],"details":{"scopePath":"crates/spt/src, crates/spt/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":40,"fileCount":4,"files":["crates/spt/src/cli.rs","crates/spt/src/picker/model.rs","crates/spt/src/rc.rs","crates/spt/tests/resume_no_control_steal_e2e.rs"],"fileMatches":[{"path":"crates/spt/src/cli.rs","count":9},{"path":"crates/spt/src/picker/model.rs","count":8},{"path":"crates/spt/src/rc.rs","count":20},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","count":3}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt/\n\n## src/\n### cli.rs\n   184│    /// the degenerate single-node case of the cross-node attach (one pump,\n  *185│    /// loopback peer). Detach with the **ctrl-b** prefix then `d`\n   186│    /// (`ctrl-b ctrl-b` sends a literal ctrl-b); detaching leaves the session\n   187│    /// running on the broker. `--view` watches read-only.\n   188│    Rc {\n      │...\n  1128│    /// within one pump cadence; `spt subnet attach` reverses it.\n *1129│    Detach {\n  1130│        /// The held subnet to stop serving.\n  1131│        name: String,\n  1132│        /// Also persist as the startup default (survives daemon restarts).\n      │...\n  1330│                }) => cmd_subnet_join(name, code, verbose),\n *1331│                Some(SubnetCmd::Detach { name, save }) => cmd_subnet_attachment(&name, true, save),\n  1332│                Some(SubnetCmd::Attach { name, save }) => cmd_subnet_attachment(&name, false, save),\n  1333│                Some(SubnetCmd::Leave { name }) => cmd_subnet_leave(&name),\n  1334│                Some(SubnetCmd::Prune { node }) => cmd_subnet_prune(&node),\n      │...\n  1714│/// makes `--view`/`--take` mutually exclusive, so at most one is set.\n *1715│fn rc_intent(view: bool, take: bool) -> spt_net::net::attach::AttachIntent {\n *1716│    use spt_net::net::attach::AttachIntent;\n  1717│    if view {\n *1718│        AttachIntent::Viewer\n  1719│    } else if take {\n *1720│        AttachIntent::Take\n  1721│    } else {\n *1722│        AttachIntent::Control\n  1723│    }\n  1724│}\n  1725│\n      │...\n 16931│            subnet_cmd(&[\"spt\", \"subnet\", \"detach\", \"home\"]),\n*16932│            Some(SubnetCmd::Detach { name, save: false }) if name == \"home\"\n 16933│        ));\n 16934│        assert!(matches!(\n 16935│            subnet_cmd(&[\"spt\", \"subnet\", \"attach\", \"home\", \"--save\"]),\n### rc.rs\n   12│//!\n  *13│//! **Detach never kills the session.** The broker owns the PTY; closing our send\n   14│//! side ends the viewport with the session still running (`serve_attach` →\n   15│//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS\n   16│//! attach-lifecycle invariant).\n     │...\n   32│use spt_daemon::msg::decode_bytes;\n  *33│use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\n   34│\n   35│/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the\n   36│/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one\n     │...\n   50│    /// The operator hit the detach keybind — end the viewport, leave the session.\n  *51│    Detach,\n   52│}\n   53│\n   54│/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET\n     │...\n  610│    /// The operator hit `ctrl-b d` — end the viewport.\n *611│    Detach,\n  612│    /// Bytes to forward to the session (empty ⇒ nothing to send).\n  613│    Forward(Vec<u8>),\n  614│    /// Saw the lone `ctrl-b` prefix — armed; nothing to forward yet.\n     │...\n  619│/// [`parse_stdin_chunk`] (the byte path) so both inputs share IDENTICAL detach\n *620│/// semantics (doyle ruling): Ctrl+B arms; armed + plain `'d'` ⇒ Detach; armed +\n  621│/// Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` then the translated event. The\n  622│/// detach key is PLAIN `'d'` only (a Ctrl+D stays prefix + `0x04`, matching the byte\n  623│/// SM's bare-`DETACH_KEY`). Pure given `armed`, so it unit-tests without a console.\n     │...\n  632│        if ke.code == KeyCode::Char('d') && ke.modifiers.is_empty() {\n *633│            return KeyAction::Detach;\n  634│        }\n  635│        if is_ctrl_b {\n  636│            return KeyAction::Forward(vec![DETACH_PREFIX]); // literal ctrl-b\n     │...\n  962│/// raw byte-read path: Unix already delivers proper VT, and the e2e byte-injection\n *963│/// relies on it. Detach is the `ctrl-b d` prefix on BOTH paths.\n  964│fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {\n  965│    #[cfg(windows)]\n  966│    {\n     │...\n 1000│            if parsed.detach {\n*1001│                let _ = tx.send(StdinMsg::Detach);\n 1002│                return;\n 1003│            }\n 1004│        }\n     │...\n 1051│                    match key_event_step(&mut armed, ke) {\n*1052│                        KeyAction::Detach => {\n*1053│                            let _ = tx.send(StdinMsg::Detach);\n 1054│                            return;\n 1055│                        }\n 1056│                        KeyAction::Forward(bytes) => {\n     │...\n 1081│// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\n*1082│pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n 1083│    run_attach_inner(endpoint_id, intent, false)\n 1084│}\n 1085│\n     │...\n 1097│    endpoint_id: &str,\n*1098│    intent: AttachIntent,\n 1099│) -> Result<(), String> {\n 1100│    run_attach_inner(endpoint_id, intent, true)\n 1101│}\n     │...\n 1104│    endpoint_id: &str,\n*1105│    intent: AttachIntent,\n 1106│    session_confirmed: bool,\n 1107│) -> Result<(), String> {\n*1108│    let view = intent == AttachIntent::Viewer;\n 1109│    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\n 1110│    // an endpoint a DIFFERENT node already controls gets actionable guidance here\n 1111│    // (the broker would also refuse on the wire — this is the friendly path). A\n 1112│    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\n*1113│    if intent == AttachIntent::Control {\n 1114│        if let Some(node) = current_driver(endpoint_id) {\n 1115│            println!(\"'{endpoint_id}' is controlled by {node}.\");\n 1116│            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\n     │...\n 1388│// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\n*1389│fn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n 1390│    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n 1391│    // launcher. It must NEVER auto-start the broker — the historical\n 1392│    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n     │...\n 1520│    endpoint_id: &str,\n*1521│    intent: AttachIntent,\n 1522│    view: bool,\n 1523│    mut est: Established,\n 1524│    rx: Option<&mpsc::Receiver<StdinMsg>>,\n     │...\n 1591│                while let Ok(msg) = rx.try_recv() {\n*1592│                    if matches!(msg, StdinMsg::Detach) {\n 1593│                        detached = true;\n 1594│                    }\n 1595│                }\n     │...\n 1731│    /// conn dropped (node bounce, network cut), NOT an operator detach (that\n*1732│    /// returns via the stdin `Detach` path directly) and NOT a session end (an\n 1733│    /// `Exit` record precedes a session-death EOF). Pre-B-3 this was mislabeled\n 1734│    /// \"detached — still running\"; it is the SEVERED case the reconnect loop\n 1735│    /// re-drives. [impl->REQ-RC-RECONNECT]\n     │...\n 1904│                    }\n*1905│                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\n 1906│                    Err(mpsc::TryRecvError::Empty) => break,\n 1907│                    Err(mpsc::TryRecvError::Disconnected) => {\n 1908│                        return Ok(detach(brain, stream_id))\n\n### picker/\n#### model.rs\n  779│        id: String,\n *780│        intent: spt_net::net::attach::AttachIntent,\n  781│    },\n  782│    /// Write/update a `spt-<id>` shortcut, then stay (re-enter the loop).\n  783│    Shortcut {\n     │...\n 1343│        };\n*1344│        use spt_net::net::attach::AttachIntent;\n 1345│        match opt {\n 1346│            // An online endpoint's PTY is live → attach the rc pump; an offline\n 1347│            // one is brought up then ATTACHED by default (W4 UX: Start-now lands you\n     │...\n 1351│            ConfirmOption::Attach | ConfirmOption::Start => Some(if online {\n*1352│                Outcome::Attach { id: ep.id.clone(), intent: AttachIntent::Control }\n 1353│            } else {\n 1354│                bringup(RunMode::Attach)\n 1355│            }),\n 1356│            ConfirmOption::View => Some(if online {\n*1357│                Outcome::Attach { id: ep.id.clone(), intent: AttachIntent::Viewer }\n 1358│            } else {\n 1359│                bringup(RunMode::View)\n 1360│            }),\n     │...\n 1363│                id: ep.id.clone(),\n*1364│                intent: AttachIntent::Take,\n 1365│            }),\n 1366│            // C-2: wake a REMOTE suspended endpoint through its owning node's rest\n 1367│            // edge — carries the RAW node hex (node_key) so the dispatch qualifies\n     │...\n 2558│        m.enter_pick();\n*2559│        use spt_net::net::attach::AttachIntent;\n 2560│        assert_eq!(\n 2561│            m.confirm_terminal(ConfirmOption::Attach),\n*2562│            Some(Outcome::Attach { id: \"live\".into(), intent: AttachIntent::Control })\n 2563│        );\n 2564│        assert_eq!(\n 2565│            m.confirm_terminal(ConfirmOption::View),\n*2566│            Some(Outcome::Attach { id: \"live\".into(), intent: AttachIntent::Viewer })\n 2567│        );\n 2568│        match m.confirm_terminal(ConfirmOption::Shortcut).unwrap() {\n 2569│            Outcome::Shortcut { adapter, id, .. } => {\n\n## tests/\n### resume_no_control_steal_e2e.rs\n  13│//! `resume_sessions()` at boot, which re-attached EVERY session `KIND_SESSIONS` returns as\n *14│//! `subscribe_with(AttachIntent::Control, by:None)`. The docstring's \"a None identity never\n  15│//! displaces (falls back to viewer)\" is a MYTH for FREE / SAME-LOCAL-IDENTITY slots:\n  16│//! `resolve_subscribe` (broker.rs) stall-evicts first, then `become_controller` when the\n  17│//! slot is free OR the incumbent is also local (`None == None`) — viewer-fallback fires\n    │...\n  48│//!\n *49│//! `resume_sessions` (still used by DRAINING callers) re-attaches as **`AttachIntent::Viewer`**\n  50│//! (a viewer never owns `driven_by` and is never stall-evicted). The RED→GREEN discriminator\n  51│//! this rig asserts is\n  52│//! the STEAL itself — the deterministic ROOT that CAUSES both A and B — observed as OUTPUT\n    │...\n 483│    // swap — each A_i went on receiving output after the respawn. RED (resume re-attaches\n*484│    // AttachIntent::Control): resolve_subscribe same_identity(None==None) become_controller'd\n 485│    // each slot, dropping A_i → its tally froze → gained==0. GREEN (Viewer): A_i untouched.\n 486│    // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n 487│    for (i, g) in gained.iter().enumerate() {"},"isError":false,"timestamp":1784195752999}}
{"type":"message","id":"28848451","parentId":"64169b6b","timestamp":"2026-07-16T09:55:53.039Z","message":{"role":"toolResult","toolCallId":"call_OqH0NzPjFkqL2rz1g6oen8Jc|fc_008390f508d1d09b016a58aaa8e46c819a9e933dd3ed5d7d07","toolName":"read","content":[{"type":"text","text":"# Known Hazards\n\nHard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** — the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.\n\n**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.\n\n---\n\n## 1. Race conditions & ordering\n\n### 1.1 Phantom INIT_SIGNOFF after grace period\n- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spooled and drains on the next iteration → teardown despite a live Self.\n- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.\n- **spt-core mapping:** in-daemon now (no separate wrapper), but the ordering invariant is identical — orphan/teardown logic must re-evaluate liveness after the grace wait, not before enqueue.\n- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.\n\n### 1.2 Poll-rewrite race & info.json mid-write reads\n- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\n- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).\n- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\n- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\n\n### 1.3 Stale `index.lock` wedge from prior git crash\n- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.\n- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.\n- **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- **Sister cite:** CHANGELOG v1.11.20 \"Stale `index.lock`\"; `src/common/git.rs`.\n\n### 1.4 Deferred spool rows must not leak to the event stream\n- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.\n- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.\n- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.\n- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.\n\n### 1.5 Worker (working-perch) lifecycle path consistency\n- **Failure:** subagent-start creates the perch at one path layout; later hooks read it at another → not found; stop-hook scan misses nested perches.\n- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.\n- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.\n- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.\n\n---\n\n## 2. Identity & session-binding\n\n### 2.1 Parent PID over ephemeral poll PID\n- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).\n- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.\n- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\n\n### 2.2 Stdin session_id precedence over env\n- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.\n- **Invariant:** stdin-provided session_id wins; env is fallback only.\n- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.\n- **Sister cite:** CHANGELOG v1.35.1 \"IN-05\"; `hook_subagent_start.rs:40-51`.\n\n### 2.3 Binary-handoff argv schema must stay backward-compatible\n- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.\n- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.\n- **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- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\n\n### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\n- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.\n- **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- **spt-core mapping:** carries to the daemon's per-instance generation tracking.\n- **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 all session continuity by **querying the broker** (`Brain::resume_sessions` over the broker's cursor-of-record), never a frame. `BrainState` / `Brain::handoff` / `Brain::snapshot` remain `pub` and compiled **only for the integration tests** (handoff/idempotent/da…\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\n\n### 2.5 Daemon-hosted endpoints have no dedicated liveness PID\n- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `is_process_alive(pid)` reads \"alive\" for a torn-down endpoint as long as the daemon runs — while `clean_stale_entries` (dead-pid deletion) can no longer distinguish a dead endpoint from a live one. The 2.1/5.1 liveness models do **not** cover this third categor…\n- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`info.json` carries daemon-managed `status`, capability resolved by `adapter_name` — CONTEXT \"Shell… Not in the subnet registry\") and extends it to daemon-hosted *agent* perches.\n- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let the per-pid assumption leak into new call sites.\n- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\n\n---\n\n## 3. Lifecycle\n\n### 3.1 Ephemeral perch cleanup on every `ring` exit path\n- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.\n- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.\n- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.\n- **Sister cite:** `src/owl/ring.rs:58-294`.\n\n### 3.2 Stale signoff sentinel must not kill a fresh start\n- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.\n- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.\n- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.\n- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.\n\n### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF\n- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.\n- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.\n- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.\n- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.\n\n---\n\n## 4. Wire / transport\n\n### 4.1 Envelope HTML-entity codec ordering — `&amp;` decoded LAST\n- **Failure:** decoding `&amp;`-entity before the others double-decodes nested entities (`&amp;amp;lt;` → wrong result).\n- **Invariant:** ENCODE order amp→first … `<br>`→last; DECODE order `<br>`→first … amp→**last** (`&lt;`,`&gt;`,`&quot;`, then `&amp;`). One sole decode site (at the LLM/stdin boundary); the parser never decodes.\n- **spt-core mapping:** `spt-proto` owns the envelope grammar (public SDK, semver + wire-version). This codec contract is a copy-verbatim commodity item (ADR-0001) and a public-API conformance test.\n- **Sister cite:** `src/owl/poll.rs:1-73`; `src/common/envelope.rs`.\n- **CR-linesafety `[REQ-HAZARD-ENVELOPE-CR-LINESAFE]`:** the EVENT is LINE-FRAMED, so the codec must neutralize raw `\\r` too — `event_body_escape` folds CRLF/lone-CR to `\\n` (→`<br>`) **before** framing. **Failure (field, 2026-06-08):** a cross-node `spt send` from Windows (`echo` → CRLF) carried a raw `\\r` into the single-line envelope; the receiver terminal did a CR→column-0 overwrite (`</EVENT>` clobbered `<EVENT t`). `\\r` was never line-representable here, so normalizing it is robustness, not an ADR-0001 wire divergence (decoder + amp-last untouched). Belt-and-suspenders: `spt send`/`ring` trim stdin like `notify`.\n\n### 4.2 Two-slice envelope parser is panic-free and tolerant\n- **Failure:** malformed envelope (unclosed/misordered/nested tags) panics or drops output.\n- **Invariant:** tags case-sensitive, all optional; no tags → whole body to live slot; unclosed → None for that tag; out-of-order → both still extracted; nested unknown tags preserved verbatim; zero `unwrap` on parsed text.\n- **spt-core mapping:** `spt-proto` parser; property-test the robustness rules.\n- **Sister cite:** `src/common/envelope.rs:64-92`; tests 99-207.\n\n### 4.3 Registry stale-entry cleanup precedes lookup\n- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n- **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- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n\n### 4.4 Deferred rows survive poll drain\n- **Failure:** poll `drain_all` flushes a deferred (spool-only) message meant for a hook consumer → message lost.\n- **Invariant:** deferred rows are never flushed by the event-stream drain; only `drain_non_deferred_*` / `peek_all` touch them.\n- **Sister cite:** CHANGELOG v1.11.20; `src/common/spool.rs`. (See also 1.4.)\n\n### 4.5 Inbox legacy compat must not double-deliver\n- **Failure:** message surfaced via both spool (durable) and legacy inbox files → duplicate or racing delivery.\n- **Invariant:** spool is the sole read path at poll time; inbox is write-for-compat only and never read.\n- **spt-core mapping:** clean-room — likely drop the legacy inbox entirely. If kept for any compat, preserve \"never read at drain time.\"\n- **Sister cite:** `src/common/inbox.rs`.\n\n### 4.6 Addressable-id charset reserves the address delimiters\n<!-- [doc->REQ-HAZARD-ID-CHARSET] -->\n- **Failure:** a bare endpoint id that contains `:` or `@` (or a path separator / whitespace / control char) makes the canonical qualified address `[subnet:]id[@node]` (ADR-0006 / REQ-INST-10) ambiguous to parse, and lets a name smuggle into a perch directory path. Once permissive ids exist in the wild, tightening later needs a migration.\n- **Invariant:** every addressable id/name is validated to `[A-Za-z0-9_-]` + Hiragana/Katakana/CJK only, length `1..=64`, **at every creation seam** (`ready` start, `api bind`, `api listen`, `api worker-start`). `:` and `@` are permanently reserved as address delimiters; reads of existing perches are never re-validated. Enforce now (pre-M3/M4) so no permissive id-data accumulates.\n- **spt-core mapping:** `spt_proto::id::validate_endpoint_id`; called at the four creation seams. The existing Psyche (`<parent>-psyche`) / Worker (`<parent>-w<N>`) suffix scheme uses only `-` + alphanumerics, so composite ids validate.\n\n### 4.7 Concurrent SQLite openers must not fail with \"database is locked\"\n<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->\n- **Failure:** two endpoints on one machine open the same SQLite store at once (e.g. two `ReadyAgent::start` calls registering simultaneously) and one fails outright with `SQLITE_BUSY` / \"database is locked\" → spurious registration/spool failure. Surfaced as a parallel-test flake in `two_agents_exchange_message_tcp_and_spool`, but the bug is real concurrency, not test-only.\n- **Invariant:** `busy_timeout` is set **before** any lock-taking statement on every connection. Switching `journal_mode=WAL` takes a brief exclusive lock; with the default 0ms timeout it fails immediately under contention, so the pragma order is load-bearing: `Connection::open` → `busy_timeout` → `journal_mode=WAL` → `CREATE TABLE …`. WAL alone is insufficient (concurrent *writers* still serialize; they must *wait*, not error).\n- **spt-core mapping:** `spt_store::registry::open_registry` + `spt_store::spool::open_spool_at`; both set `busy_timeout=5000` first. Any future SQLite store (history Path B, instance registry) must follow the same ordering.\n\n### 4.8 Registry merge ordered by epoch, never wall-clock (red-team #8)\n<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->\n- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or \"last write wins\"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.\n- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-clock is at most a human tiebreaker hint inside a flagged conflict, never the ordering authority. The same epoch source unifies with the D6 sync-precedence concurrent-write detection (#7).\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.\n\n### 4.9 SQLite stores must create their parent dir — SQLite won't\n<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->\n- **Failure:** `Connection::open` creates the database FILE but never its parent DIRECTORY. On a fresh home (first boot, fresh CI `_work` dir) a registry op that runs before any perch-creating op (`create_dir_all` side effects) fails `SQLITE_CANTOPEN` — \"unable to open database file …owlery\\.registry\". Timing-dependent: whichever code path touches the home first decides the outcome, so it surfaces as a parallel-test flake (bind-first tests losing the dir-creation race to perch-first tests). Bit the hfenduleam CI leg twice (2026-06-03/04, four spt-msg unit tests at once on the second strike) before being run to ground; a slow runner filesystem (AV scanning fresh dirs) widens the window but is not the cause.\n- **Invariant:** every SQLite store's open path `create_dir_all`s its parent dir itself, best-effort, before `Connection::open` — never relying on another subsystem having materialized the home first. (Mirrors the spool, which always did this; the registry didn't.)\n- **spt-core mapping:** `spt_store::registry::open_registry` (`create_dir_all(owlery)` before open). `spt_store::spool::open_spool_at` already creates its perch dir. Any future SQLite store must do the same — pair this with the 4.7 pragma ordering on every new store.\n\n### 4.10 Dead node identities leave immortal registry rows  `[REQ-HAZARD-REGISTRY-GHOST-ROWS]`\n<!-- [doc->REQ-HAZARD-REGISTRY-GHOST-ROWS] -->\n- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the \"node\" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the dead identity and refuses with a **phantom `AcrossNodes` ambiguity** — unfixable by the user, because no qualifier reaches a node that no longer exists. Hit live in the M7 acceptance run (2026-06-06): gravity paired under two identities (09ef…, then 03854a… af…\n- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future update for a node comes from that node itself, alive, re-inserting from its durable `EpochSource` within one cadence — there is no lagging third-party replay to mis-order against. A merely-offline node loses its rows after the window and reconverges on return; meanwhi…\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Ghost registry row eviction\"); the AMBIGUOUS render fix rode along.\n- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. \"No transitive gossip\" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So \"any future update for a node comes from that node itself, alive\" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transitive *row* relay — would have broken this; roster-only relay was chosen precisely to keep it.)\n\n### 4.11 Advertisement-epoch reset strands a node  `[REQ-HAZARD-EPOCH-RESET]`\n<!-- [doc->REQ-HAZARD-EPOCH-RESET] -->\n- **Failure:** a node whose advertisement-epoch counter resets (the durable `EpochSource` file lost/recreated) re-advertises with LOW epochs; peers hold a higher last-seen epoch for that `(endpoint, node)` lease and drop every fresh row as **stale** — the node advertises into a void until its counter outruns its own history. Nothing renders the cause: the node looks healthy locally, peers simply never update.\n- **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 epoch sub-check).\n- **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 above).\n- **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- **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\n---\n\n## 5. Platform-specific\n\n### 5.1 Windows PID recycling false positives\n- **Failure:** recycled PID reads alive for the wrong process → orphan misclassification.\n- **Invariant:** anchor liveness on the stable parent/harness PID; minimal info.json for supervisor-owned perches; mtime grace window (≥60s) masks transient mismatches.\n- **spt-core mapping:** broker-held handles supersede PID polling for spt-hosted sessions; keep the grace window for harness-hosted.\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; `src/common/list_filter.rs:168-175`.\n\n### 5.2 Windows EBUSY on atomic rename\n- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\n- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\n- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\n- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\n\n### 5.3 Git/subprocess timeout stamping\n- **Failure:** a hung subprocess (git on slow net) blocks the supervisor indefinitely.\n- **Invariant:** every metadata-producing subprocess has a timeout; timeout yields `None` + rate-limited stderr, never a hang.\n- **spt-core mapping:** generalize to all manifest-declared harness invocations (delegated commands, adapter updates) — timeouts mandatory.\n- **Sister cite:** `src/common/git.rs`.\n\n### 5.4 Windows UNC prefix in serialized paths\n- **Failure:** canonicalized `\\\\?\\C:\\...` serializes to `//?/C:/...` and fails `read_to_string`.\n- **Invariant:** strip the `\\\\?\\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.\n- **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- **Sister cite:** `src/common/owlery.rs:377-384`.\n\n### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`\n- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.\n- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.\n- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).\n- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.\n\n<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->\n### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`\n- **Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::null()`. The caller's pipe read never sees EOF: the capturing caller **hangs forever** (unix is immune — pipe fds are `CLOEXEC`). Paid twice: daemon spawn (guarded at D4a-era `spawn_detached`), then again at M5-D3e when the mock-shell E2E hung `spt shell spawn` for h…\n- **Invariant:** every detach-spawn of a long-lived child runs with **`bInheritHandles = FALSE`** (`spt-daemon::daemon::detached_no_inherit`) — zero handles flow, whatever the pipe's depth in the ancestry. Stripping `HANDLE_FLAG_INHERIT` from the spawner's *std* handles is NOT sufficient: a grandparent capture's pipe sits in the handle table as a stray inheritable handle and still flows through every `bInheritHandles = TRUE` hop (the first guard shipped that way and was wedged by exactly this — a daemon spawned three layers deep held the pwsh-level pipe of the CI/test harness).\n- **spt-core mapping:** `spt-daemon::daemon::spawn_detached` (the daemon) and `spt-daemon::shellhost::launch_shell` (the relay-receipt shell binary). Any future long-lived detached spawn (manifest-template children included) must use the same no-inherit spawn.\n- **Source:** spt-core, M5-D3e (`shell_e2e.rs` hang, 2026-06-04, twice — once per guard generation); Rust `Command` restricts *its own* created stdio handles but a parent's inheritable handle table still flows.\n\n### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`\n<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->\n- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill (hit live, M7 acceptance 2026-06-06). **Linux:** a sudo'd command spawns a root daemon and/or root-owned state — and because sudo flips `$HOME`, the daemon can mint a *different node identity* in root's universe (the very key-flip that produced the 4.10 ghost r…\n- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run` entry guard catches a *directly* elevated `spt daemon` — Linux drops privileges in-process before touching any state; Windows respawns de-elevated and exits. When no unelevated identity exists to drop to (UAC disabled, genuine root login, SYSTEM), the daemon runs …\n- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\n\n<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\n### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\n- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).\n- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).\n- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 \"flag combo breaks spawn\" regression class); window-absence was verified live by process-watch capture.\n- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-runtime::ManifestRuntime::command_for` (the one shared builder behind `spawn_session` + `run_bounded_stdin` — the notif pump's `spawn_notif_command` and the live agent's psyche/echo/turn spawns), `spt-daemon::shellwake` (already guarded). The flag lives in each shared builder, not per call site, so the invariant holds for every ManifestRuntime spawn by construction.\n- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).\n\n### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`\n- **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 *environment-conditional* — green on any host up longer than the offset, red below it — so it slips local dev and only bites a fresh CI box or a just-rebooted machine.\n- **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- **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- **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- **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### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`\n- **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 work for the common install shape. Field-hit on KITSUBITO at the v0.1.1 ship.\n- **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-interactive or sudo-absent falls back to printing the runnable hint. Never emit a bare-name elevation instruction.\n- **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- **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- **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- **Source:** spt-core field report, 2026-06-07 — `reavus@KITSUBITO`, `spt` in `~/.local/bin`; the absolute-path `sudo` invocation was confirmed working before the fix landed.\n\n### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\n- **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-elevating the already-elevated child (an infinite UAC/polkit loop). The user's UAC/polkit/sudo prompt is the ONLY consent gate — the mechanism must never bypass or widen it.\n- **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 the user (composes with the 5.7 de-elevation) and **never re-elevates**: `decide_elevation_path` returns `AlreadyElevated` whenever the process is `Elevated`, on every OS (loop-safety). The unprivileged parent never pipes/captures the elevated child's stdout acro…\n- **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 quoted token).\n- **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 composes with).\n- **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<!-- [doc->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE] -->\n### 5.12 Native-PTY spawn of a bare program runs the wrong (non-PE) file on Windows  `[REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]`\n- **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:\\nvm4w\\nodejs\\ccs` 193 (operator, 2026-06-16). The same bites any harness/shell whose `[session.self]`/`[shell].spawn` names a `.cmd`/`.bat`/`.ps1`-backed command — `CreateProcessW` cannot execute a batch or PowerShell script directly.\n- **Invariant:** spt-term resolves the program ITSELF before handing it to `CommandBuilder`, bypassing portable-pty's `which`. A bare name is searched over `PATH` × `PATHEXT` (whose default order already prefers `.EXE`/`.COM` over `.BAT`/`.CMD`), then an extensionless fallback. A non-PE target is wrapped in its interpreter: `.cmd`/`.bat` → `cmd.exe /d /c <path>`, `.ps1` → `powershell -NoProfile -File <path>` (the wrap args precede the caller's args); a real executable spawns directly; an unresolvable name passes through unchanged (never makes a working case worse). Unix is a passthrough — `execve` honours a shebang on an extensionless script. Applied at the ONE `CommandBuilder` chokepoint (`PtySession::spawn_program_in`), so every broker harness + shell spaw…\n- **spt-core mapping:** `spt_term::winprog::{resolve_for_pty, resolve_in}` (the pure PATHEXT-precedence kernel + the Windows env wiring), wired into `spt_term::pty::PtySession::spawn_program_in`. Unit: `resolve_in` precedence (`.cmd`-over-shim, `.exe`-direct, explicit-extension, path-order, passthrough) [`winprog.rs`].\n- **Source:** field diagnosis 2026-06-16 (operator dogfood, `claude-spt:ccs` bringup) — doyle.\n\n<!-- [doc->REQ-HAZARD-PERCH-RECORD-POWER-LOSS] -->\n### 5.13 Atomic write leaves data un-synced before the rename → NUL zero-fill on power loss  `[REQ-HAZARD-PERCH-RECORD-POWER-LOSS]`\n- **Failure:** `atomic_write_bytes` was `fs::write(tmp)` + `rename(tmp, path)` with no `fsync`. The rename's directory **metadata** is journaled durable, but the tmp file's **data blocks** are still in the page cache. A hard reset (power loss, forced reboot) between the two flushes lands the rename but loses the data → the file reappears at its **full length filled with NUL**. Field incident: after a machine restart `owlery/hall-a/info.json` was 360 bytes of all-NUL (the nested psyche record 125 bytes all-NUL); `Get-Content` renders blank, `Format-Hex` shows the zero-fill. A wiped-but-present record then read as a live ONLINE endpoint for days (compounded by 5.14).\n- **Invariant:** for records whose loss is **unrecoverable**, flush data to stable storage **before** the rename publishes the name — `File::create(tmp)` → `write_all` → `sync_all()` → `rename_with_retry`. Then a crash yields the complete old file OR the complete new file, never a NUL husk. (A file of correct length that is ALL-NUL is the diagnostic tell of this class — a non-fsync'd write caught by a hard reset.) **Durability is SCOPED, not blanket.** `fsync`ing *every* atomic write serializes a durable flush behind each of the ~26 `atomic_write` callers — and for `info.json` the flush lands under the per-perch `.info.lock` (the **W1b lock-across-fsync wedge shape**, §6-era) — which stalls daemon bringup 4–6× so the endpoint misses its ONLINE window. Scope …\n- **spt-core mapping:** `spt_store::atomic::atomic_write_bytes_durable` / `atomic_write_string_durable` are the opt-in durable siblings; the default `atomic_write_bytes` / `atomic_write_string` stay non-durable. Durable callers are exactly `spt_store::info::write_info` (the perch record — the hall-a surface), `spt_store::nodeid` (the node seed — corrupt is node-bricking, never regenerated), and `spt_daemon::machineid` (minted-once machine id — a NUL husk would silently re-mint a different id). Reader-side handling of an already-corrupt record is the sibling 5.14. **Canary:** `attach_wedge_e2e` is the regression guard — a blanket fsync fails it (bringup misses the 20s ONLINE budget); keep durability scoped so it stays green.\n- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; blanket-fsync perf regression pinned + scoped by the gate rig (doyle), same date.\n\n<!-- [doc->REQ-HAZARD-CORRUPT-PERCH-COHERENCE] -->\n### 5.14 Corrupt info.json read as ABSENT → fail-open readers gossip a wiped perch ONLINE  `[REQ-HAZARD-CORRUPT-PERCH-COHERENCE]`\n- **Failure:** three readers each collapsed a **corrupt** (present-but-unparseable) `info.json` into their fail-open ABSENT default, so a NUL-wiped perch (5.13) read as permanently live: `is_perch_alive` returned `true` (unreadable ⇒ interim-alive), `advertised_status` then saw alive + no resting record ⇒ `Active`, and the daemon self-gossiped that Active row every round (epoch 173k+). Result: `hall-a`, dead since a machine restart, showed ONLINE in `spt whoami` and on every remote picker.\n- **Invariant:** CORRUPT ≠ ABSENT. A record that EXISTS but stays unparseable across the retry budget is a **destroyed** record — never a live endpoint. `is_perch_alive` reads corrupt ⇒ **not alive** (ABSENT keeps interim-alive parity — the ONE case that stays true); `advertised_status` then lands corrupt in the cold arm ⇒ **Suspended**, never Active/Dormant. Readers that decide liveness/status must branch on the tri-state (present / absent / corrupt), not an `Option` that fuses the last two.\n- **spt-core mapping:** `spt_store::liveness::{read_raw_state, is_perch_alive}` (the tri-state kernel) and `spt_daemon::registryhost::advertised_status` (cascades to Suspended off the alive fix). `list_self_perch_ids` deliberately still lists a corrupt dir by existence — that is the *visibility* the local-roster fix (counter-39 #3) relies on, not a liveness claim. Candidate sibling seam `is_registry_entry_alive` (corrupt hosted row falls to a daemon-pid probe) is parked — it does not produce the ONLINE gossip.\n- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; sequel to the v0.17.0 W4 presence-truth fix (cold ⇒ Suspended) which a corrupt perch bypassed via false-alive.\n\n<!-- [doc->REQ-HAZARD-ATOMIC-TMP-COLLISION] -->\n### 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file)  `[REQ-HAZARD-ATOMIC-TMP-COLLISION]`\n- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `BindError::Io` *after* having written the record, mock-session's bind-is-fatal check `exit(1)`s, the harness dies, and the endpoint never reaches ONLINE. A microsecond window that existed forever (bare `fs::write` + rename back-to-back) — the 5.13 `sync_all` widened c…\n- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind to them.\n- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\n- **Source:** RCA 2026-07-01 (doyle gate rig) — write-trace side-channel + `cmd_bind` step probes pinned `PROBE cmd_bind establish ERR: Io(os error 2)`; explains the counter-39 gate-1 blanket failure, the dummy_harness ONLINE-race DIAGs, and the nondeterminism. Lesson: a widened timing window doesn't create a race, it reveals one.\n\n<!-- [doc->REQ-HAZARD-INFO-RMW-LOST-UPDATE] -->\n### 5.16 Unlocked whole-record info.json write races a locked RMW → silent lost update  `[REQ-HAZARD-INFO-RMW-LOST-UPDATE]`\n- **Failure:** `mutate_info` serializes its read→mutate→write under the per-perch `.info.lock` sentinel, but `establish_perch` (`spt::api::startup`) did read→conflict-check→`write_info` with **no lock**. At bind the two writers race (~700µs apart): the daemon RMW reads the PRE-BIND record, bind's `write_info` renames the full record in (`state=live_agent`, `controllable=Some(true)`, `session_id`), then the RMW writes its STALE pre-bind snapshot back plus a `status=online` stamp. The surviving `info.json` is a **pre-bind shape carrying `status=online`** — bringup passes (it sees ONLINE), the broker serves the session, then on child death `reconcile_hosted_liveness` reads `state != \"live_agent\"` (or `controllable != Some(true)`) and *silently `continue`s* — th…\n- **Invariant:** every `info.json` writer serializes under the one per-perch `.info.lock`. A whole-record write (`write_info`) takes the lock exactly as the RMW (`mutate_info`) does — a unique tmp only makes concurrent writes *last-writer-wins*, which is safe ONLY if the writers are serialized (cross-ref 5.15). A multi-step read→check→write (bind's establish) must hold ONE lock acquisition across all three (a true compare-and-set) — check-then-write with the lock dropped in between still interleaves a stamp. Readers stay lock-free (the atomic rename already gives a complete old-or-new record). Holding `.info.lock` across the durable fsync is ms-scale on these infrequent record writes (the same thing every `mutate_info` already does), NOT the per-keystroke W1…\n- **spt-core mapping:** `spt_store::info` — `write_info` now acquires the sentinel then calls a private `write_info_unlocked` (and `mutate_info` — the public RMW primitive — calls the unlocked writer while holding its own lock, so no double-lock deadlock); `info::establish_locked(perch, build)` runs the bind's read→check→build→write as one locked CAS, called by `spt::api::startup::establish_perch`. The other read-modify-write callers were audited and the two that mutate a load-bearing field were converted to the `mutate_info` CAS: `spt::api::reporting::cmd_boundary` (rc-rebind session-id rotation) and `spt_store::home::adopt_for_unset` (home-subnet adoption; the `is_none()` re-check runs inside the closure). **Deliberately parked (known unlocked-RMW residue,…\n- **Source:** RCA 2026-07-01 (doyle gate rig) — differential (baseline PASS / 77beeac+baseline-atomic PASS / fe385f5 2/3 FAIL) isolated the atomic rework as the delta; `reconcile_hosted_liveness` emitting ZERO `LIVENESS_RECONCILE_OFFLINE` lines for the dead victim over 20s pinned the silent-skip. The unique-tmp fix worked (bringup + serve fine) but exposed this deeper pairing.\n\n---\n\n## 6. Documented regressions (non-obvious invariants)\n\n### 6.1 No flat/nested perch siblings; resolver-routed paths\n- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.\n- **Invariant:** one path resolver; never create divergent siblings.\n- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.\n- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.\n\n### 6.2 Soft-cleanup preserves state, removes `ready`\n- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.\n- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.\n- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.\n- **Sister cite:** `src/owl/stop.rs`.\n\n…\n\n…\n<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->\n\n[Showing lines 1-300 of 736. Use :301 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"# Known Hazards\r\n\r\nHard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** — the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.\r\n\r\n**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.\r\n\r\n---\r\n\r\n## 1. Race conditions & ordering\r\n\r\n### 1.1 Phantom INIT_SIGNOFF after grace period\r\n- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spooled and drains on the next iteration → teardown despite a live Self.\r\n- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.\r\n- **spt-core mapping:** in-daemon now (no separate wrapper), but the ordering invariant is identical — orphan/teardown logic must re-evaluate liveness after the grace wait, not before enqueue.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.\r\n\r\n### 1.2 Poll-rewrite race & info.json mid-write reads\r\n- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\r\n- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).\r\n- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\r\n- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\r\n\r\n### 1.3 Stale `index.lock` wedge from prior git crash\r\n- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.\r\n- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.\r\n- **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.\r\n- **Sister cite:** CHANGELOG v1.11.20 \"Stale `index.lock`\"; `src/common/git.rs`.\r\n\r\n### 1.4 Deferred spool rows must not leak to the event stream\r\n- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.\r\n- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.\r\n- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.\r\n- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.\r\n\r\n### 1.5 Worker (working-perch) lifecycle path consistency\r\n- **Failure:** subagent-start creates the perch at one path layout; later hooks read it at another → not found; stop-hook scan misses nested perches.\r\n- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.\r\n- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.\r\n- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.\r\n\r\n---\r\n\r\n## 2. Identity & session-binding\r\n\r\n### 2.1 Parent PID over ephemeral poll PID\r\n- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).\r\n- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.\r\n- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\r\n\r\n### 2.2 Stdin session_id precedence over env\r\n- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.\r\n- **Invariant:** stdin-provided session_id wins; env is fallback only.\r\n- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.\r\n- **Sister cite:** CHANGELOG v1.35.1 \"IN-05\"; `hook_subagent_start.rs:40-51`.\r\n\r\n### 2.3 Binary-handoff argv schema must stay backward-compatible\r\n- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.\r\n- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.\r\n- **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.\r\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\r\n\r\n### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\r\n- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.\r\n- **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\".\r\n- **spt-core mapping:** carries to the daemon's per-instance generation tracking.\r\n- **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 all session continuity by **querying the broker** (`Brain::resume_sessions` over the broker's cursor-of-record), never a frame. `BrainState` / `Brain::handoff` / `Brain::snapshot` remain `pub` and compiled **only for the integration tests** (handoff/idempotent/da…\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\r\n\r\n### 2.5 Daemon-hosted endpoints have no dedicated liveness PID\r\n- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `is_process_alive(pid)` reads \"alive\" for a torn-down endpoint as long as the daemon runs — while `clean_stale_entries` (dead-pid deletion) can no longer distinguish a dead endpoint from a live one. The 2.1/5.1 liveness models do **not** cover this third categor…\n- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`info.json` carries daemon-managed `status`, capability resolved by `adapter_name` — CONTEXT \"Shell… Not in the subnet registry\") and extends it to daemon-hosted *agent* perches.\r\n- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let the per-pid assumption leak into new call sites.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\r\n\r\n---\r\n\r\n## 3. Lifecycle\r\n\r\n### 3.1 Ephemeral perch cleanup on every `ring` exit path\r\n- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.\r\n- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.\r\n- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.\r\n- **Sister cite:** `src/owl/ring.rs:58-294`.\r\n\r\n### 3.2 Stale signoff sentinel must not kill a fresh start\r\n- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.\r\n- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.\r\n- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.\r\n- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.\r\n\r\n### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF\r\n- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.\r\n- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.\r\n- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.\r\n\r\n---\r\n\r\n## 4. Wire / transport\r\n\r\n### 4.1 Envelope HTML-entity codec ordering — `&amp;` decoded LAST\r\n- **Failure:** decoding `&amp;`-entity before the others double-decodes nested entities (`&amp;amp;lt;` → wrong result).\r\n- **Invariant:** ENCODE order amp→first … `<br>`→last; DECODE order `<br>`→first … amp→**last** (`&lt;`,`&gt;`,`&quot;`, then `&amp;`). One sole decode site (at the LLM/stdin boundary); the parser never decodes.\r\n- **spt-core mapping:** `spt-proto` owns the envelope grammar (public SDK, semver + wire-version). This codec contract is a copy-verbatim commodity item (ADR-0001) and a public-API conformance test.\r\n- **Sister cite:** `src/owl/poll.rs:1-73`; `src/common/envelope.rs`.\r\n- **CR-linesafety `[REQ-HAZARD-ENVELOPE-CR-LINESAFE]`:** the EVENT is LINE-FRAMED, so the codec must neutralize raw `\\r` too — `event_body_escape` folds CRLF/lone-CR to `\\n` (→`<br>`) **before** framing. **Failure (field, 2026-06-08):** a cross-node `spt send` from Windows (`echo` → CRLF) carried a raw `\\r` into the single-line envelope; the receiver terminal did a CR→column-0 overwrite (`</EVENT>` clobbered `<EVENT t`). `\\r` was never line-representable here, so normalizing it is robustness, not an ADR-0001 wire divergence (decoder + amp-last untouched). Belt-and-suspenders: `spt send`/`ring` trim stdin like `notify`.\r\n\r\n### 4.2 Two-slice envelope parser is panic-free and tolerant\r\n- **Failure:** malformed envelope (unclosed/misordered/nested tags) panics or drops output.\r\n- **Invariant:** tags case-sensitive, all optional; no tags → whole body to live slot; unclosed → None for that tag; out-of-order → both still extracted; nested unknown tags preserved verbatim; zero `unwrap` on parsed text.\r\n- **spt-core mapping:** `spt-proto` parser; property-test the robustness rules.\r\n- **Sister cite:** `src/common/envelope.rs:64-92`; tests 99-207.\r\n\r\n### 4.3 Registry stale-entry cleanup precedes lookup\r\n- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\r\n- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\r\n- **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.\r\n- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\r\n\r\n### 4.4 Deferred rows survive poll drain\r\n- **Failure:** poll `drain_all` flushes a deferred (spool-only) message meant for a hook consumer → message lost.\r\n- **Invariant:** deferred rows are never flushed by the event-stream drain; only `drain_non_deferred_*` / `peek_all` touch them.\r\n- **Sister cite:** CHANGELOG v1.11.20; `src/common/spool.rs`. (See also 1.4.)\r\n\r\n### 4.5 Inbox legacy compat must not double-deliver\r\n- **Failure:** message surfaced via both spool (durable) and legacy inbox files → duplicate or racing delivery.\r\n- **Invariant:** spool is the sole read path at poll time; inbox is write-for-compat only and never read.\r\n- **spt-core mapping:** clean-room — likely drop the legacy inbox entirely. If kept for any compat, preserve \"never read at drain time.\"\r\n- **Sister cite:** `src/common/inbox.rs`.\r\n\r\n### 4.6 Addressable-id charset reserves the address delimiters\r\n<!-- [doc->REQ-HAZARD-ID-CHARSET] -->\r\n- **Failure:** a bare endpoint id that contains `:` or `@` (or a path separator / whitespace / control char) makes the canonical qualified address `[subnet:]id[@node]` (ADR-0006 / REQ-INST-10) ambiguous to parse, and lets a name smuggle into a perch directory path. Once permissive ids exist in the wild, tightening later needs a migration.\r\n- **Invariant:** every addressable id/name is validated to `[A-Za-z0-9_-]` + Hiragana/Katakana/CJK only, length `1..=64`, **at every creation seam** (`ready` start, `api bind`, `api listen`, `api worker-start`). `:` and `@` are permanently reserved as address delimiters; reads of existing perches are never re-validated. Enforce now (pre-M3/M4) so no permissive id-data accumulates.\r\n- **spt-core mapping:** `spt_proto::id::validate_endpoint_id`; called at the four creation seams. The existing Psyche (`<parent>-psyche`) / Worker (`<parent>-w<N>`) suffix scheme uses only `-` + alphanumerics, so composite ids validate.\r\n\r\n### 4.7 Concurrent SQLite openers must not fail with \"database is locked\"\r\n<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->\r\n- **Failure:** two endpoints on one machine open the same SQLite store at once (e.g. two `ReadyAgent::start` calls registering simultaneously) and one fails outright with `SQLITE_BUSY` / \"database is locked\" → spurious registration/spool failure. Surfaced as a parallel-test flake in `two_agents_exchange_message_tcp_and_spool`, but the bug is real concurrency, not test-only.\r\n- **Invariant:** `busy_timeout` is set **before** any lock-taking statement on every connection. Switching `journal_mode=WAL` takes a brief exclusive lock; with the default 0ms timeout it fails immediately under contention, so the pragma order is load-bearing: `Connection::open` → `busy_timeout` → `journal_mode=WAL` → `CREATE TABLE …`. WAL alone is insufficient (concurrent *writers* still serialize; they must *wait*, not error).\r\n- **spt-core mapping:** `spt_store::registry::open_registry` + `spt_store::spool::open_spool_at`; both set `busy_timeout=5000` first. Any future SQLite store (history Path B, instance registry) must follow the same ordering.\r\n\r\n### 4.8 Registry merge ordered by epoch, never wall-clock (red-team #8)\r\n<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->\r\n- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or \"last write wins\"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.\r\n- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-clock is at most a human tiebreaker hint inside a flagged conflict, never the ordering authority. The same epoch source unifies with the D6 sync-precedence concurrent-write detection (#7).\r\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.\r\n\r\n### 4.9 SQLite stores must create their parent dir — SQLite won't\r\n<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->\r\n- **Failure:** `Connection::open` creates the database FILE but never its parent DIRECTORY. On a fresh home (first boot, fresh CI `_work` dir) a registry op that runs before any perch-creating op (`create_dir_all` side effects) fails `SQLITE_CANTOPEN` — \"unable to open database file …owlery\\.registry\". Timing-dependent: whichever code path touches the home first decides the outcome, so it surfaces as a parallel-test flake (bind-first tests losing the dir-creation race to perch-first tests). Bit the hfenduleam CI leg twice (2026-06-03/04, four spt-msg unit tests at once on the second strike) before being run to ground; a slow runner filesystem (AV scanning fresh dirs) widens the window but is not the cause.\r\n- **Invariant:** every SQLite store's open path `create_dir_all`s its parent dir itself, best-effort, before `Connection::open` — never relying on another subsystem having materialized the home first. (Mirrors the spool, which always did this; the registry didn't.)\r\n- **spt-core mapping:** `spt_store::registry::open_registry` (`create_dir_all(owlery)` before open). `spt_store::spool::open_spool_at` already creates its perch dir. Any future SQLite store must do the same — pair this with the 4.7 pragma ordering on every new store.\r\n\r\n### 4.10 Dead node identities leave immortal registry rows  `[REQ-HAZARD-REGISTRY-GHOST-ROWS]`\r\n<!-- [doc->REQ-HAZARD-REGISTRY-GHOST-ROWS] -->\r\n- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the \"node\" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the dead identity and refuses with a **phantom `AcrossNodes` ambiguity** — unfixable by the user, because no qualifier reaches a node that no longer exists. Hit live in the M7 acceptance run (2026-06-06): gravity paired under two identities (09ef…, then 03854a… af…\n- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future update for a node comes from that node itself, alive, re-inserting from its durable `EpochSource` within one cadence — there is no lagging third-party replay to mis-order against. A merely-offline node loses its rows after the window and reconverges on return; meanwhi…\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.\r\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Ghost registry row eviction\"); the AMBIGUOUS render fix rode along.\r\n- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. \"No transitive gossip\" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So \"any future update for a node comes from that node itself, alive\" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transitive *row* relay — would have broken this; roster-only relay was chosen precisely to keep it.)\r\n\r\n### 4.11 Advertisement-epoch reset strands a node  `[REQ-HAZARD-EPOCH-RESET]`\r\n<!-- [doc->REQ-HAZARD-EPOCH-RESET] -->\r\n- **Failure:** a node whose advertisement-epoch counter resets (the durable `EpochSource` file lost/recreated) re-advertises with LOW epochs; peers hold a higher last-seen epoch for that `(endpoint, node)` lease and drop every fresh row as **stale** — the node advertises into a void until its counter outruns its own history. Nothing renders the cause: the node looks healthy locally, peers simply never update.\r\n- **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 epoch sub-check).\r\n- **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 above).\r\n- **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`.\r\n- **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.\r\n\r\n---\r\n\r\n## 5. Platform-specific\r\n\r\n### 5.1 Windows PID recycling false positives\r\n- **Failure:** recycled PID reads alive for the wrong process → orphan misclassification.\r\n- **Invariant:** anchor liveness on the stable parent/harness PID; minimal info.json for supervisor-owned perches; mtime grace window (≥60s) masks transient mismatches.\r\n- **spt-core mapping:** broker-held handles supersede PID polling for spt-hosted sessions; keep the grace window for harness-hosted.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; `src/common/list_filter.rs:168-175`.\r\n\r\n### 5.2 Windows EBUSY on atomic rename\r\n- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\r\n- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\r\n- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\r\n- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\r\n\r\n### 5.3 Git/subprocess timeout stamping\r\n- **Failure:** a hung subprocess (git on slow net) blocks the supervisor indefinitely.\r\n- **Invariant:** every metadata-producing subprocess has a timeout; timeout yields `None` + rate-limited stderr, never a hang.\r\n- **spt-core mapping:** generalize to all manifest-declared harness invocations (delegated commands, adapter updates) — timeouts mandatory.\r\n- **Sister cite:** `src/common/git.rs`.\r\n\r\n### 5.4 Windows UNC prefix in serialized paths\r\n- **Failure:** canonicalized `\\\\?\\C:\\...` serializes to `//?/C:/...` and fails `read_to_string`.\r\n- **Invariant:** strip the `\\\\?\\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.\r\n- **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.\r\n- **Sister cite:** `src/common/owlery.rs:377-384`.\r\n\r\n### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`\r\n- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.\r\n- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.\r\n- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).\r\n- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.\r\n\r\n<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->\r\n### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`\r\n- **Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::null()`. The caller's pipe read never sees EOF: the capturing caller **hangs forever** (unix is immune — pipe fds are `CLOEXEC`). Paid twice: daemon spawn (guarded at D4a-era `spawn_detached`), then again at M5-D3e when the mock-shell E2E hung `spt shell spawn` for h…\n- **Invariant:** every detach-spawn of a long-lived child runs with **`bInheritHandles = FALSE`** (`spt-daemon::daemon::detached_no_inherit`) — zero handles flow, whatever the pipe's depth in the ancestry. Stripping `HANDLE_FLAG_INHERIT` from the spawner's *std* handles is NOT sufficient: a grandparent capture's pipe sits in the handle table as a stray inheritable handle and still flows through every `bInheritHandles = TRUE` hop (the first guard shipped that way and was wedged by exactly this — a daemon spawned three layers deep held the pwsh-level pipe of the CI/test harness).\r\n- **spt-core mapping:** `spt-daemon::daemon::spawn_detached` (the daemon) and `spt-daemon::shellhost::launch_shell` (the relay-receipt shell binary). Any future long-lived detached spawn (manifest-template children included) must use the same no-inherit spawn.\r\n- **Source:** spt-core, M5-D3e (`shell_e2e.rs` hang, 2026-06-04, twice — once per guard generation); Rust `Command` restricts *its own* created stdio handles but a parent's inheritable handle table still flows.\r\n\r\n### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`\r\n<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->\r\n- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill (hit live, M7 acceptance 2026-06-06). **Linux:** a sudo'd command spawns a root daemon and/or root-owned state — and because sudo flips `$HOME`, the daemon can mint a *different node identity* in root's universe (the very key-flip that produced the 4.10 ghost r…\n- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run` entry guard catches a *directly* elevated `spt daemon` — Linux drops privileges in-process before touching any state; Windows respawns de-elevated and exits. When no unelevated identity exists to drop to (UAC disabled, genuine root login, SYSTEM), the daemon runs …\n- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\r\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\r\n\r\n<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\r\n### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\r\n- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).\r\n- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).\r\n- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 \"flag combo breaks spawn\" regression class); window-absence was verified live by process-watch capture.\r\n- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-runtime::ManifestRuntime::command_for` (the one shared builder behind `spawn_session` + `run_bounded_stdin` — the notif pump's `spawn_notif_command` and the live agent's psyche/echo/turn spawns), `spt-daemon::shellwake` (already guarded). The flag lives in each shared builder, not per call site, so the invariant holds for every ManifestRuntime spawn by construction.\r\n- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).\r\n\r\n### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`\r\n- **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 *environment-conditional* — green on any host up longer than the offset, red below it — so it slips local dev and only bites a fresh CI box or a just-rebooted machine.\r\n- **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.\r\n- **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.\r\n- **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`.\r\n- **Source:** spt-core CI failure, 2026-06-07 — Windows runner `hfenduleam` (just booted) panicked the peer pump at the v0.1.1 release gate.\r\n\r\n### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`\r\n- **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 work for the common install shape. Field-hit on KITSUBITO at the v0.1.1 ship.\r\n- **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-interactive or sudo-absent falls back to printing the runnable hint. Never emit a bare-name elevation instruction.\r\n- **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).\r\n- **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.\r\n- **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.\r\n- **Source:** spt-core field report, 2026-06-07 — `reavus@KITSUBITO`, `spt` in `~/.local/bin`; the absolute-path `sudo` invocation was confirmed working before the fix landed.\r\n\r\n### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\r\n- **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-elevating the already-elevated child (an infinite UAC/polkit loop). The user's UAC/polkit/sudo prompt is the ONLY consent gate — the mechanism must never bypass or widen it.\r\n- **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 the user (composes with the 5.7 de-elevation) and **never re-elevates**: `decide_elevation_path` returns `AlreadyElevated` whenever the process is `Elevated`, on every OS (loop-safety). The unprivileged parent never pipes/captures the elevated child's stdout acro…\n- **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 quoted token).\r\n- **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 composes with).\r\n- **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.\r\n\r\n<!-- [doc->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE] -->\r\n### 5.12 Native-PTY spawn of a bare program runs the wrong (non-PE) file on Windows  `[REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]`\r\n- **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:\\nvm4w\\nodejs\\ccs` 193 (operator, 2026-06-16). The same bites any harness/shell whose `[session.self]`/`[shell].spawn` names a `.cmd`/`.bat`/`.ps1`-backed command — `CreateProcessW` cannot execute a batch or PowerShell script directly.\r\n- **Invariant:** spt-term resolves the program ITSELF before handing it to `CommandBuilder`, bypassing portable-pty's `which`. A bare name is searched over `PATH` × `PATHEXT` (whose default order already prefers `.EXE`/`.COM` over `.BAT`/`.CMD`), then an extensionless fallback. A non-PE target is wrapped in its interpreter: `.cmd`/`.bat` → `cmd.exe /d /c <path>`, `.ps1` → `powershell -NoProfile -File <path>` (the wrap args precede the caller's args); a real executable spawns directly; an unresolvable name passes through unchanged (never makes a working case worse). Unix is a passthrough — `execve` honours a shebang on an extensionless script. Applied at the ONE `CommandBuilder` chokepoint (`PtySession::spawn_program_in`), so every broker harness + shell spaw…\n- **spt-core mapping:** `spt_term::winprog::{resolve_for_pty, resolve_in}` (the pure PATHEXT-precedence kernel + the Windows env wiring), wired into `spt_term::pty::PtySession::spawn_program_in`. Unit: `resolve_in` precedence (`.cmd`-over-shim, `.exe`-direct, explicit-extension, path-order, passthrough) [`winprog.rs`].\r\n- **Source:** field diagnosis 2026-06-16 (operator dogfood, `claude-spt:ccs` bringup) — doyle.\r\n\r\n<!-- [doc->REQ-HAZARD-PERCH-RECORD-POWER-LOSS] -->\r\n### 5.13 Atomic write leaves data un-synced before the rename → NUL zero-fill on power loss  `[REQ-HAZARD-PERCH-RECORD-POWER-LOSS]`\r\n- **Failure:** `atomic_write_bytes` was `fs::write(tmp)` + `rename(tmp, path)` with no `fsync`. The rename's directory **metadata** is journaled durable, but the tmp file's **data blocks** are still in the page cache. A hard reset (power loss, forced reboot) between the two flushes lands the rename but loses the data → the file reappears at its **full length filled with NUL**. Field incident: after a machine restart `owlery/hall-a/info.json` was 360 bytes of all-NUL (the nested psyche record 125 bytes all-NUL); `Get-Content` renders blank, `Format-Hex` shows the zero-fill. A wiped-but-present record then read as a live ONLINE endpoint for days (compounded by 5.14).\r\n- **Invariant:** for records whose loss is **unrecoverable**, flush data to stable storage **before** the rename publishes the name — `File::create(tmp)` → `write_all` → `sync_all()` → `rename_with_retry`. Then a crash yields the complete old file OR the complete new file, never a NUL husk. (A file of correct length that is ALL-NUL is the diagnostic tell of this class — a non-fsync'd write caught by a hard reset.) **Durability is SCOPED, not blanket.** `fsync`ing *every* atomic write serializes a durable flush behind each of the ~26 `atomic_write` callers — and for `info.json` the flush lands under the per-perch `.info.lock` (the **W1b lock-across-fsync wedge shape**, §6-era) — which stalls daemon bringup 4–6× so the endpoint misses its ONLINE window. Scope …\n- **spt-core mapping:** `spt_store::atomic::atomic_write_bytes_durable` / `atomic_write_string_durable` are the opt-in durable siblings; the default `atomic_write_bytes` / `atomic_write_string` stay non-durable. Durable callers are exactly `spt_store::info::write_info` (the perch record — the hall-a surface), `spt_store::nodeid` (the node seed — corrupt is node-bricking, never regenerated), and `spt_daemon::machineid` (minted-once machine id — a NUL husk would silently re-mint a different id). Reader-side handling of an already-corrupt record is the sibling 5.14. **Canary:** `attach_wedge_e2e` is the regression guard — a blanket fsync fails it (bringup misses the 20s ONLINE budget); keep durability scoped so it stays green.\r\n- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; blanket-fsync perf regression pinned + scoped by the gate rig (doyle), same date.\r\n\r\n<!-- [doc->REQ-HAZARD-CORRUPT-PERCH-COHERENCE] -->\r\n### 5.14 Corrupt info.json read as ABSENT → fail-open readers gossip a wiped perch ONLINE  `[REQ-HAZARD-CORRUPT-PERCH-COHERENCE]`\r\n- **Failure:** three readers each collapsed a **corrupt** (present-but-unparseable) `info.json` into their fail-open ABSENT default, so a NUL-wiped perch (5.13) read as permanently live: `is_perch_alive` returned `true` (unreadable ⇒ interim-alive), `advertised_status` then saw alive + no resting record ⇒ `Active`, and the daemon self-gossiped that Active row every round (epoch 173k+). Result: `hall-a`, dead since a machine restart, showed ONLINE in `spt whoami` and on every remote picker.\r\n- **Invariant:** CORRUPT ≠ ABSENT. A record that EXISTS but stays unparseable across the retry budget is a **destroyed** record — never a live endpoint. `is_perch_alive` reads corrupt ⇒ **not alive** (ABSENT keeps interim-alive parity — the ONE case that stays true); `advertised_status` then lands corrupt in the cold arm ⇒ **Suspended**, never Active/Dormant. Readers that decide liveness/status must branch on the tri-state (present / absent / corrupt), not an `Option` that fuses the last two.\r\n- **spt-core mapping:** `spt_store::liveness::{read_raw_state, is_perch_alive}` (the tri-state kernel) and `spt_daemon::registryhost::advertised_status` (cascades to Suspended off the alive fix). `list_self_perch_ids` deliberately still lists a corrupt dir by existence — that is the *visibility* the local-roster fix (counter-39 #3) relies on, not a liveness claim. Candidate sibling seam `is_registry_entry_alive` (corrupt hosted row falls to a daemon-pid probe) is parked — it does not produce the ONLINE gossip.\r\n- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; sequel to the v0.17.0 W4 presence-truth fix (cold ⇒ Suspended) which a corrupt perch bypassed via false-alive.\r\n\r\n<!-- [doc->REQ-HAZARD-ATOMIC-TMP-COLLISION] -->\r\n### 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file)  `[REQ-HAZARD-ATOMIC-TMP-COLLISION]`\r\n- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `BindError::Io` *after* having written the record, mock-session's bind-is-fatal check `exit(1)`s, the harness dies, and the endpoint never reaches ONLINE. A microsecond window that existed forever (bare `fs::write` + rename back-to-back) — the 5.13 `sync_all` widened c…\n- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind to them.\r\n- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\r\n- **Source:** RCA 2026-07-01 (doyle gate rig) — write-trace side-channel + `cmd_bind` step probes pinned `PROBE cmd_bind establish ERR: Io(os error 2)`; explains the counter-39 gate-1 blanket failure, the dummy_harness ONLINE-race DIAGs, and the nondeterminism. Lesson: a widened timing window doesn't create a race, it reveals one.\r\n\r\n<!-- [doc->REQ-HAZARD-INFO-RMW-LOST-UPDATE] -->\r\n### 5.16 Unlocked whole-record info.json write races a locked RMW → silent lost update  `[REQ-HAZARD-INFO-RMW-LOST-UPDATE]`\r\n- **Failure:** `mutate_info` serializes its read→mutate→write under the per-perch `.info.lock` sentinel, but `establish_perch` (`spt::api::startup`) did read→conflict-check→`write_info` with **no lock**. At bind the two writers race (~700µs apart): the daemon RMW reads the PRE-BIND record, bind's `write_info` renames the full record in (`state=live_agent`, `controllable=Some(true)`, `session_id`), then the RMW writes its STALE pre-bind snapshot back plus a `status=online` stamp. The surviving `info.json` is a **pre-bind shape carrying `status=online`** — bringup passes (it sees ONLINE), the broker serves the session, then on child death `reconcile_hosted_liveness` reads `state != \"live_agent\"` (or `controllable != Some(true)`) and *silently `continue`s* — th…\n- **Invariant:** every `info.json` writer serializes under the one per-perch `.info.lock`. A whole-record write (`write_info`) takes the lock exactly as the RMW (`mutate_info`) does — a unique tmp only makes concurrent writes *last-writer-wins*, which is safe ONLY if the writers are serialized (cross-ref 5.15). A multi-step read→check→write (bind's establish) must hold ONE lock acquisition across all three (a true compare-and-set) — check-then-write with the lock dropped in between still interleaves a stamp. Readers stay lock-free (the atomic rename already gives a complete old-or-new record). Holding `.info.lock` across the durable fsync is ms-scale on these infrequent record writes (the same thing every `mutate_info` already does), NOT the per-keystroke W1…\n- **spt-core mapping:** `spt_store::info` — `write_info` now acquires the sentinel then calls a private `write_info_unlocked` (and `mutate_info` — the public RMW primitive — calls the unlocked writer while holding its own lock, so no double-lock deadlock); `info::establish_locked(perch, build)` runs the bind's read→check→build→write as one locked CAS, called by `spt::api::startup::establish_perch`. The other read-modify-write callers were audited and the two that mutate a load-bearing field were converted to the `mutate_info` CAS: `spt::api::reporting::cmd_boundary` (rc-rebind session-id rotation) and `spt_store::home::adopt_for_unset` (home-subnet adoption; the `is_none()` re-check runs inside the closure). **Deliberately parked (known unlocked-RMW residue,…\n- **Source:** RCA 2026-07-01 (doyle gate rig) — differential (baseline PASS / 77beeac+baseline-atomic PASS / fe385f5 2/3 FAIL) isolated the atomic rework as the delta; `reconcile_hosted_liveness` emitting ZERO `LIVENESS_RECONCILE_OFFLINE` lines for the dead victim over 20s pinned the silent-skip. The unique-tmp fix worked (bringup + serve fine) but exposed this deeper pairing.\r\n\r\n---\r\n\r\n## 6. Documented regressions (non-obvious invariants)\r\n\r\n### 6.1 No flat/nested perch siblings; resolver-routed paths\r\n- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.\r\n- **Invariant:** one path resolver; never create divergent siblings.\r\n- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.\r\n- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.\r\n\r\n### 6.2 Soft-cleanup preserves state, removes `ready`\r\n- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.\r\n- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.\r\n- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.\r\n- **Sister cite:** `src/owl/stop.rs`.\r\n\r","truncated":true,"truncatedBy":"lines","totalLines":736,"totalBytes":57468,"outputLines":300,"outputBytes":57468,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"# Known Hazards\r\n\r\nHard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** — the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.\r\n\r\n**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.\r\n\r\n---\r\n\r\n## 1. Race conditions & ordering\r\n\r\n### 1.1 Phantom INIT_SIGNOFF after grace period\r\n- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spooled and drains on the next iteration → teardown despite a live Self.\r\n- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.\r\n- **spt-core mapping:** in-daemon now (no separate wrapper), but the ordering invariant is identical — orphan/teardown logic must re-evaluate liveness after the grace wait, not before enqueue.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.\r\n\r\n### 1.2 Poll-rewrite race & info.json mid-write reads\r\n- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\r\n- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).\r\n- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\r\n- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\r\n\r\n### 1.3 Stale `index.lock` wedge from prior git crash\r\n- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.\r\n- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.\r\n- **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.\r\n- **Sister cite:** CHANGELOG v1.11.20 \"Stale `index.lock`\"; `src/common/git.rs`.\r\n\r\n### 1.4 Deferred spool rows must not leak to the event stream\r\n- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.\r\n- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.\r\n- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.\r\n- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.\r\n\r\n### 1.5 Worker (working-perch) lifecycle path consistency\r\n- **Failure:** subagent-start creates the perch at one path layout; later hooks read it at another → not found; stop-hook scan misses nested perches.\r\n- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.\r\n- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.\r\n- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.\r\n\r\n---\r\n\r\n## 2. Identity & session-binding\r\n\r\n### 2.1 Parent PID over ephemeral poll PID\r\n- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).\r\n- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.\r\n- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\r\n\r\n### 2.2 Stdin session_id precedence over env\r\n- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.\r\n- **Invariant:** stdin-provided session_id wins; env is fallback only.\r\n- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.\r\n- **Sister cite:** CHANGELOG v1.35.1 \"IN-05\"; `hook_subagent_start.rs:40-51`.\r\n\r\n### 2.3 Binary-handoff argv schema must stay backward-compatible\r\n- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.\r\n- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.\r\n- **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.\r\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\r\n\r\n### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\r\n- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.\r\n- **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\".\r\n- **spt-core mapping:** carries to the daemon's per-instance generation tracking.\r\n- **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 all session continuity by **querying the broker** (`Brain::resume_sessions` over the broker's cursor-of-record), never a frame. `BrainState` / `Brain::handoff` / `Brain::snapshot` remain `pub` and compiled **only for the integration tests** (handoff/idempotent/da…\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\r\n\r\n### 2.5 Daemon-hosted endpoints have no dedicated liveness PID\r\n- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `is_process_alive(pid)` reads \"alive\" for a torn-down endpoint as long as the daemon runs — while `clean_stale_entries` (dead-pid deletion) can no longer distinguish a dead endpoint from a live one. The 2.1/5.1 liveness models do **not** cover this third categor…\n- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`info.json` carries daemon-managed `status`, capability resolved by `adapter_name` — CONTEXT \"Shell… Not in the subnet registry\") and extends it to daemon-hosted *agent* perches.\r\n- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let the per-pid assumption leak into new call sites.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\r\n\r\n---\r\n\r\n## 3. Lifecycle\r\n\r\n### 3.1 Ephemeral perch cleanup on every `ring` exit path\r\n- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.\r\n- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.\r\n- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.\r\n- **Sister cite:** `src/owl/ring.rs:58-294`.\r\n\r\n### 3.2 Stale signoff sentinel must not kill a fresh start\r\n- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.\r\n- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.\r\n- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.\r\n- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.\r\n\r\n### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF\r\n- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.\r\n- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.\r\n- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.\r\n\r\n---\r\n\r\n## 4. Wire / transport\r\n\r\n### 4.1 Envelope HTML-entity codec ordering — `&amp;` decoded LAST\r\n- **Failure:** decoding `&amp;`-entity before the others double-decodes nested entities (`&amp;amp;lt;` → wrong result).\r\n- **Invariant:** ENCODE order amp→first … `<br>`→last; DECODE order `<br>`→first … amp→**last** (`&lt;`,`&gt;`,`&quot;`, then `&amp;`). One sole decode site (at the LLM/stdin boundary); the parser never decodes.\r\n- **spt-core mapping:** `spt-proto` owns the envelope grammar (public SDK, semver + wire-version). This codec contract is a copy-verbatim commodity item (ADR-0001) and a public-API conformance test.\r\n- **Sister cite:** `src/owl/poll.rs:1-73`; `src/common/envelope.rs`.\r\n- **CR-linesafety `[REQ-HAZARD-ENVELOPE-CR-LINESAFE]`:** the EVENT is LINE-FRAMED, so the codec must neutralize raw `\\r` too — `event_body_escape` folds CRLF/lone-CR to `\\n` (→`<br>`) **before** framing. **Failure (field, 2026-06-08):** a cross-node `spt send` from Windows (`echo` → CRLF) carried a raw `\\r` into the single-line envelope; the receiver terminal did a CR→column-0 overwrite (`</EVENT>` clobbered `<EVENT t`). `\\r` was never line-representable here, so normalizing it is robustness, not an ADR-0001 wire divergence (decoder + amp-last untouched). Belt-and-suspenders: `spt send`/`ring` trim stdin like `notify`.\r\n\r\n### 4.2 Two-slice envelope parser is panic-free and tolerant\r\n- **Failure:** malformed envelope (unclosed/misordered/nested tags) panics or drops output.\r\n- **Invariant:** tags case-sensitive, all optional; no tags → whole body to live slot; unclosed → None for that tag; out-of-order → both still extracted; nested unknown tags preserved verbatim; zero `unwrap` on parsed text.\r\n- **spt-core mapping:** `spt-proto` parser; property-test the robustness rules.\r\n- **Sister cite:** `src/common/envelope.rs:64-92`; tests 99-207.\r\n\r\n### 4.3 Registry stale-entry cleanup precedes lookup\r\n- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\r\n- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\r\n- **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.\r\n- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\r\n\r\n### 4.4 Deferred rows survive poll drain\r\n- **Failure:** poll `drain_all` flushes a deferred (spool-only) message meant for a hook consumer → message lost.\r\n- **Invariant:** deferred rows are never flushed by the event-stream drain; only `drain_non_deferred_*` / `peek_all` touch them.\r\n- **Sister cite:** CHANGELOG v1.11.20; `src/common/spool.rs`. (See also 1.4.)\r\n\r\n### 4.5 Inbox legacy compat must not double-deliver\r\n- **Failure:** message surfaced via both spool (durable) and legacy inbox files → duplicate or racing delivery.\r\n- **Invariant:** spool is the sole read path at poll time; inbox is write-for-compat only and never read.\r\n- **spt-core mapping:** clean-room — likely drop the legacy inbox entirely. If kept for any compat, preserve \"never read at drain time.\"\r\n- **Sister cite:** `src/common/inbox.rs`.\r\n\r\n### 4.6 Addressable-id charset reserves the address delimiters\r\n<!-- [doc->REQ-HAZARD-ID-CHARSET] -->\r\n- **Failure:** a bare endpoint id that contains `:` or `@` (or a path separator / whitespace / control char) makes the canonical qualified address `[subnet:]id[@node]` (ADR-0006 / REQ-INST-10) ambiguous to parse, and lets a name smuggle into a perch directory path. Once permissive ids exist in the wild, tightening later needs a migration.\r\n- **Invariant:** every addressable id/name is validated to `[A-Za-z0-9_-]` + Hiragana/Katakana/CJK only, length `1..=64`, **at every creation seam** (`ready` start, `api bind`, `api listen`, `api worker-start`). `:` and `@` are permanently reserved as address delimiters; reads of existing perches are never re-validated. Enforce now (pre-M3/M4) so no permissive id-data accumulates.\r\n- **spt-core mapping:** `spt_proto::id::validate_endpoint_id`; called at the four creation seams. The existing Psyche (`<parent>-psyche`) / Worker (`<parent>-w<N>`) suffix scheme uses only `-` + alphanumerics, so composite ids validate.\r\n\r\n### 4.7 Concurrent SQLite openers must not fail with \"database is locked\"\r\n<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->\r\n- **Failure:** two endpoints on one machine open the same SQLite store at once (e.g. two `ReadyAgent::start` calls registering simultaneously) and one fails outright with `SQLITE_BUSY` / \"database is locked\" → spurious registration/spool failure. Surfaced as a parallel-test flake in `two_agents_exchange_message_tcp_and_spool`, but the bug is real concurrency, not test-only.\r\n- **Invariant:** `busy_timeout` is set **before** any lock-taking statement on every connection. Switching `journal_mode=WAL` takes a brief exclusive lock; with the default 0ms timeout it fails immediately under contention, so the pragma order is load-bearing: `Connection::open` → `busy_timeout` → `journal_mode=WAL` → `CREATE TABLE …`. WAL alone is insufficient (concurrent *writers* still serialize; they must *wait*, not error).\r\n- **spt-core mapping:** `spt_store::registry::open_registry` + `spt_store::spool::open_spool_at`; both set `busy_timeout=5000` first. Any future SQLite store (history Path B, instance registry) must follow the same ordering.\r\n\r\n### 4.8 Registry merge ordered by epoch, never wall-clock (red-team #8)\r\n<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->\r\n- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or \"last write wins\"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.\r\n- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-clock is at most a human tiebreaker hint inside a flagged conflict, never the ordering authority. The same epoch source unifies with the D6 sync-precedence concurrent-write detection (#7).\r\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.\r\n\r\n### 4.9 SQLite stores must create their parent dir — SQLite won't\r\n<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->\r\n- **Failure:** `Connection::open` creates the database FILE but never its parent DIRECTORY. On a fresh home (first boot, fresh CI `_work` dir) a registry op that runs before any perch-creating op (`create_dir_all` side effects) fails `SQLITE_CANTOPEN` — \"unable to open database file …owlery\\.registry\". Timing-dependent: whichever code path touches the home first decides the outcome, so it surfaces as a parallel-test flake (bind-first tests losing the dir-creation race to perch-first tests). Bit the hfenduleam CI leg twice (2026-06-03/04, four spt-msg unit tests at once on the second strike) before being run to ground; a slow runner filesystem (AV scanning fresh dirs) widens the window but is not the cause.\r\n- **Invariant:** every SQLite store's open path `create_dir_all`s its parent dir itself, best-effort, before `Connection::open` — never relying on another subsystem having materialized the home first. (Mirrors the spool, which always did this; the registry didn't.)\r\n- **spt-core mapping:** `spt_store::registry::open_registry` (`create_dir_all(owlery)` before open). `spt_store::spool::open_spool_at` already creates its perch dir. Any future SQLite store must do the same — pair this with the 4.7 pragma ordering on every new store.\r\n\r\n### 4.10 Dead node identities leave immortal registry rows  `[REQ-HAZARD-REGISTRY-GHOST-ROWS]`\r\n<!-- [doc->REQ-HAZARD-REGISTRY-GHOST-ROWS] -->\r\n- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the \"node\" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the dead identity and refuses with a **phantom `AcrossNodes` ambiguity** — unfixable by the user, because no qualifier reaches a node that no longer exists. Hit live in the M7 acceptance run (2026-06-06): gravity paired under two identities (09ef…, then 03854a… af…\n- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future update for a node comes from that node itself, alive, re-inserting from its durable `EpochSource` within one cadence — there is no lagging third-party replay to mis-order against. A merely-offline node loses its rows after the window and reconverges on return; meanwhi…\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.\r\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Ghost registry row eviction\"); the AMBIGUOUS render fix rode along.\r\n- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. \"No transitive gossip\" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So \"any future update for a node comes from that node itself, alive\" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transitive *row* relay — would have broken this; roster-only relay was chosen precisely to keep it.)\r\n\r\n### 4.11 Advertisement-epoch reset strands a node  `[REQ-HAZARD-EPOCH-RESET]`\r\n<!-- [doc->REQ-HAZARD-EPOCH-RESET] -->\r\n- **Failure:** a node whose advertisement-epoch counter resets (the durable `EpochSource` file lost/recreated) re-advertises with LOW epochs; peers hold a higher last-seen epoch for that `(endpoint, node)` lease and drop every fresh row as **stale** — the node advertises into a void until its counter outruns its own history. Nothing renders the cause: the node looks healthy locally, peers simply never update.\r\n- **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 epoch sub-check).\r\n- **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 above).\r\n- **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`.\r\n- **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.\r\n\r\n---\r\n\r\n## 5. Platform-specific\r\n\r\n### 5.1 Windows PID recycling false positives\r\n- **Failure:** recycled PID reads alive for the wrong process → orphan misclassification.\r\n- **Invariant:** anchor liveness on the stable parent/harness PID; minimal info.json for supervisor-owned perches; mtime grace window (≥60s) masks transient mismatches.\r\n- **spt-core mapping:** broker-held handles supersede PID polling for spt-hosted sessions; keep the grace window for harness-hosted.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; `src/common/list_filter.rs:168-175`.\r\n\r\n### 5.2 Windows EBUSY on atomic rename\r\n- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\r\n- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\r\n- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\r\n- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\r\n\r\n### 5.3 Git/subprocess timeout stamping\r\n- **Failure:** a hung subprocess (git on slow net) blocks the supervisor indefinitely.\r\n- **Invariant:** every metadata-producing subprocess has a timeout; timeout yields `None` + rate-limited stderr, never a hang.\r\n- **spt-core mapping:** generalize to all manifest-declared harness invocations (delegated commands, adapter updates) — timeouts mandatory.\r\n- **Sister cite:** `src/common/git.rs`.\r\n\r\n### 5.4 Windows UNC prefix in serialized paths\r\n- **Failure:** canonicalized `\\\\?\\C:\\...` serializes to `//?/C:/...` and fails `read_to_string`.\r\n- **Invariant:** strip the `\\\\?\\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.\r\n- **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.\r\n- **Sister cite:** `src/common/owlery.rs:377-384`.\r\n\r\n### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`\r\n- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.\r\n- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.\r\n- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).\r\n- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.\r\n\r\n<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->\r\n### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`\r\n- **Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::null()`. The caller's pipe read never sees EOF: the capturing caller **hangs forever** (unix is immune — pipe fds are `CLOEXEC`). Paid twice: daemon spawn (guarded at D4a-era `spawn_detached`), then again at M5-D3e when the mock-shell E2E hung `spt shell spawn` for h…\n- **Invariant:** every detach-spawn of a long-lived child runs with **`bInheritHandles = FALSE`** (`spt-daemon::daemon::detached_no_inherit`) — zero handles flow, whatever the pipe's depth in the ancestry. Stripping `HANDLE_FLAG_INHERIT` from the spawner's *std* handles is NOT sufficient: a grandparent capture's pipe sits in the handle table as a stray inheritable handle and still flows through every `bInheritHandles = TRUE` hop (the first guard shipped that way and was wedged by exactly this — a daemon spawned three layers deep held the pwsh-level pipe of the CI/test harness).\r\n- **spt-core mapping:** `spt-daemon::daemon::spawn_detached` (the daemon) and `spt-daemon::shellhost::launch_shell` (the relay-receipt shell binary). Any future long-lived detached spawn (manifest-template children included) must use the same no-inherit spawn.\r\n- **Source:** spt-core, M5-D3e (`shell_e2e.rs` hang, 2026-06-04, twice — once per guard generation); Rust `Command` restricts *its own* created stdio handles but a parent's inheritable handle table still flows.\r\n\r\n### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`\r\n<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->\r\n- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill (hit live, M7 acceptance 2026-06-06). **Linux:** a sudo'd command spawns a root daemon and/or root-owned state — and because sudo flips `$HOME`, the daemon can mint a *different node identity* in root's universe (the very key-flip that produced the 4.10 ghost r…\n- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run` entry guard catches a *directly* elevated `spt daemon` — Linux drops privileges in-process before touching any state; Windows respawns de-elevated and exits. When no unelevated identity exists to drop to (UAC disabled, genuine root login, SYSTEM), the daemon runs …\n- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\r\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\r\n\r\n<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\r\n### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\r\n- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).\r\n- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).\r\n- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 \"flag combo breaks spawn\" regression class); window-absence was verified live by process-watch capture.\r\n- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-runtime::ManifestRuntime::command_for` (the one shared builder behind `spawn_session` + `run_bounded_stdin` — the notif pump's `spawn_notif_command` and the live agent's psyche/echo/turn spawns), `spt-daemon::shellwake` (already guarded). The flag lives in each shared builder, not per call site, so the invariant holds for every ManifestRuntime spawn by construction.\r\n- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).\r\n\r\n### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`\r\n- **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 *environment-conditional* — green on any host up longer than the offset, red below it — so it slips local dev and only bites a fresh CI box or a just-rebooted machine.\r\n- **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.\r\n- **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.\r\n- **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`.\r\n- **Source:** spt-core CI failure, 2026-06-07 — Windows runner `hfenduleam` (just booted) panicked the peer pump at the v0.1.1 release gate.\r\n\r\n### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`\r\n- **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 work for the common install shape. Field-hit on KITSUBITO at the v0.1.1 ship.\r\n- **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-interactive or sudo-absent falls back to printing the runnable hint. Never emit a bare-name elevation instruction.\r\n- **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).\r\n- **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.\r\n- **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.\r\n- **Source:** spt-core field report, 2026-06-07 — `reavus@KITSUBITO`, `spt` in `~/.local/bin`; the absolute-path `sudo` invocation was confirmed working before the fix landed.\r\n\r\n### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\r\n- **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-elevating the already-elevated child (an infinite UAC/polkit loop). The user's UAC/polkit/sudo prompt is the ONLY consent gate — the mechanism must never bypass or widen it.\r\n- **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 the user (composes with the 5.7 de-elevation) and **never re-elevates**: `decide_elevation_path` returns `AlreadyElevated` whenever the process is `Elevated`, on every OS (loop-safety). The unprivileged parent never pipes/captures the elevated child's stdout acro…\n- **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 quoted token).\r\n- **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 composes with).\r\n- **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.\r\n\r\n<!-- [doc->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE] -->\r\n### 5.12 Native-PTY spawn of a bare program runs the wrong (non-PE) file on Windows  `[REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]`\r\n- **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:\\nvm4w\\nodejs\\ccs` 193 (operator, 2026-06-16). The same bites any harness/shell whose `[session.self]`/`[shell].spawn` names a `.cmd`/`.bat`/`.ps1`-backed command — `CreateProcessW` cannot execute a batch or PowerShell script directly.\r\n- **Invariant:** spt-term resolves the program ITSELF before handing it to `CommandBuilder`, bypassing portable-pty's `which`. A bare name is searched over `PATH` × `PATHEXT` (whose default order already prefers `.EXE`/`.COM` over `.BAT`/`.CMD`), then an extensionless fallback. A non-PE target is wrapped in its interpreter: `.cmd`/`.bat` → `cmd.exe /d /c <path>`, `.ps1` → `powershell -NoProfile -File <path>` (the wrap args precede the caller's args); a real executable spawns directly; an unresolvable name passes through unchanged (never makes a working case worse). Unix is a passthrough — `execve` honours a shebang on an extensionless script. Applied at the ONE `CommandBuilder` chokepoint (`PtySession::spawn_program_in`), so every broker harness + shell spaw…\n- **spt-core mapping:** `spt_term::winprog::{resolve_for_pty, resolve_in}` (the pure PATHEXT-precedence kernel + the Windows env wiring), wired into `spt_term::pty::PtySession::spawn_program_in`. Unit: `resolve_in` precedence (`.cmd`-over-shim, `.exe`-direct, explicit-extension, path-order, passthrough) [`winprog.rs`].\r\n- **Source:** field diagnosis 2026-06-16 (operator dogfood, `claude-spt:ccs` bringup) — doyle.\r\n\r\n<!-- [doc->REQ-HAZARD-PERCH-RECORD-POWER-LOSS] -->\r\n### 5.13 Atomic write leaves data un-synced before the rename → NUL zero-fill on power loss  `[REQ-HAZARD-PERCH-RECORD-POWER-LOSS]`\r\n- **Failure:** `atomic_write_bytes` was `fs::write(tmp)` + `rename(tmp, path)` with no `fsync`. The rename's directory **metadata** is journaled durable, but the tmp file's **data blocks** are still in the page cache. A hard reset (power loss, forced reboot) between the two flushes lands the rename but loses the data → the file reappears at its **full length filled with NUL**. Field incident: after a machine restart `owlery/hall-a/info.json` was 360 bytes of all-NUL (the nested psyche record 125 bytes all-NUL); `Get-Content` renders blank, `Format-Hex` shows the zero-fill. A wiped-but-present record then read as a live ONLINE endpoint for days (compounded by 5.14).\r\n- **Invariant:** for records whose loss is **unrecoverable**, flush data to stable storage **before** the rename publishes the name — `File::create(tmp)` → `write_all` → `sync_all()` → `rename_with_retry`. Then a crash yields the complete old file OR the complete new file, never a NUL husk. (A file of correct length that is ALL-NUL is the diagnostic tell of this class — a non-fsync'd write caught by a hard reset.) **Durability is SCOPED, not blanket.** `fsync`ing *every* atomic write serializes a durable flush behind each of the ~26 `atomic_write` callers — and for `info.json` the flush lands under the per-perch `.info.lock` (the **W1b lock-across-fsync wedge shape**, §6-era) — which stalls daemon bringup 4–6× so the endpoint misses its ONLINE window. Scope …\n- **spt-core mapping:** `spt_store::atomic::atomic_write_bytes_durable` / `atomic_write_string_durable` are the opt-in durable siblings; the default `atomic_write_bytes` / `atomic_write_string` stay non-durable. Durable callers are exactly `spt_store::info::write_info` (the perch record — the hall-a surface), `spt_store::nodeid` (the node seed — corrupt is node-bricking, never regenerated), and `spt_daemon::machineid` (minted-once machine id — a NUL husk would silently re-mint a different id). Reader-side handling of an already-corrupt record is the sibling 5.14. **Canary:** `attach_wedge_e2e` is the regression guard — a blanket fsync fails it (bringup misses the 20s ONLINE budget); keep durability scoped so it stays green.\r\n- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; blanket-fsync perf regression pinned + scoped by the gate rig (doyle), same date.\r\n\r\n<!-- [doc->REQ-HAZARD-CORRUPT-PERCH-COHERENCE] -->\r\n### 5.14 Corrupt info.json read as ABSENT → fail-open readers gossip a wiped perch ONLINE  `[REQ-HAZARD-CORRUPT-PERCH-COHERENCE]`\r\n- **Failure:** three readers each collapsed a **corrupt** (present-but-unparseable) `info.json` into their fail-open ABSENT default, so a NUL-wiped perch (5.13) read as permanently live: `is_perch_alive` returned `true` (unreadable ⇒ interim-alive), `advertised_status` then saw alive + no resting record ⇒ `Active`, and the daemon self-gossiped that Active row every round (epoch 173k+). Result: `hall-a`, dead since a machine restart, showed ONLINE in `spt whoami` and on every remote picker.\r\n- **Invariant:** CORRUPT ≠ ABSENT. A record that EXISTS but stays unparseable across the retry budget is a **destroyed** record — never a live endpoint. `is_perch_alive` reads corrupt ⇒ **not alive** (ABSENT keeps interim-alive parity — the ONE case that stays true); `advertised_status` then lands corrupt in the cold arm ⇒ **Suspended**, never Active/Dormant. Readers that decide liveness/status must branch on the tri-state (present / absent / corrupt), not an `Option` that fuses the last two.\r\n- **spt-core mapping:** `spt_store::liveness::{read_raw_state, is_perch_alive}` (the tri-state kernel) and `spt_daemon::registryhost::advertised_status` (cascades to Suspended off the alive fix). `list_self_perch_ids` deliberately still lists a corrupt dir by existence — that is the *visibility* the local-roster fix (counter-39 #3) relies on, not a liveness claim. Candidate sibling seam `is_registry_entry_alive` (corrupt hosted row falls to a daemon-pid probe) is parked — it does not produce the ONLINE gossip.\r\n- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; sequel to the v0.17.0 W4 presence-truth fix (cold ⇒ Suspended) which a corrupt perch bypassed via false-alive.\r\n\r\n<!-- [doc->REQ-HAZARD-ATOMIC-TMP-COLLISION] -->\r\n### 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file)  `[REQ-HAZARD-ATOMIC-TMP-COLLISION]`\r\n- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `BindError::Io` *after* having written the record, mock-session's bind-is-fatal check `exit(1)`s, the harness dies, and the endpoint never reaches ONLINE. A microsecond window that existed forever (bare `fs::write` + rename back-to-back) — the 5.13 `sync_all` widened c…\n- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind to them.\r\n- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\r\n- **Source:** RCA 2026-07-01 (doyle gate rig) — write-trace side-channel + `cmd_bind` step probes pinned `PROBE cmd_bind establish ERR: Io(os error 2)`; explains the counter-39 gate-1 blanket failure, the dummy_harness ONLINE-race DIAGs, and the nondeterminism. Lesson: a widened timing window doesn't create a race, it reveals one.\r\n\r\n<!-- [doc->REQ-HAZARD-INFO-RMW-LOST-UPDATE] -->\r\n### 5.16 Unlocked whole-record info.json write races a locked RMW → silent lost update  `[REQ-HAZARD-INFO-RMW-LOST-UPDATE]`\r\n- **Failure:** `mutate_info` serializes its read→mutate→write under the per-perch `.info.lock` sentinel, but `establish_perch` (`spt::api::startup`) did read→conflict-check→`write_info` with **no lock**. At bind the two writers race (~700µs apart): the daemon RMW reads the PRE-BIND record, bind's `write_info` renames the full record in (`state=live_agent`, `controllable=Some(true)`, `session_id`), then the RMW writes its STALE pre-bind snapshot back plus a `status=online` stamp. The surviving `info.json` is a **pre-bind shape carrying `status=online`** — bringup passes (it sees ONLINE), the broker serves the session, then on child death `reconcile_hosted_liveness` reads `state != \"live_agent\"` (or `controllable != Some(true)`) and *silently `continue`s* — th…\n- **Invariant:** every `info.json` writer serializes under the one per-perch `.info.lock`. A whole-record write (`write_info`) takes the lock exactly as the RMW (`mutate_info`) does — a unique tmp only makes concurrent writes *last-writer-wins*, which is safe ONLY if the writers are serialized (cross-ref 5.15). A multi-step read→check→write (bind's establish) must hold ONE lock acquisition across all three (a true compare-and-set) — check-then-write with the lock dropped in between still interleaves a stamp. Readers stay lock-free (the atomic rename already gives a complete old-or-new record). Holding `.info.lock` across the durable fsync is ms-scale on these infrequent record writes (the same thing every `mutate_info` already does), NOT the per-keystroke W1…\n- **spt-core mapping:** `spt_store::info` — `write_info` now acquires the sentinel then calls a private `write_info_unlocked` (and `mutate_info` — the public RMW primitive — calls the unlocked writer while holding its own lock, so no double-lock deadlock); `info::establish_locked(perch, build)` runs the bind's read→check→build→write as one locked CAS, called by `spt::api::startup::establish_perch`. The other read-modify-write callers were audited and the two that mutate a load-bearing field were converted to the `mutate_info` CAS: `spt::api::reporting::cmd_boundary` (rc-rebind session-id rotation) and `spt_store::home::adopt_for_unset` (home-subnet adoption; the `is_none()` re-check runs inside the closure). **Deliberately parked (known unlocked-RMW residue,…\n- **Source:** RCA 2026-07-01 (doyle gate rig) — differential (baseline PASS / 77beeac+baseline-atomic PASS / fe385f5 2/3 FAIL) isolated the atomic rework as the delta; `reconcile_hosted_liveness` emitting ZERO `LIVENESS_RECONCILE_OFFLINE` lines for the dead victim over 20s pinned the silent-skip. The unique-tmp fix worked (bringup + serve fine) but exposed this deeper pairing.\r\n\r\n---\r\n\r\n## 6. Documented regressions (non-obvious invariants)\r\n\r\n### 6.1 No flat/nested perch siblings; resolver-routed paths\r\n- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.\r\n- **Invariant:** one path resolver; never create divergent siblings.\r\n- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.\r\n- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.\r\n\r\n### 6.2 Soft-cleanup preserves state, removes `ready`\r\n- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.\r\n- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.\r\n- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.\r\n- **Sister cite:** `src/owl/stop.rs`.\r\n\r\n…\n\n…\n<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->","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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,null,364,null,735]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\KNOWN-HAZARDS.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":736,"totalBytes":57468,"outputLines":300,"outputBytes":57468,"shownRange":{"start":1,"end":300},"nextOffset":301},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195753039}}
{"type":"message","id":"ada4e590","parentId":"28848451","timestamp":"2026-07-16T09:55:53.043Z","message":{"role":"toolResult","toolCallId":"call_BcHSYYk9Gf6WMWVS1hBSSNIP|fc_008390f508d1d09b016a58aaa8e494819abab7e9a17d562b00","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## broker.rs\n 59|    NetStreamsReply, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n*60|    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n 61|    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n 62|    KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,\n 63|    KIND_NET_SENT,\n...\n 108|// [impl->REQ-HAZARD-VIEWER-ISOLATION]\n*109|fn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {\n 110|    r.is_err()\n 111|}\n 112|\n...\n 117|/// dropped (the `tx` closes → the writer ends) and removed from the map.\n*118|struct ViewerSink {\n 119|    /// The bounded live-frame queue to this viewer's writer thread.\n 120|    tx: SyncSender<Envelope>,\n 121|    /// The viewer's IPC send half — kept ONLY for identity matching on detach\n...\n 180|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*181|fn brain_write_deadline() -> Duration {\n 182|    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n 183|        Ok(ms) => ms\n 184|            .parse::<u64>()\n...\n 205|/// strands the endpoint permanently claimed.\n*206|struct WakeClaimGuard<'a> {\n 207|    set: &'a Mutex<HashSet<String>>,\n 208|    endpoint: String,\n 209|}\n 210|\n*211|impl Drop for WakeClaimGuard<'_> {\n 212|    fn drop(&mut self) {\n 213|        recover(self.set).remove(&self.endpoint);\n 214|    }\n...\n 224|#[derive(Debug, PartialEq, Eq)]\n*225|enum WakeGate {\n 226|    AlreadyLive,\n 227|    Racing,\n 228|    Claim,\n...\n 231|// [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\n*232|fn wake_gate_decision(live: bool, claimed: bool, past_deadline: bool) -> WakeGate {\n 233|    if live {\n 234|        WakeGate::AlreadyLive\n 235|    } else if claimed && !past_deadline {\n...\n 246|#[derive(Clone, Copy, PartialEq, Eq)]\n*247|enum StampMode {\n 248|    Inline,\n 249|    Deferred,\n 250|}\n...\n 262|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*263|fn record_stall_evict() {\n 264|    STALL_EVICT_COUNT.fetch_add(1, Ordering::Relaxed);\n 265|    STALL_EVICT_LAST_MS.store(crate::brain::now_ms(), Ordering::Relaxed);\n 266|}\n...\n 271|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*272|pub(crate) fn stall_evict_stats() -> (u64, u64) {\n 273|    (\n 274|        STALL_EVICT_COUNT.load(Ordering::Relaxed),\n 275|        STALL_EVICT_LAST_MS.load(Ordering::Relaxed),\n...\n 293|/// the deadline's PRESENCE is the invariant, not its exact length.\n*294|fn inject_commit_deadline() -> Duration {\n 295|    match std::env::var(\"SPT_INJECT_COMMIT_DEADLINE_MS\") {\n 296|        Ok(ms) => ms\n 297|            .parse::<u64>()\n...\n 321|/// invariant, not its exact length.\n*322|fn inject_miss_strike_budget() -> u32 {\n 323|    match std::env::var(\"SPT_INJECT_MISS_STRIKE_BUDGET\") {\n 324|        Ok(n) => n.parse::<u32>().ok().filter(|b| *b >= 1).unwrap_or(INJECT_MISS_STRIKE_BUDGET),\n 325|        Err(_) => INJECT_MISS_STRIKE_BUDGET,\n...\n 344|/// then-give-up path fast).\n*345|fn translate_respawn_budget() -> u32 {\n 346|    match std::env::var(\"SPT_TRANSLATE_RESPAWN_BUDGET\") {\n 347|        Ok(n) => n.parse::<u32>().ok().unwrap_or(TRANSLATE_RESPAWN_BUDGET),\n 348|        Err(_) => TRANSLATE_RESPAWN_BUDGET,\n...\n 378|/// `SPT_INJECT_SETTLE_MS` (its PRESENCE is the invariant, not the exact length).\n*379|fn inject_settle_deadline() -> Duration {\n 380|    match std::env::var(\"SPT_INJECT_SETTLE_MS\") {\n 381|        Ok(ms) => ms\n 382|            .parse::<u64>()\n...\n 394|/// The effective echo-verify wait; the int gate tunes it via `SPT_INJECT_ECHO_MS`.\n*395|fn inject_echo_verify_deadline() -> Duration {\n 396|    match std::env::var(\"SPT_INJECT_ECHO_MS\") {\n 397|        Ok(ms) => ms\n 398|            .parse::<u64>()\n...\n 421|\n*422|fn inject_text_chunk() -> usize {\n 423|    match std::env::var(\"SPT_INJECT_TEXT_CHUNK\") {\n 424|        Ok(n) => n.parse::<usize>().ok().filter(|c| *c > 0).unwrap_or(INJECT_TEXT_CHUNK),\n 425|        Err(_) => INJECT_TEXT_CHUNK,\n...\n 430|/// — the echo-verify head match over concatenated ring bytes.)\n*431|fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {\n 432|    if needle.is_empty() {\n 433|        return true;\n 434|    }\n...\n 445|/// empty payload (a keys-only sequence — no echoable text) is vacuously verified.\n*446|fn echo_verified(sent_text: &[u8], echoed: &[u8]) -> bool {\n 447|    if sent_text.is_empty() {\n 448|        return true;\n 449|    }\n...\n 462|#[derive(Debug, PartialEq, Eq)]\n*463|enum MissRespool {\n 464|    /// First miss of this envelope — respool it once and remember it.\n 465|    Respool,\n 466|    /// A repeat miss of an already-respooled envelope — dead-letter it, do NOT\n## conn.rs\n 11|//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n*12|//! logical stall-evict (`stall_evict_controller`) releases only the controller\n 13|//! ROLE — it never cancels the in-flight pipe write or closes the physical\n 14|//! connection, so the writer (and every write queued behind the same gate)\n 15|//! stays wedged until the peer process dies (~128 s in the field capture, on a\n...\n 90|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n*91|pub(crate) fn init_log_anchor() {\n 92|    let _ = MONO_ANCHOR.set(Instant::now());\n 93|}\n 94|\n...\n 100|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n*101|pub(crate) fn log_stamp() -> String {\n 102|    let wall_ms = SystemTime::now()\n 103|        .duration_since(UNIX_EPOCH)\n 104|        .map(|d| d.as_millis())\n...\n 116|#[derive(Debug, PartialEq, Eq)]\n*117|pub(crate) enum ConnWrite {\n 118|    /// The frame was fully written.\n 119|    Done,\n 120|    /// The pre-write gate closure declined (e.g. a superseded controller\n...\n 126|/// flight (checked out) or the conn has retired (then `poisoned` is set).\n*127|struct Gate {\n 128|    half: Option<SendHalf>,\n 129|}\n 130|\n 131|/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\n*132|struct Dog {\n 133|    /// `Some((op_id, deadline))` while a write is in flight.\n 134|    inflight: Option<(u64, Instant)>,\n 135|    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n...\n 140|/// the watchdog must not keep the conn alive).\n*141|struct Inner {\n 142|    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n 143|    id: u64,\n 144|    /// Accreted attribution facts (subscriber role, endpoint/session where\n...\n 185|/// protect is a plain slot/flag pair, valid on every panic boundary.\n*186|fn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\n 187|    match m.lock() {\n 188|        Ok(g) => g,\n 189|        Err(p) => p.into_inner(),\n...\n 193|#[cfg(windows)]\n*194|fn raw_of(half: &SendHalf) -> isize {\n 195|    use std::os::windows::io::{AsHandle, AsRawHandle};\n 196|    match half {\n 197|        SendHalf::NamedPipe(h) => h.as_handle().as_raw_handle() as isize,\n...\n 201|#[cfg(unix)]\n*202|fn raw_of(half: &SendHalf) -> i32 {\n 203|    use std::os::fd::{AsFd, AsRawFd};\n 204|    match half {\n 205|        SendHalf::UdSocket(h) => h.as_fd().as_raw_fd(),\n...\n 218|\n*219|impl Inner {\n 220|    /// The attribution snapshot every lifecycle record carries: stable conn id,\n 221|    /// accreted role/endpoint/session facts, wall + monotonic stamps.\n 222|    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n...\n 414|/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\n*415|pub(crate) struct BrokerConn {\n 416|    inner: Arc<Inner>,\n 417|    /// Joined on drop — after `shutdown` is raised — so the raw handle the\n 418|    /// watchdog aborts through strictly outlives every raw use.\n...\n 421|\n*422|impl BrokerConn {\n 423|    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\n 424|    /// deadline (gate-wait + OS write completion) — the broker passes its\n 425|    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n...\n 608|#[cfg(test)]\n*609|pub(crate) struct TestGatePin {\n 610|    inner: Arc<Inner>,\n 611|    half: Option<SendHalf>,\n 612|}\n...\n 614|#[cfg(test)]\n*615|impl Drop for TestGatePin {\n 616|    fn drop(&mut self) {\n 617|        let mut g = recover(&self.inner.gate);\n 618|        g.half = self.half.take();\n...\n 624|#[cfg(test)]\n*625|impl BrokerConn {\n 626|    /// Check the half out of the gate without arming the watchdog — writers\n 627|    /// queue behind the gate (bounded by their own deadlines) until the\n 628|    /// returned pin drops. Test-only; never a production path.\n...\n 638|\n*639|impl Drop for BrokerConn {\n 640|    fn drop(&mut self) {\n 641|        // Once-per-conn `transport-close` lifecycle record (leg d): the handle\n 642|        // is about to close — the terminal record of this conn id's life, with\n...\n 664|\n*665|fn poisoned_err() -> io::Error {\n 666|    io::Error::new(\n 667|        io::ErrorKind::BrokenPipe,\n 668|        \"broker conn poisoned (bounded write retired the physical connection)\",\n...\n 671|\n*672|fn timeout_err() -> io::Error {\n 673|    io::Error::new(\n 674|        io::ErrorKind::TimedOut,\n 675|        \"broker conn write exceeded its bound (conn poisoned + aborted)\",\n## brain.rs\n 43|    NetStreamSubscribeReq, NetStreamsReply, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n*44|    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,\n 45|    ViewerEvictedEvent,\n 46|    KIND_ADAPTER_APPLY, KIND_APPLIED, KIND_BRAIN_RESTART, KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_VIEWER_EVICTED,\n 47|    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,\n...\n 61|/// the epoch-ms stamping used elsewhere in the tree; no date dependency).\n*62|pub fn now_ms() -> u64 {\n 63|    SystemTime::now()\n 64|        .duration_since(UNIX_EPOCH)\n 65|        .map(|d| d.as_millis() as u64)\n...\n 82|#[derive(Debug, Clone)]\n*83|pub struct BrainState {\n 84|    /// The session the brain was driving.\n 85|    pub session_id: u64,\n 86|    /// The generation counter (incremented on each handoff).\n...\n 96|#[derive(Debug)]\n*97|pub enum BrokerEvent {\n 98|    /// A contiguous chunk of a session's output (the cursor has advanced past it).\n 99|    Output {\n 100|        session_id: u64,\n...\n 112|        session_id: u64,\n*113|        outcome: SubscribeOutcome,\n 114|    },\n 115|    /// The current PTY size of a session, pushed to a VIEWER (the letterbox feed,\n 116|    /// REQ-RCVIEW-1).\n...\n 181|#[derive(Debug, Clone)]\n*182|pub enum MetOutcome {\n 183|    /// A seed-holder was met — proceed to collect the code and submit it.\n 184|    Met(MetMember),\n 185|    /// The meet failed (no member, unknown subnet, …) — a classified\n...\n 207|/// old conn's drop eventually errors its read and the thread exits).\n*208|enum BrainConn {\n 209|    /// The ordinary blocking carrier — every non-pump brain.\n 210|    Whole(Stream),\n 211|    /// The pump carrier: writes go out [`SendHalf`]; framed reads arrive over\n...\n 223|\n*224|impl BrainConn {\n 225|    /// Split a connected+handshaked stream into the pump carrier: spawn the\n 226|    /// `pump-ipc-reader` thread (blocking [`read_frame`] → channel) and keep the\n 227|    /// [`SendHalf`] for writes. The reader logs spawn/exit so a leaked (parked)\n...\n 262|/// A live brain attached to a broker.\n*263|pub struct Brain {\n 264|    conn: BrainConn,\n 265|    generation: u64,\n 266|    gen_start_ms: u64,\n...\n 314|\n*315|impl Brain {\n 316|    /// Cold start: connect, handshake, fresh generation 0, `gen_start = now`.\n 317|    /// The ordinary blocking carrier ([`BrainConn::Whole`]) — every client\n 318|    /// except the peer pump, which uses [`Brain::cold_start_pump`].\n...\n 372|    /// ALSO subscribes from 0 over this same socket → two racing\n*373|    /// `controller_writer`s) is fixed in the BROKER, not by dropping this\n*374|    /// subscribe: the epoch gate ([`OutputLog::become_controller`]) guarantees a\n 375|    /// single live writer per connection, and the seeded `session_cursors` below\n 376|    /// routes the consumer through the dedup-below + snap-above path so any\n 377|    /// at-least-once boundary re-send is tolerated, never a fatal reject-gap\n...\n 1032|    /// `SharedSend` mutex ACROSS a blocking `write_frame` (broker.rs\n*1033|    /// `viewer_writer`/`controller_writer`), the dispatch reply path (`send_frame`) needs\n 1034|    /// that same lock, so any request/reply the non-draining consumer issues is starved\n 1035|    /// (dead-peer-independent — the v0.30.4 field re-wedge). The supervised daemon brain\n 1036|    /// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn\n...\n 1594|    /// `resolve_subscribe` (broker.rs) silently re-takes a FREE or SAME-LOCAL\n*1595|    /// (`None==None`) controller slot via `become_controller` (only a\n 1596|    /// DIFFERENT-REMOTE incumbent falls back to a silent viewer — the gate-#7\n 1597|    /// cross-operator guard). That silent re-take fires NO `Displaced` notice and\n 1598|    /// ORPHANS the prior local controller (the broker stops writing its output but\n 1599|    /// does not close the conn), FREEZING the session immediately; a >15s\n*1600|    /// `stall_evict_controller` is a separate, later, output-dependent symptom\n 1601|    /// (only an actively-producing stolen session blocks long enough to evict).\n 1602|    /// That is correct for `attach` (a serve loop that WILL drive the session), but\n 1603|    /// it is why a brain-respawn must NOT resume-re-attach as Control — it would\n...\n 1626|    /// take, `by` carries the operator identity (`Some(node)` remote, `None`\n*1627|    /// local). The broker replies the resolved [`SubscribeOutcome`] as a\n 1628|    /// [`BrokerEvent::Subscribed`].\n 1629|    fn subscribe_with(\n 1630|        &mut self,\n...\n 1738|// [impl->REQ-PUMP-DIAL-FASTFAIL]\n*1739|pub(crate) fn peer_reply_deadline(io_timeout: Option<Duration>, now: Instant) -> Option<Instant> {\n 1740|    io_timeout.map(|d| now + d.min(PEER_REPLY_READ_BUDGET))\n 1741|}\n 1742|\n...\n 1749|// [impl->REQ-PUMP-DIAL-FASTFAIL]\n*1750|pub(crate) fn reclassify_peer_reply_err(e: io::Error) -> io::Error {\n 1751|    if e.kind() == io::ErrorKind::TimedOut {\n 1752|        // NON-poison kind (Other) => peer_outcome's ordinary per-peer drop arm.\n 1753|        io::Error::other(format!(\n...\n 1761|/// Whether `haystack` contains the contiguous `needle`.\n*1762|fn contains(haystack: &[u8], needle: &[u8]) -> bool {\n 1763|    if needle.is_empty() {\n 1764|        return true;\n 1765|    }\n## attach.rs\n 61|// [impl->REQ-INST-3]\n*62|fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {\n 63|    let _ = crate::resting::daemon_rest_event(endpoint, event, None);\n 64|}\n 65|\n...\n 67|#[derive(Debug, Clone, PartialEq, Eq)]\n*68|pub enum AttachServeOutcome {\n 69|    /// The access gate refused the requesting origin, OR a `Control` attach hit\n 70|    /// an endpoint a DIFFERENT operator already controls (busy-refuse,\n 71|    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the\n...\n 85|\n*86|/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\n 87|/// arrives — gates wake (controller-only) and PTY input.\n 88|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n*89|enum ServeRole {\n 90|    /// The interactive controller (input + resize; wakes the endpoint).\n 91|    Controller,\n 92|    /// A read-only viewer (output only; wake-neutral).\n...\n 111|#[derive(Debug)]\n*112|pub struct ControllerIrrecoverablyBehind {\n 113|    /// The frozen `delivered_through` floor the ring has already rolled past.\n 114|    pub floor: u64,\n 115|}\n 116|\n*117|impl std::fmt::Display for ControllerIrrecoverablyBehind {\n 118|    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n 119|        write!(\n 120|            f,\n...\n 126|\n*127|impl std::error::Error for ControllerIrrecoverablyBehind {}\n 128|\n 129|/// Is `e` the brain's strict reject-gap fatal (`output gap: got seq N want M`)?\n 130|/// That is the only error the controller gap-resume recovers from; every other\n 131|/// error propagates unchanged.\n*132|fn is_output_gap(e: &io::Error) -> bool {\n 133|    e.kind() == io::ErrorKind::InvalidData && e.to_string().contains(\"output gap\")\n 134|}\n 135|\n...\n 157|// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n*158|pub fn read_controller_event_resuming(\n 159|    brain: &mut Brain,\n 160|    session_id: u64,\n 161|    by: Option<&str>,\n...\n 201|// [impl->REQ-RC-CROSS-NODE-ATTACH]\n*202|pub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n 203|    brain.sessions().ok().and_then(|reply| {\n 204|        reply\n 205|            .sessions\n...\n 234|// [impl->REQ-RC-CROSS-NODE-ATTACH]\n*235|pub fn serve_attach(\n 236|    brain: &mut Brain,\n 237|    broker_name: &str,\n 238|    stream_id: u64,\n...\n 248|    // session output (broker→serve) — including the controller's synchronous,\n*249|    // inline, multi-frame ring replay (`become_controller`), which holds the\n 250|    // broker's single-threaded per-conn handler for the whole replay. If the\n 251|    // forwards rode `brain` too, that stuck handler could not read them →\n 252|    // serve→broker backs up while broker→serve replay backs up → mutual IPC\n...\n 260|    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\n*261|    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\n 262|    // the controller onto that async path would remove the inline-replay block at\n 263|    // the source, but it touches the authoritative cursor-advancing path.)\n 264|    let mut wire = Brain::cold_start(broker_name, now_ms())?;\n...\n 429|            BrokerEvent::Subscribed { outcome, .. } => {\n*430|                use crate::msg::SubscribeOutcome as O;\n 431|                match outcome {\n 432|                    O::Controller | O::TookControl => {\n 433|                        role = Some(ServeRole::Controller);\n...\n 552|// [impl->REQ-INST-8]\n*553|pub fn request_attach(\n 554|    brain: &mut Brain,\n 555|    conn_id: u64,\n 556|    session_id: u64,\n...\n 571|// [impl->REQ-RC-CROSS-NODE-ATTACH]\n*572|pub fn request_attach_endpoint(\n 573|    brain: &mut Brain,\n 574|    conn_id: u64,\n 575|    session_id: u64,\n...\n 597|// [impl->REQ-RCVIEW-1]\n*598|pub fn send_attach_resize(\n 599|    brain: &mut Brain,\n 600|    stream_id: u64,\n 601|    rows: u16,\n...\n 613|// [impl->REQ-INST-8]\n*614|pub fn send_attach_input(\n 615|    brain: &mut Brain,\n 616|    stream_id: u64,\n 617|    bytes: &[u8],"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs, crates/spt-daemon/src/conn.rs, crates/spt-daemon/src/brain.rs, crates/spt-daemon/src/attach.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":76,"fileCount":4,"files":["crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/attach.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/conn.rs","count":19},{"path":"crates/spt-daemon/src/brain.rs","count":19},{"path":"crates/spt-daemon/src/attach.rs","count":18}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## broker.rs\n  59│    NetStreamsReply, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n *60│    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n  61│    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n  62│    KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,\n  63│    KIND_NET_SENT,\n    │...\n 108│// [impl->REQ-HAZARD-VIEWER-ISOLATION]\n*109│fn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {\n 110│    r.is_err()\n 111│}\n 112│\n    │...\n 117│/// dropped (the `tx` closes → the writer ends) and removed from the map.\n*118│struct ViewerSink {\n 119│    /// The bounded live-frame queue to this viewer's writer thread.\n 120│    tx: SyncSender<Envelope>,\n 121│    /// The viewer's IPC send half — kept ONLY for identity matching on detach\n    │...\n 180│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*181│fn brain_write_deadline() -> Duration {\n 182│    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n 183│        Ok(ms) => ms\n 184│            .parse::<u64>()\n    │...\n 205│/// strands the endpoint permanently claimed.\n*206│struct WakeClaimGuard<'a> {\n 207│    set: &'a Mutex<HashSet<String>>,\n 208│    endpoint: String,\n 209│}\n 210│\n*211│impl Drop for WakeClaimGuard<'_> {\n 212│    fn drop(&mut self) {\n 213│        recover(self.set).remove(&self.endpoint);\n 214│    }\n    │...\n 224│#[derive(Debug, PartialEq, Eq)]\n*225│enum WakeGate {\n 226│    AlreadyLive,\n 227│    Racing,\n 228│    Claim,\n    │...\n 231│// [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\n*232│fn wake_gate_decision(live: bool, claimed: bool, past_deadline: bool) -> WakeGate {\n 233│    if live {\n 234│        WakeGate::AlreadyLive\n 235│    } else if claimed && !past_deadline {\n    │...\n 246│#[derive(Clone, Copy, PartialEq, Eq)]\n*247│enum StampMode {\n 248│    Inline,\n 249│    Deferred,\n 250│}\n    │...\n 262│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*263│fn record_stall_evict() {\n 264│    STALL_EVICT_COUNT.fetch_add(1, Ordering::Relaxed);\n 265│    STALL_EVICT_LAST_MS.store(crate::brain::now_ms(), Ordering::Relaxed);\n 266│}\n    │...\n 271│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*272│pub(crate) fn stall_evict_stats() -> (u64, u64) {\n 273│    (\n 274│        STALL_EVICT_COUNT.load(Ordering::Relaxed),\n 275│        STALL_EVICT_LAST_MS.load(Ordering::Relaxed),\n    │...\n 293│/// the deadline's PRESENCE is the invariant, not its exact length.\n*294│fn inject_commit_deadline() -> Duration {\n 295│    match std::env::var(\"SPT_INJECT_COMMIT_DEADLINE_MS\") {\n 296│        Ok(ms) => ms\n 297│            .parse::<u64>()\n    │...\n 321│/// invariant, not its exact length.\n*322│fn inject_miss_strike_budget() -> u32 {\n 323│    match std::env::var(\"SPT_INJECT_MISS_STRIKE_BUDGET\") {\n 324│        Ok(n) => n.parse::<u32>().ok().filter(|b| *b >= 1).unwrap_or(INJECT_MISS_STRIKE_BUDGET),\n 325│        Err(_) => INJECT_MISS_STRIKE_BUDGET,\n    │...\n 344│/// then-give-up path fast).\n*345│fn translate_respawn_budget() -> u32 {\n 346│    match std::env::var(\"SPT_TRANSLATE_RESPAWN_BUDGET\") {\n 347│        Ok(n) => n.parse::<u32>().ok().unwrap_or(TRANSLATE_RESPAWN_BUDGET),\n 348│        Err(_) => TRANSLATE_RESPAWN_BUDGET,\n    │...\n 378│/// `SPT_INJECT_SETTLE_MS` (its PRESENCE is the invariant, not the exact length).\n*379│fn inject_settle_deadline() -> Duration {\n 380│    match std::env::var(\"SPT_INJECT_SETTLE_MS\") {\n 381│        Ok(ms) => ms\n 382│            .parse::<u64>()\n    │...\n 394│/// The effective echo-verify wait; the int gate tunes it via `SPT_INJECT_ECHO_MS`.\n*395│fn inject_echo_verify_deadline() -> Duration {\n 396│    match std::env::var(\"SPT_INJECT_ECHO_MS\") {\n 397│        Ok(ms) => ms\n 398│            .parse::<u64>()\n    │...\n 421│\n*422│fn inject_text_chunk() -> usize {\n 423│    match std::env::var(\"SPT_INJECT_TEXT_CHUNK\") {\n 424│        Ok(n) => n.parse::<usize>().ok().filter(|c| *c > 0).unwrap_or(INJECT_TEXT_CHUNK),\n 425│        Err(_) => INJECT_TEXT_CHUNK,\n    │...\n 430│/// — the echo-verify head match over concatenated ring bytes.)\n*431│fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {\n 432│    if needle.is_empty() {\n 433│        return true;\n 434│    }\n    │...\n 445│/// empty payload (a keys-only sequence — no echoable text) is vacuously verified.\n*446│fn echo_verified(sent_text: &[u8], echoed: &[u8]) -> bool {\n 447│    if sent_text.is_empty() {\n 448│        return true;\n 449│    }\n    │...\n 462│#[derive(Debug, PartialEq, Eq)]\n*463│enum MissRespool {\n 464│    /// First miss of this envelope — respool it once and remember it.\n 465│    Respool,\n 466│    /// A repeat miss of an already-respooled envelope — dead-letter it, do NOT\n## conn.rs\n  11│//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n *12│//! logical stall-evict (`stall_evict_controller`) releases only the controller\n  13│//! ROLE — it never cancels the in-flight pipe write or closes the physical\n  14│//! connection, so the writer (and every write queued behind the same gate)\n  15│//! stays wedged until the peer process dies (~128 s in the field capture, on a\n    │...\n  90│// [impl->REQ-CONN-POISON-ATTRIBUTION]\n *91│pub(crate) fn init_log_anchor() {\n  92│    let _ = MONO_ANCHOR.set(Instant::now());\n  93│}\n  94│\n    │...\n 100│// [impl->REQ-CONN-POISON-ATTRIBUTION]\n*101│pub(crate) fn log_stamp() -> String {\n 102│    let wall_ms = SystemTime::now()\n 103│        .duration_since(UNIX_EPOCH)\n 104│        .map(|d| d.as_millis())\n    │...\n 116│#[derive(Debug, PartialEq, Eq)]\n*117│pub(crate) enum ConnWrite {\n 118│    /// The frame was fully written.\n 119│    Done,\n 120│    /// The pre-write gate closure declined (e.g. a superseded controller\n    │...\n 126│/// flight (checked out) or the conn has retired (then `poisoned` is set).\n*127│struct Gate {\n 128│    half: Option<SendHalf>,\n 129│}\n 130│\n 131│/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\n*132│struct Dog {\n 133│    /// `Some((op_id, deadline))` while a write is in flight.\n 134│    inflight: Option<(u64, Instant)>,\n 135│    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n    │...\n 140│/// the watchdog must not keep the conn alive).\n*141│struct Inner {\n 142│    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n 143│    id: u64,\n 144│    /// Accreted attribution facts (subscriber role, endpoint/session where\n    │...\n 185│/// protect is a plain slot/flag pair, valid on every panic boundary.\n*186│fn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\n 187│    match m.lock() {\n 188│        Ok(g) => g,\n 189│        Err(p) => p.into_inner(),\n    │...\n 193│#[cfg(windows)]\n*194│fn raw_of(half: &SendHalf) -> isize {\n 195│    use std::os::windows::io::{AsHandle, AsRawHandle};\n 196│    match half {\n 197│        SendHalf::NamedPipe(h) => h.as_handle().as_raw_handle() as isize,\n    │...\n 201│#[cfg(unix)]\n*202│fn raw_of(half: &SendHalf) -> i32 {\n 203│    use std::os::fd::{AsFd, AsRawFd};\n 204│    match half {\n 205│        SendHalf::UdSocket(h) => h.as_fd().as_raw_fd(),\n    │...\n 218│\n*219│impl Inner {\n 220│    /// The attribution snapshot every lifecycle record carries: stable conn id,\n 221│    /// accreted role/endpoint/session facts, wall + monotonic stamps.\n 222│    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    │...\n 414│/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\n*415│pub(crate) struct BrokerConn {\n 416│    inner: Arc<Inner>,\n 417│    /// Joined on drop — after `shutdown` is raised — so the raw handle the\n 418│    /// watchdog aborts through strictly outlives every raw use.\n    │...\n 421│\n*422│impl BrokerConn {\n 423│    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\n 424│    /// deadline (gate-wait + OS write completion) — the broker passes its\n 425│    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n    │...\n 608│#[cfg(test)]\n*609│pub(crate) struct TestGatePin {\n 610│    inner: Arc<Inner>,\n 611│    half: Option<SendHalf>,\n 612│}\n    │...\n 614│#[cfg(test)]\n*615│impl Drop for TestGatePin {\n 616│    fn drop(&mut self) {\n 617│        let mut g = recover(&self.inner.gate);\n 618│        g.half = self.half.take();\n    │...\n 624│#[cfg(test)]\n*625│impl BrokerConn {\n 626│    /// Check the half out of the gate without arming the watchdog — writers\n 627│    /// queue behind the gate (bounded by their own deadlines) until the\n 628│    /// returned pin drops. Test-only; never a production path.\n    │...\n 638│\n*639│impl Drop for BrokerConn {\n 640│    fn drop(&mut self) {\n 641│        // Once-per-conn `transport-close` lifecycle record (leg d): the handle\n 642│        // is about to close — the terminal record of this conn id's life, with\n    │...\n 664│\n*665│fn poisoned_err() -> io::Error {\n 666│    io::Error::new(\n 667│        io::ErrorKind::BrokenPipe,\n 668│        \"broker conn poisoned (bounded write retired the physical connection)\",\n    │...\n 671│\n*672│fn timeout_err() -> io::Error {\n 673│    io::Error::new(\n 674│        io::ErrorKind::TimedOut,\n 675│        \"broker conn write exceeded its bound (conn poisoned + aborted)\",\n## brain.rs\n   43│    NetStreamSubscribeReq, NetStreamsReply, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n  *44│    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,\n   45│    ViewerEvictedEvent,\n   46│    KIND_ADAPTER_APPLY, KIND_APPLIED, KIND_BRAIN_RESTART, KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_VIEWER_EVICTED,\n   47│    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,\n     │...\n   61│/// the epoch-ms stamping used elsewhere in the tree; no date dependency).\n  *62│pub fn now_ms() -> u64 {\n   63│    SystemTime::now()\n   64│        .duration_since(UNIX_EPOCH)\n   65│        .map(|d| d.as_millis() as u64)\n     │...\n   82│#[derive(Debug, Clone)]\n  *83│pub struct BrainState {\n   84│    /// The session the brain was driving.\n   85│    pub session_id: u64,\n   86│    /// The generation counter (incremented on each handoff).\n     │...\n   96│#[derive(Debug)]\n  *97│pub enum BrokerEvent {\n   98│    /// A contiguous chunk of a session's output (the cursor has advanced past it).\n   99│    Output {\n  100│        session_id: u64,\n     │...\n  112│        session_id: u64,\n *113│        outcome: SubscribeOutcome,\n  114│    },\n  115│    /// The current PTY size of a session, pushed to a VIEWER (the letterbox feed,\n  116│    /// REQ-RCVIEW-1).\n     │...\n  181│#[derive(Debug, Clone)]\n *182│pub enum MetOutcome {\n  183│    /// A seed-holder was met — proceed to collect the code and submit it.\n  184│    Met(MetMember),\n  185│    /// The meet failed (no member, unknown subnet, …) — a classified\n     │...\n  207│/// old conn's drop eventually errors its read and the thread exits).\n *208│enum BrainConn {\n  209│    /// The ordinary blocking carrier — every non-pump brain.\n  210│    Whole(Stream),\n  211│    /// The pump carrier: writes go out [`SendHalf`]; framed reads arrive over\n     │...\n  223│\n *224│impl BrainConn {\n  225│    /// Split a connected+handshaked stream into the pump carrier: spawn the\n  226│    /// `pump-ipc-reader` thread (blocking [`read_frame`] → channel) and keep the\n  227│    /// [`SendHalf`] for writes. The reader logs spawn/exit so a leaked (parked)\n     │...\n  262│/// A live brain attached to a broker.\n *263│pub struct Brain {\n  264│    conn: BrainConn,\n  265│    generation: u64,\n  266│    gen_start_ms: u64,\n     │...\n  314│\n *315│impl Brain {\n  316│    /// Cold start: connect, handshake, fresh generation 0, `gen_start = now`.\n  317│    /// The ordinary blocking carrier ([`BrainConn::Whole`]) — every client\n  318│    /// except the peer pump, which uses [`Brain::cold_start_pump`].\n     │...\n  372│    /// ALSO subscribes from 0 over this same socket → two racing\n *373│    /// `controller_writer`s) is fixed in the BROKER, not by dropping this\n *374│    /// subscribe: the epoch gate ([`OutputLog::become_controller`]) guarantees a\n  375│    /// single live writer per connection, and the seeded `session_cursors` below\n  376│    /// routes the consumer through the dedup-below + snap-above path so any\n  377│    /// at-least-once boundary re-send is tolerated, never a fatal reject-gap\n     │...\n 1032│    /// `SharedSend` mutex ACROSS a blocking `write_frame` (broker.rs\n*1033│    /// `viewer_writer`/`controller_writer`), the dispatch reply path (`send_frame`) needs\n 1034│    /// that same lock, so any request/reply the non-draining consumer issues is starved\n 1035│    /// (dead-peer-independent — the v0.30.4 field re-wedge). The supervised daemon brain\n 1036│    /// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn\n     │...\n 1594│    /// `resolve_subscribe` (broker.rs) silently re-takes a FREE or SAME-LOCAL\n*1595│    /// (`None==None`) controller slot via `become_controller` (only a\n 1596│    /// DIFFERENT-REMOTE incumbent falls back to a silent viewer — the gate-#7\n 1597│    /// cross-operator guard). That silent re-take fires NO `Displaced` notice and\n 1598│    /// ORPHANS the prior local controller (the broker stops writing its output but\n 1599│    /// does not close the conn), FREEZING the session immediately; a >15s\n*1600│    /// `stall_evict_controller` is a separate, later, output-dependent symptom\n 1601│    /// (only an actively-producing stolen session blocks long enough to evict).\n 1602│    /// That is correct for `attach` (a serve loop that WILL drive the session), but\n 1603│    /// it is why a brain-respawn must NOT resume-re-attach as Control — it would\n     │...\n 1626│    /// take, `by` carries the operator identity (`Some(node)` remote, `None`\n*1627│    /// local). The broker replies the resolved [`SubscribeOutcome`] as a\n 1628│    /// [`BrokerEvent::Subscribed`].\n 1629│    fn subscribe_with(\n 1630│        &mut self,\n     │...\n 1738│// [impl->REQ-PUMP-DIAL-FASTFAIL]\n*1739│pub(crate) fn peer_reply_deadline(io_timeout: Option<Duration>, now: Instant) -> Option<Instant> {\n 1740│    io_timeout.map(|d| now + d.min(PEER_REPLY_READ_BUDGET))\n 1741│}\n 1742│\n     │...\n 1749│// [impl->REQ-PUMP-DIAL-FASTFAIL]\n*1750│pub(crate) fn reclassify_peer_reply_err(e: io::Error) -> io::Error {\n 1751│    if e.kind() == io::ErrorKind::TimedOut {\n 1752│        // NON-poison kind (Other) => peer_outcome's ordinary per-peer drop arm.\n 1753│        io::Error::other(format!(\n     │...\n 1761│/// Whether `haystack` contains the contiguous `needle`.\n*1762│fn contains(haystack: &[u8], needle: &[u8]) -> bool {\n 1763│    if needle.is_empty() {\n 1764│        return true;\n 1765│    }\n## attach.rs\n  61│// [impl->REQ-INST-3]\n *62│fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {\n  63│    let _ = crate::resting::daemon_rest_event(endpoint, event, None);\n  64│}\n  65│\n    │...\n  67│#[derive(Debug, Clone, PartialEq, Eq)]\n *68│pub enum AttachServeOutcome {\n  69│    /// The access gate refused the requesting origin, OR a `Control` attach hit\n  70│    /// an endpoint a DIFFERENT operator already controls (busy-refuse,\n  71│    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the\n    │...\n  85│\n *86│/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\n  87│/// arrives — gates wake (controller-only) and PTY input.\n  88│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n *89│enum ServeRole {\n  90│    /// The interactive controller (input + resize; wakes the endpoint).\n  91│    Controller,\n  92│    /// A read-only viewer (output only; wake-neutral).\n    │...\n 111│#[derive(Debug)]\n*112│pub struct ControllerIrrecoverablyBehind {\n 113│    /// The frozen `delivered_through` floor the ring has already rolled past.\n 114│    pub floor: u64,\n 115│}\n 116│\n*117│impl std::fmt::Display for ControllerIrrecoverablyBehind {\n 118│    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n 119│        write!(\n 120│            f,\n    │...\n 126│\n*127│impl std::error::Error for ControllerIrrecoverablyBehind {}\n 128│\n 129│/// Is `e` the brain's strict reject-gap fatal (`output gap: got seq N want M`)?\n 130│/// That is the only error the controller gap-resume recovers from; every other\n 131│/// error propagates unchanged.\n*132│fn is_output_gap(e: &io::Error) -> bool {\n 133│    e.kind() == io::ErrorKind::InvalidData && e.to_string().contains(\"output gap\")\n 134│}\n 135│\n    │...\n 157│// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n*158│pub fn read_controller_event_resuming(\n 159│    brain: &mut Brain,\n 160│    session_id: u64,\n 161│    by: Option<&str>,\n    │...\n 201│// [impl->REQ-RC-CROSS-NODE-ATTACH]\n*202│pub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n 203│    brain.sessions().ok().and_then(|reply| {\n 204│        reply\n 205│            .sessions\n    │...\n 234│// [impl->REQ-RC-CROSS-NODE-ATTACH]\n*235│pub fn serve_attach(\n 236│    brain: &mut Brain,\n 237│    broker_name: &str,\n 238│    stream_id: u64,\n    │...\n 248│    // session output (broker→serve) — including the controller's synchronous,\n*249│    // inline, multi-frame ring replay (`become_controller`), which holds the\n 250│    // broker's single-threaded per-conn handler for the whole replay. If the\n 251│    // forwards rode `brain` too, that stuck handler could not read them →\n 252│    // serve→broker backs up while broker→serve replay backs up → mutual IPC\n    │...\n 260│    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\n*261│    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\n 262│    // the controller onto that async path would remove the inline-replay block at\n 263│    // the source, but it touches the authoritative cursor-advancing path.)\n 264│    let mut wire = Brain::cold_start(broker_name, now_ms())?;\n    │...\n 429│            BrokerEvent::Subscribed { outcome, .. } => {\n*430│                use crate::msg::SubscribeOutcome as O;\n 431│                match outcome {\n 432│                    O::Controller | O::TookControl => {\n 433│                        role = Some(ServeRole::Controller);\n    │...\n 552│// [impl->REQ-INST-8]\n*553│pub fn request_attach(\n 554│    brain: &mut Brain,\n 555│    conn_id: u64,\n 556│    session_id: u64,\n    │...\n 571│// [impl->REQ-RC-CROSS-NODE-ATTACH]\n*572│pub fn request_attach_endpoint(\n 573│    brain: &mut Brain,\n 574│    conn_id: u64,\n 575│    session_id: u64,\n    │...\n 597│// [impl->REQ-RCVIEW-1]\n*598│pub fn send_attach_resize(\n 599│    brain: &mut Brain,\n 600│    stream_id: u64,\n 601│    rows: u16,\n    │...\n 613│// [impl->REQ-INST-8]\n*614│pub fn send_attach_input(\n 615│    brain: &mut Brain,\n 616│    stream_id: u64,\n 617│    bytes: &[u8],"},"isError":false,"timestamp":1784195753043}}
{"type":"message","id":"39113c9c","parentId":"ada4e590","timestamp":"2026-07-16T09:56:03.316Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning targeted document grep and review**"},{"type":"toolCall","id":"call_zC8S4JgzgZpbYahtQlHQHNHK|fc_008390f508d1d09b016a58aab33708819aae17a286a378b56b","name":"grep","arguments":{"i":"Finding PTY hazard sections","pattern":"^### .*?(attach|controller|viewer|brain|broker|wedge|half-open|reconnect|re-attach|stream|detach)","path":"docs/KNOWN-HAZARDS.md","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_bmxBswjvehJEOqPl5JOQoLhr|fc_008390f508d1d09b016a58aab33720819a827e5496805b730d","name":"grep","arguments":{"i":"Finding context terminal rules","pattern":"^(##|###).*?(terminal|remote control|self-update|broker)|\\b(controller|viewer|attach|detach|reattach|PTY ownership|broker owns|connection epoch)\\b","path":"CONTEXT.md","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_VigRJTsNViTUwiOeXTgggM10|fc_008390f508d1d09b016a58aab3372c819a9fe9f54b9849b191","name":"read","arguments":{"i":"Reading rc attach loop","path":"crates/spt/src/rc.rs","selector":"1080-1760"}},{"type":"toolCall","id":"call_NOOuSCB4W4WcndEri4hnbjnP|fc_008390f508d1d09b016a58aab33758819abd20e86bec490f94","name":"read","arguments":{"i":"Reading rc pump termination","path":"crates/spt/src/rc.rs","selector":"1760-1945"}},{"type":"toolCall","id":"call_OecapSYePAMofRmv04AIZQ3L|fc_008390f508d1d09b016a58aab33764819a8e22f3e26fa782bc","name":"read","arguments":{"i":"Reading attach serve loop","path":"crates/spt-daemon/src/attach.rs","selector":"136-550"}},{"type":"toolCall","id":"call_J5Y7eRRn63eCDqeoYB8X9ZMB|fc_008390f508d1d09b016a58aab3376c819a8c58c2616853356c","name":"read","arguments":{"i":"Reading broker sink ownership","path":"crates/spt-daemon/src/broker.rs","selector":"80-205,960-1420"}},{"type":"toolCall","id":"call_zENxjFbVW50ls5fRuAClKQ4U|fc_008390f508d1d09b016a58aab33778819a97f5bec7b17f678d","name":"read","arguments":{"i":"Reading brain subscribe lifecycle","path":"crates/spt-daemon/src/brain.rs","selector":"1000-1085,1560-1685"}},{"type":"toolCall","id":"call_WlKkSYPQ1XlLSvbx40qyZlUz|fc_008390f508d1d09b016a58aab33780819a8df10b8af7c8bebc","name":"read","arguments":{"i":"Reading connection cancellation","path":"crates/spt-daemon/src/conn.rs","selector":"1-180,219-680"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":43542,"output":453,"cacheRead":29184,"cacheWrite":0,"totalTokens":73179,"reasoningTokens":42,"cost":{"input":0.21771000000000001,"output":0.01359,"cacheRead":0.014591999999999999,"cacheWrite":0,"total":0.245892}},"stopReason":"toolUse","timestamp":1784195753088,"responseId":"resp_008390f508d1d09b016a58aaa97948819aa6d914171a1eee35","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_008390f508d1d09b016a58aaaaff28819a9bca36b93d3f7baf","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKqzddyzo08Zu-v4u9GF9p6LQt3psB_kCFkIFNAUBQ5F01DjFTHBwbfokDNTbDYjVhaVnoUi_RP01gVRXpMslxCQqMik3xZs8ovB_amh20BlcwLNtDqUgQy4RjmR4UKrs33UedSmWWoExpYJOUTJXnrTRgR41fpTzgVbzhNjaEsIDjY1meRlzkNnFgnYU0f3CqFRMX-RDnOX_r7YLBBLnjzJzrgG-lUaGLl-n5YAQ2uu52fvb8ITJf8RMUgzBU7XuAtLpOhjBbWMPizXEojfHPdlbjCA2UQODIc7TSqonEv8AVu9RbdzS-z_FfkcQvETmxT7Icz8XXM8Wr3YriXeYEtCrbg5C1kRLJqQBhLDyGHN6lTEbmj5OjeSV6g6lNXXBcGHEeRactd5wGabu-h-gBodoVj8G_DwocldCnfxUzk29J4bPGNnjliADCGj62aW83KnYgoxd_pmOfmibSoPaQGEwXf_HfnewY1h3PGqivfTBJuW7fThWLj8EL7VswAerfC9HTYnsaSJ6fvUcU6nBiKPwCOH7lkj1BHUbSwShsjx2yZOnEC2FRsPFCo7brS3lyUEsWqNuH9CS5Tejn5P49iQAyRjl0G4henq5AAuoRm_ao7UyeHBcD-cIKpqMNaYA085ryfb9SMSnXif8gfexaD8ivcwoye2oFG59bGQNg2Gaphzsu1RwPlhpfWzMa-RI2qzVbinp8_LNvjQHkFarGvcHz4oH_NR8pipFSXYJeKcafcTP2o_db4lH1IkeTbKRJ-rU5pkk2kwK4kiSsBck9bAV2v8atduxgqPnXkq5r_EMyvAqShr8dzKsLHAoDiBhWesyyxtQbquMkL1De-LlAFBsjRdKMBHUe3W2BqtU0IgoyTplGWrZbbLUIk0wqxBA8EyY0vPQcjWJOuGF4eJqaB1iCzc7eeqzs7QwY9MG9k2nMs8WKIM6bTCWcORz3a4Hr2RKFzLcLxovaFA4-j0xvZsUz3a3oEgL9BKXa_IAaq9LG49Z5uiP8qVZCvE1gzssCL-be-8nEfNvfqs6vFJ14lIF94ewplnUlc8NoGpU-rx1lIxPaQwK6v8FIKkdtWoqvOYZ5vpxvxO3_LxPFTIWM7ugQ==","summary":[{"type":"summary_text","text":"**Planning targeted document grep and review**"}]},{"id":"fc_008390f508d1d09b016a58aab33708819aae17a286a378b56b","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding PTY hazard sections\",\"pattern\":\"^### .*?(attach|controller|viewer|brain|broker|wedge|half-open|reconnect|re-attach|stream|detach)\",\"path\":\"docs/KNOWN-HAZARDS.md\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_zC8S4JgzgZpbYahtQlHQHNHK","name":"grep"},{"id":"fc_008390f508d1d09b016a58aab33720819a827e5496805b730d","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding context terminal rules\",\"pattern\":\"^(##|###).*?(terminal|remote control|self-update|broker)|\\\\b(controller|viewer|attach|detach|reattach|PTY ownership|broker owns|connection epoch)\\\\b\",\"path\":\"CONTEXT.md\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_bmxBswjvehJEOqPl5JOQoLhr","name":"grep"},{"id":"fc_008390f508d1d09b016a58aab3372c819a9fe9f54b9849b191","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading rc attach loop\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1080-1760\"}","call_id":"call_VigRJTsNViTUwiOeXTgggM10","name":"read"},{"id":"fc_008390f508d1d09b016a58aab33758819abd20e86bec490f94","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading rc pump termination\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1760-1945\"}","call_id":"call_NOOuSCB4W4WcndEri4hnbjnP","name":"read"},{"id":"fc_008390f508d1d09b016a58aab33764819a8e22f3e26fa782bc","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach serve loop\",\"path\":\"crates/spt-daemon/src/attach.rs\",\"selector\":\"136-550\"}","call_id":"call_OecapSYePAMofRmv04AIZQ3L","name":"read"},{"id":"fc_008390f508d1d09b016a58aab3376c819a8c58c2616853356c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker sink ownership\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"80-205,960-1420\"}","call_id":"call_J5Y7eRRn63eCDqeoYB8X9ZMB","name":"read"},{"id":"fc_008390f508d1d09b016a58aab33778819a97f5bec7b17f678d","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain subscribe lifecycle\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":\"1000-1085,1560-1685\"}","call_id":"call_zENxjFbVW50ls5fRuAClKQ4U","name":"read"},{"id":"fc_008390f508d1d09b016a58aab33780819a8df10b8af7c8bebc","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading connection cancellation\",\"path\":\"crates/spt-daemon/src/conn.rs\",\"selector\":\"1-180,219-680\"}","call_id":"call_WlKkSYPQ1XlLSvbx40qyZlUz","name":"read"}]},"duration":10205.031799999997,"ttft":1886.2286000000313,"contextSnapshot":{"promptTokens":72726,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_zC8S4JgzgZpbYahtQlHQHNHK|fc_008390f508d1d09b016a58aab33708819aae17a286a378b56b","toolName":"grep","startedAt":"2026-07-16T09:56:03.316Z","args":{"path":"docs/KNOWN-HAZARDS.md"},"intent":"Finding PTY hazard sections"},"id":"fa0a6ba0","parentId":"39113c9c","timestamp":"2026-07-16T09:56:03.317Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_bmxBswjvehJEOqPl5JOQoLhr|fc_008390f508d1d09b016a58aab33720819a827e5496805b730d","toolName":"grep","startedAt":"2026-07-16T09:56:03.317Z","args":{"path":"CONTEXT.md"},"intent":"Finding context terminal rules"},"id":"b0f0d8d4","parentId":"fa0a6ba0","timestamp":"2026-07-16T09:56:03.317Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_VigRJTsNViTUwiOeXTgggM10|fc_008390f508d1d09b016a58aab3372c819a9fe9f54b9849b191","toolName":"read","startedAt":"2026-07-16T09:56:03.317Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading rc attach loop"},"id":"80d1b204","parentId":"b0f0d8d4","timestamp":"2026-07-16T09:56:03.317Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_NOOuSCB4W4WcndEri4hnbjnP|fc_008390f508d1d09b016a58aab33758819abd20e86bec490f94","toolName":"read","startedAt":"2026-07-16T09:56:03.317Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading rc pump termination"},"id":"314f3507","parentId":"80d1b204","timestamp":"2026-07-16T09:56:03.317Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_OecapSYePAMofRmv04AIZQ3L|fc_008390f508d1d09b016a58aab33764819a8e22f3e26fa782bc","toolName":"read","startedAt":"2026-07-16T09:56:03.317Z","args":{"path":"crates/spt-daemon/src/attach.rs"},"intent":"Reading attach serve loop"},"id":"c66612a5","parentId":"314f3507","timestamp":"2026-07-16T09:56:03.317Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_J5Y7eRRn63eCDqeoYB8X9ZMB|fc_008390f508d1d09b016a58aab3376c819a8c58c2616853356c","toolName":"read","startedAt":"2026-07-16T09:56:03.317Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker sink ownership"},"id":"12975506","parentId":"c66612a5","timestamp":"2026-07-16T09:56:03.317Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_zENxjFbVW50ls5fRuAClKQ4U|fc_008390f508d1d09b016a58aab33778819a97f5bec7b17f678d","toolName":"read","startedAt":"2026-07-16T09:56:03.317Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Reading brain subscribe lifecycle"},"id":"c9c4e8c4","parentId":"12975506","timestamp":"2026-07-16T09:56:03.317Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_WlKkSYPQ1XlLSvbx40qyZlUz|fc_008390f508d1d09b016a58aab33780819a8df10b8af7c8bebc","toolName":"read","startedAt":"2026-07-16T09:56:03.317Z","args":{"path":"crates/spt-daemon/src/conn.rs"},"intent":"Reading connection cancellation"},"id":"d13243d0","parentId":"c9c4e8c4","timestamp":"2026-07-16T09:56:03.317Z"}
{"type":"message","id":"7475a9be","parentId":"d13243d0","timestamp":"2026-07-16T09:56:03.335Z","message":{"role":"toolResult","toolCallId":"call_zC8S4JgzgZpbYahtQlHQHNHK|fc_008390f508d1d09b016a58aab33708819aae17a286a378b56b","toolName":"grep","content":[{"type":"text","text":" 22|\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...\n 28|\n*29|### 1.4 Deferred spool rows must not leak to the event stream\n 30|- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.\n 31|- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.\n 32|- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.\n...\n 206|<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->\n*207|### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`\n 208|- **Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::null...\n 209|- **Invariant:** every detach-spawn of a long-lived child runs with **`bInheritHandles = FALSE`** (`spt-daemon::daemon::detached_no_inherit`) — zero handles flow, whatever the pipe's depth in the ancestry. Stripping `HANDLE_FLAG_INHERIT` from the spawner's *std* handles is NOT sufficient: a grandparent capture's pipe sits in the handle table as a stray inheritable handle and still flows through every `bInheritHandles = TRUE` hop (the first guard shipped that way and was wedged by exactly this — a dae...\n 210|- **spt-core mapping:** `spt-daemon::daemon::spawn_detached` (the daemon) and `spt-daemon::shellhost::launch_shell` (the relay-receipt shell binary). Any future long-lived detached spawn (manifest-template children included) must use the same no-inherit spawn.\n...\n 324|\n*325|### 6.7 Broker and brain MUST be separate processes (in-process collapse silently breaks no-endpoint-drop update) `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n 326|- **Failure:** the daemon hosts the broker as a background *thread* in the single `spt daemon` process (`daemon.rs:165-170`, `Arc<Broker>` + `thread::spawn(serve)`) instead of a separate process. A brain restart onto a swapped binary then cannot happen without killing the broker thread — closing every PTY, orphaning every harness child, dropping every socket. So `spt update apply` degrades to an in-process `Brain::handoff` no-op: the binary swaps on disk but the running daemon keeps executing the old c...\n 327|- **Invariant:** the broker runs as its own long-lived process that survives every brain restart; the brain restarts onto the new binary and re-attaches via the versioned IPC. A routine (brain-only) update must leave every hosted endpoint untouched at the *process* level — not merely re-subscribe a brain within the same process. The evidence for REQ-UPD-3 / REQ-DAEMON-2 must prove process-level survival (a PTY child + a live QUIC conn survive a brain-process restart onto a swapped binary — SPIKE-01/0...\n 328|- **spt-core mapping:** restoration is ADR-0018 (next milestone). The current `int` tags on REQ-DAEMON-2 / REQ-UPD-3 are regression-masked and re-point at restoration; the broker becomes the always-up per-machine anchor (seed-lock + liveness + brain supervisor). Two-process supervision, generation custody, durable-deadline loop timing, broker-cursor-of-record, and readiness-gated auto-rollback all hang off this.\n...\n 343|\n*344|### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output\n 345|- **Failure:** a resume-mode brain (per-session `session_cursors` populated by `resume_sessions`) drives `Brain` over **blocking** `read_event` calls. Any command that loops `read_event` until its own reply — `spawn_session_pid` waiting for `Spawned`, `net_status`, `sessions`, etc. — calls `read_event` on *every* interleaved frame, so an OUTPUT frame for a **different** session is **cursor-processed** (its `session_cursors` entry snaps forward; the broker already counted it delivered via `delivered_t...\n 346|- **Invariant:** the daemon-hosted multi-session event loop must not consume a session's OUTPUT inside another session's blocking wait. The **live-agent adapter milestone must restructure the brain event pump** so command/`spawn` is non-blocking (a single demux loop owns `read_event` and routes every frame to its session's consumer), OR a blocking wait must re-queue/route the frames it reads for other sessions rather than dropping them.\n 347|- **spt-core mapping:** **unreachable today** — the supervised daemon brain hosts no PTY sessions and spawns none; a single-session seat (legacy, empty map) has no \"other session\" to starve. Surfaces the moment daemon-hosted sessions land (the live-agent adapter), which must rebuild the blocking `read_event` loop regardless (N interactive sessions cannot share one blocking reader). Recorded so that redesign inherits the constraint rather than rediscovering it. No machinery now (would be untested dead c...\n...\n 349|\n*350|### 6.10 Phase-significant loop timing must be a durable absolute-deadline grid, not phase-relative sleep `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n 351|- **Failure:** a periodic loop that sleeps a flat `period` each iteration (`pulse_tick` then `sleep(pulse_period)`) is **phase-relative** — every brain restart silently re-phases the grid to the restart instant. Under the seamless-update model (the supervisor respawns the brain onto a swapped binary, ADR-0018 D3-3), a routine update would shift the cadence of every phase-significant loop, and continuity cannot ride a brain→brain frame (the outgoing brain is gone before the new one starts — the same...\n 352|- **Invariant:** phase-significant periodic timing lives as durable absolute-deadline state on disk (`(anchor, interval)`), rehydrated on every brain start, with fires **derived functionally** (`next_fire = anchor + interval·⌈max(0,now−anchor)/interval⌉`) and **no per-fire write**. An **Update** restart re-reads the anchor and keeps deriving (phase preserved, lands mid-grid); a **Crash**/**Cold** restart re-bases the anchor to `now` (phase reset acceptable — the loop is idempotent catch-up). The...\n 353|- **spt-core mapping:** ADR-0018 Q4/V3/V4, restoration D5. Mechanism in `spt-daemon::deadline` (`DeadlineAnchor` periodic + `OneShotDeadline` rule-only pure helper); the pulse loop (`lifecycle::run_pulse_loop`) consumes it. The one-shot **machinery** (a durable in-daemon alarm scheduler) is the deferred alarm port (`docs/DEFERRED.md`) — the daemon has no one-shot consumer today, so building the timer now would ship untested dead code (activate-don't-pre-fail); D5 fixes the *rule* as a tested-unwired he...\n...\n 355|\n*356|### 6.11 Brain respawn must exec the APPLIED bytes, not the renamed old binary (Linux `current_exe` follows the apply-rename; readiness ≠ new-bytes) `[REQ-HAZARD-BRAIN-RESPAWN-PATH]`\n 357|- **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 358|- **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 359|- **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...\n 374|\n*375|### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`\n 376|- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.\n 377|- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.\n 378|- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.\n...\n 400|\n*401|### 7.6 Pump brain-IPC reads must be deadline-bounded (a blocked read wedges the whole pump)  `[REQ-HAZARD-PUMP-IPC-DEADLINE]`\n 402|<!-- [doc->REQ-HAZARD-PUMP-IPC-DEADLINE] -->\n 403|- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop...\n 404|- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a heal...\n...\n 407|\n*408|### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`\n 409|<!-- [doc->REQ-HAZARD-VIEWER-ISOLATION] -->\n 410|- **Failure:** the W2.5 controller/viewer model lets ANY number of read-only `--view` attachers ride one session's broker `OutputLog`. The single drain thread fans each output chunk to every attacher. If a viewer's socket is fanned out with a **blocking** write under the log lock (the controller's authoritative path), one wedged viewer (a slow terminal, a black-holed WAN peer, a hostile non-reader) stalls the drain — freezing the controller's stream and backing up the PTY child. A single watcher must n...\n 411|- **Invariant:** the drain writes the **controller** on the authoritative blocking bounded path (it alone advances `delivered_through`), but each **viewer** gets an **isolated bounded SPSC queue + a dedicated writer thread**; the drain `try_send`s under the log lock and **evicts** any viewer whose queue is `Full` (fell behind the live stream) or `Disconnected` (its writer died on a dead socket) — the drain thread **never touches a viewer socket**, so no viewer write can backpressure it. A **soft cap** ...\n...\n 414|\n*415|### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`\n 416|<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\n 417|- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only v...\n 418|- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`...\n...\n 421|\n*422|### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`\n 423|<!-- [doc->REQ-HAZARD-BROKER-SEED-WIRE-SKEW] -->\n 424|- **Failure:** the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004's no-terminate-during-update pillar forbids auto-killing it — 6.7). A self-update that changes a daemon-state WIRE FORMAT — e.g. the v0.9.0 adapter-agnostic `Seed` (the `adapter` field dropped) — therefore lands a NEW-version CLI talking to the STILL-RESIDENT OLD broker. The old broker cannot deserialize the new `Seed` (its formerly-required `adapter` is absent), so it drops the seed-...\n 425|- **Invariant:** (a) spt-core surfaces an ACTIONABLE diagnostic on the seed-ack EOF — naming the stale-broker cause + the fix (`spt daemon stop`; the broker restarts on the next `spt api` call) — never the bare io error (scoped to `UnexpectedEof` on the seed-ack path, so it never mis-fires on an unrelated error like a refused connect). (b) A daemon-state wire change requires a DELIBERATE full broker restart; this is NOT automatic — ADR-0004 forbids auto-killing the resident broker, and a brain-only...\n...\n 428|\n*429|### 7.10 A VIEW is independent from the endpoint — closing the launching tab must NOT reap the daemon-hosted harness  `[REQ-HAZARD-VIEWER-CLOSE-DETACH]`\n 430|<!-- [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH] -->\n 431|- **Failure (Windows):** `spt endpoint run` autostarts the daemon (`ensure_running` → `detached_no_inherit`), which INHERITS the launching terminal's Windows Job Object. Windows Terminal / VS Code place the shell AND every descendant in a Job with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; closing the tab drops the job's last handle → the OS reaps the daemon + its broker-spawned ConPTY harness subtree. The `rc` pump detaching should end ONLY the viewport; the harness must keep running + stay re-attachable...\n 432|- **Invariant:** both daemon spawn paths request `CREATE_BREAKAWAY_FROM_JOB` (0x0100_0000), best-effort with an in-job FALLBACK on `ERROR_ACCESS_DENIED`(5) / `ERROR_INVALID_PARAMETER`(87) so a breakaway-denying job NEVER regresses the spawn. **A real job CAN deny breakaway** (proven: the cargo/CI runner's own ancestor job ACCESS_DENIES it), so efficacy against the real terminal is UNKNOWN until measured. **int stage = OPERATOR MANUAL ACCEPTANCE, not CI** (doyle ruling 2026-06-18): the runner sits inside ...\n...\n 435|\n*436|### 7.11 A dead PTY child + a dropped operator pump must NOT wedge the broker for other clients  `[REQ-HAZARD-ATTACH-WEDGE]`\n 437|- **Failure (hypothesized, v0.12.0):** a legitimately dead PTY child (crash/kill) plus an `rc` pump dropped without a clean detach (closed tab) was thought to make the broker's loopback forward `write_all` block forever on a full 64 KB duplex, park a worker in the 2-worker net runtime, saturate both, and stall every new attach / `endpoint run` (with `daemon stop` unable to join).\n 438|- **Invariant — DISPOSITION = PROVE-DON'T-CHANGE** (doyle GATE-PASS @e883f45, 2026-06-18): the post-L0 code ALREADY prevents the wedge; NO fail-fast / worker-count code was added. (1) `serve_attach` forwards fire-and-forget (`net_stream_send` `op_id=None`) and the broker-side `send_stream` is already deadline-bounded (`bounded_block_on`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s — hazard 7.8), not forever. (2) the loopback duplex is drained broker-INTERNALLY by the operator row's OWN read pump (`nethost.rs` ...\n 439|- **spt-core mapping:** int `crates/spt/tests/attach_wedge_e2e.rs` (REAL detached daemon + dummy-harness fixture): serve the victim (rc sees its tick), abruptly kill rc (dropped pump) + kill the PTY child → a NEW endpoint still comes online + is served (no wedge), the dead endpoint is offlined within a tick (`LIVENESS_RECONCILE_OFFLINE`), `daemon stop` bounded. Leans on 7.8 (the QUIC-op deadline) + 7.7 (viewer isolation).\n...\n 441|\n*442|### 7.12 Controller output must NOT be written inline on the drain thread — a backed-up controller wedges the session  `[REQ-HAZARD-INJECT-CONTROL-COEXIST]`\n 443|- **Failure (v0.12.x, operator dogfooding + doyle /diagnose 2026-06-19):** `OutputLog::append` fanned each live chunk to the CONTROLLER via a SYNCHRONOUS, blocking `write_frame` held INLINE on the session's single drain thread while `Mutex<OutputLog>` was locked (viewers already had a dedicated writer thread + bounded evicting channel). A backed-up controller socket — a slow operator, or the full 64 KB `rc` loopback duplex under heavy TUI redraw — parked the drain thread WITH THE LOG LOCK HELD: outpu...\n 444|- **Invariant:** controller delivery runs OFF the drain thread — a dedicated controller writer thread + bounded channel (the 7.7 viewer_writer pattern) does the blocking socket write; the drain hands each chunk off with a BOUNDED, OFF-LOCK send (deadline → detach + `clear_controller`, never park forever, never silently evict a LIVE operator's authoritative view). The controller is AUTHORITATIVE (unlike a viewer): its writer advances the `delivered_through` resume cursor (atomic, only-on-success, mono...\n 445|- **spt-core mapping:** impl `broker.rs` `OutputLog::append`/`controller_writer`/`become_controller`/`ControllerJob::deliver`/`clear_controller`; int `crates/spt-daemon/tests/inject_control_wedge.rs` (a fully-backed-up controller keeps `KIND_SESSIONS` answering = no wedge); unit the bounded-deliver + monotonic-cursor kernels. The input-side parks (write_input `write_all` on a full buffer; DSR-answer writer-mutex contention) are BENIGN on Windows ConPTY (absorbs a large inject), real only on Unix forkpty ...\n...\n 454|\n*455|### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`\n 456|- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two ...\n 457|- **Invariant:** `apply_once` RELEASES the inner lock across `effect()` — reserve the key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable, mark applied). Crash-idempotency comes from the per-key reservation + the applied-set, not from holding the lock across the effect (the brain-crash guarantee is unchanged: the broker survives the brain, so reserve→effect→finalize runs uninterrupted on the broker threa...\n 458|- **spt-core mapping:** impl `effect.rs` `apply_once` (reserve/release/finalize) + `EffectKind::is_durable`; unit (`effect.rs`) a barrier proving the lock is NOT held across `effect()` (a different key's `apply_once` completes while one effect blocks) + `PtyWrite` writes no journal line while a durable kind (`NetSend`) does (the no-fsync proxy); int `crates/spt-daemon/tests/inject_control_wedge.rs` (real broker+PTY, a stalled consumer + sustained blocking input effect: a concurrent `spt rc` attach stays ...\n...\n 483|<!-- [doc->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] -->\n*484|### 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 485|- **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 486|- **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 487|- **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 497|<!-- [doc->REQ-HAZARD-CONTROLLER-WRITER-REORDER] -->\n*498|### 7.21 Exactly ONE `controller_writer` per brain↔broker connection — a superseded writer must write nothing further  `[REQ-HAZARD-CONTROLLER-WRITER-REORDER]`\n 499|- **Failure (doyle instrumented RACEDIAG repro, kitsubito):** on a brain-restart re-serve the handoff brain registered as controller on the SAME session TWICE over the SAME socket — `Brain::handoff` eagerly `subscribe(prior.next_seq=1)` → `become_controller(from_seq=1)`, spawning writer-A (writes seq 1); then `serve_attach` re-handled the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, spawning writer-B (writes 0,1). `become_controller` dropped the prior `Co...\n 500|- **Invariant:** on a single brain↔broker connection exactly ONE `controller_writer` is ever the LIVE writer; a SUPERSEDED writer writes no further frames after the epoch bump it observes; and **every `controller_writer` emits a strictly ASCENDING seq stream** (sorted initial batch + ascending live frames). The CORRECTNESS guarantee that falls out: a snap-above consumer over any interleaving of ascending writers — where the surviving writer (`serve_attach`'s `attach_as(sid,0)`) offers the COMPLETE ra...\n 501|- **spt-core mapping:** impl `brain.rs` `Brain::handoff` (KEEP the eager `subscribe`; seed `session_cursors`) + `Brain::subscribe_with` (resume-mode dedup-cursor reset to `from_seq`, shared by `attach`/`attach_as` — the operator-stream boundary fix) + `broker.rs` `OutputLog.controller_epoch: Arc<AtomicU64>` / `become_controller` (atomic `fetch_add`, passes the new epoch + `Arc::clone` into the writer) / `controller_writer` (epoch gate read UNDER the lock on both loops) / `mark_controller_gone` + the `C...\n...\n 539|\n*540|### 7.25 A perch PINNED to a DEAD session self-heals (dead-owner re-pin) instead of wedging forever; a LIVE-owner rotation still refuses  `[REQ-HAZARD-SESSION-PIN-WEDGE]`\n 541|- **Failure (F-024C/F-024D, ENLYZEAM field + clean-room repro 2026-07-02):** `authenticate()` (auth.rs) gates `api poll`/`state`/`boundary` on `proof.session_id == info.json.session_id`. If ONE boundary rotation is LOST — the departing session dies (crash / tab-close) or its `/clear`-era `boundary` call never lands — the perch stays PINNED to the dead sid, and every id-scoped hook call thereafter AUTH_REFUSES *including `boundary` itself* (the very call that would re-pin presents the NEW sid). A perm...\n 542|- **Invariant:** `authenticate()` gains a DEAD-OWNER fallback — when the caller's sid MISMATCHES the pin **AND** the perch's recorded owner pid is DEAD (`proc::is_process_alive == false`), ACCEPT the caller's sid and RE-PIN (rotate `info.json.session_id` via the locked `mutate_info` RMW + a LOUD `SESSION_REPIN` stderr line). Same trust model as `establish_perch`'s conflict gate, which already allows a rebind exactly when `owner_alive == false` (an orphaned perch accepts a new LOCAL owner). This is **AD...\n 543|- **spt-core mapping:** impl `spt/src/api/auth.rs` `authenticate` dead-owner-mismatch branch (`read_pid` → `proc::is_process_alive` → `mutate_info` rotate + `SESSION_REPIN` log); unit `auth.rs` `pinned_to_dead_sid_mismatched_poll_repins` (AUTH_REFUSED → drains+re-pins, RED pre-fix) + `live_owner_mismatch_still_refuses` (the `/clear` live-rotation squat guard, unwidened) + `token_auth_path_unchanged` (correct token wins first, no rotation).\n...\n 556|<!-- [doc->REQ-HAZARD-CONTROL-STAMP-LIFETIME] -->\n*557|### 7.27 A control/viewer stamp must NEVER outlive its session — every teardown path clears what attach stamped  `[REQ-HAZARD-CONTROL-STAMP-LIFETIME]`\n 558|- **Failure (F-026 #2, live evidence HFENDULEAM):** an spt-hosted endpoint stayed `ONLINE+CONTROLLED` after the operator's RC `/exit` — hours later hall-a's `info.json` still read `controlled:true` (status offline, dormant) and hfenduleam still GOSSIPED `controller_node=self` cross-node (SPT_DEV.json snapshot). ROOT: `/exit` kills the CHILD, not the controller connection, so the endpoint suspends via child termination and the teardown runs the REAP path (`broker.rs` exit-waiter → `sessions.remove(&id...\n 559|- **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 560|- **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...\n 575|<!-- [doc->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE] -->\n*576|### 7.29 Control/viewer stamps CONVERGE to broker session-table truth, not merely edge-trigger  `[REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]`\n 577|- **Failure (F-026 stamp-gap, hall-b + the original ball-b):** a picker-created endpoint (`endpoint run` → new) read plain `ONLINE` in the list + picker while genuinely driven — `info.json` `controlled:false` throughout. ROOT (the UPWARD companion to 7.27's downward edge-clear): the broker spawn path's `become_controller` → `stamp_driven_by` → `set_controlled(true)` fires at SPAWN time, but a FRESH endpoint has NO PERCH yet (the adapter binds it after claude boots), so `mutate_info` returns `NotF...\n 578|- **Invariant:** the broker (SINGLE writer) re-asserts each live session's control/viewer stamps to session-table TRUTH, DIVERGENCE-GATED — read the perch's recorded `(driven_by, controlled, viewer_count)`, compare to the live truth (`controller_by()` / `has_controller()` / live `viewer_count`), and write ONLY on a real difference (no per-poll fsync storm). The re-assert rides the `KIND_SESSIONS` handler (the daemon reconcile + picker poll it on cadence), so a fresh perch CONVERGES within one reconcile...\n 579|- **Coverage boundary (who MAY clear vs who MUST NOT — cross-ref 7.15):** the convergence's `driven_by`-clear leg fires ONLY when `has_controller()==false` — the DEFINITIVE broker signal (it reads its OWN controller slot, not the ambiguous `controller_by`). That is why the broker MAY clear a stale `driven_by` where the BRAIN reconcile (7.15) MUST NOT: the brain only sees `controller_by==None`, which a live LOCAL controller also reads, so a brain clear would false-heal a genuinely-driven session. This...\n...\n 603|\n*604|### 7.33 NO bare `.lock().unwrap()` on a broker-resident lock reachable from serve/dispatch — a poison permanently wedges every attach  `[REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]`\n 605|<!-- [doc->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON] -->\n 606|- **Failure (paid-for class):** a brain-only self-update restarts the BRAIN but keeps the BROKER process — and every one of its `Mutex`es — ALIVE by design (REQ-UPD-3). So a single panic while another thread held a broker-resident lock POISONS it permanently: the next `.lock().unwrap()` panics too, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines (`spt rc` → \"brain IPC read deadline elapsed\") while non-locked ops keep working. The effect journal (bug #16, `[REQ-HAZARD...\n 607|- **Invariant:** NO broker-resident lock reachable from serve/dispatch may use a bare `.lock().unwrap()`. Recover via the shared `recover` (`into_inner` — safe for the short, coherent-on-recovery map ops of `sessions`/`pair_holds`) or, for the `OutputLog` ring, `recover_log` — which adds a COHERENCE CLAMP on the recovery path: a panic mid-`append` can leave the ring torn (over-cap, a last seq not below `next_seq`, non-monotonic), and serving those bytes risks garbage, so `clamp_or_reset` cheap-checks...\n...\n 623|\n*624|### 7.36 The broker control plane and PTY fan-out must NEVER block on a single subscriber connection — a suspended brain conn must not wedge control  `[REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]`\n 625|<!-- [doc->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE] -->\n 626|- **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 627|- **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...\n 630|\n*631|### 7.37 The Layer-1 settle-gate must RE-ARM per delivery on an observable PTY — a mid-session reader reattach (`/clear`) re-creates the head-swallow race  `[REQ-HAZARD-INJECT-SETTLE-REARM]`\n 632|<!-- [doc->REQ-HAZARD-INJECT-SETTLE-REARM] -->\n 633|- **Failure (paid-for, field-CONFIRMED on 0.29.0 — doyle diagnosis + perri screenshot `WindowsTerminal_6iSjya8pMt.png`):** the shipped W5-A settle-gate (7-of-W5, `REQ-INJECT-MULTILINE-INTEGRITY`) gated Layer 1 behind a worker-local ONE-SHOT (`settled_once`) on the premise that the head-swallow race is a STARTUP condition (the input reader not yet attached after spawn). That premise is FALSE: a mid-session `/clear` re-enters the harness's raw-mode input reader, re-attaching it and RE-CREATING the pre-se...\n 634|- **Invariant:** the settle-gate re-arms before EVERY delivery on an OBSERVABLE (echoing/interactive) PTY — the bug-prone class, where re-settling is cheap (a tick or two once the reader answers) and necessary (a mid-session reader reattach must be re-confirmed). The steady-state settle is latch-skipped ONLY where the probe is known UNOBSERVABLE on this PTY (a non-echoing ConPTY whose DSR answer never surfaces): there is no reader-reattach race to guard, and each settle would burn the full `INJECT_SETT...\n...\n 637|\n*638|### 7.38 EVERY write on a physical broker connection is bounded + cancelable + poison-on-failure — no writer holds the send gate across an UNBOUNDED OS write  `[REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]`\n 639|<!-- [doc->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK] -->\n 640|- **Failure (paid-for, field capture + deterministic Windows repro 2026-07-09 — hertz RCA, UPDATE-WEDGE round-4):** `controller_writer` held the `SharedSend = Arc<Mutex<SendHalf>>` guard ACROSS a blocking `write_frame`. On Windows, `interprocess 2.4.2` routes the send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — this local-socket send path exposes **no supported write timeout** (`set_timeout` → `Unsupported`). An `rc --take` controller consumer that stopped reading blocked the write **indefi...\n 641|- **Invariant:** every write on a physical broker connection rides ONE broker-owned bounded/cancelable framed-write primitive with an **independent out-of-band aborter**. The `SendHalf` never leaves the conn object (a bypass cannot compile). An ABSOLUTE deadline stamped at write entry covers BOTH the serialized gate-wait AND OS write completion; a per-conn watchdog fires at that deadline and aborts the in-flight op — never relying on the write returning or on opportunistic stall-evict sampling. On dead..."}],"details":{"scopePath":"docs/KNOWN-HAZARDS.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\KNOWN-HAZARDS.md","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":25,"fileCount":1,"files":["docs/KNOWN-HAZARDS.md"],"fileMatches":[{"path":"docs/KNOWN-HAZARDS.md","count":25}],"truncated":false,"displayContent":"  22│\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    │...\n  28│\n *29│### 1.4 Deferred spool rows must not leak to the event stream\n  30│- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.\n  31│- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.\n  32│- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.\n    │...\n 206│<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->\n*207│### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`\n 208│- **Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::null...\n 209│- **Invariant:** every detach-spawn of a long-lived child runs with **`bInheritHandles = FALSE`** (`spt-daemon::daemon::detached_no_inherit`) — zero handles flow, whatever the pipe's depth in the ancestry. Stripping `HANDLE_FLAG_INHERIT` from the spawner's *std* handles is NOT sufficient: a grandparent capture's pipe sits in the handle table as a stray inheritable handle and still flows through every `bInheritHandles = TRUE` hop (the first guard shipped that way and was wedged by exactly this — a dae...\n 210│- **spt-core mapping:** `spt-daemon::daemon::spawn_detached` (the daemon) and `spt-daemon::shellhost::launch_shell` (the relay-receipt shell binary). Any future long-lived detached spawn (manifest-template children included) must use the same no-inherit spawn.\n    │...\n 324│\n*325│### 6.7 Broker and brain MUST be separate processes (in-process collapse silently breaks no-endpoint-drop update) `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n 326│- **Failure:** the daemon hosts the broker as a background *thread* in the single `spt daemon` process (`daemon.rs:165-170`, `Arc<Broker>` + `thread::spawn(serve)`) instead of a separate process. A brain restart onto a swapped binary then cannot happen without killing the broker thread — closing every PTY, orphaning every harness child, dropping every socket. So `spt update apply` degrades to an in-process `Brain::handoff` no-op: the binary swaps on disk but the running daemon keeps executing the old c...\n 327│- **Invariant:** the broker runs as its own long-lived process that survives every brain restart; the brain restarts onto the new binary and re-attaches via the versioned IPC. A routine (brain-only) update must leave every hosted endpoint untouched at the *process* level — not merely re-subscribe a brain within the same process. The evidence for REQ-UPD-3 / REQ-DAEMON-2 must prove process-level survival (a PTY child + a live QUIC conn survive a brain-process restart onto a swapped binary — SPIKE-01/0...\n 328│- **spt-core mapping:** restoration is ADR-0018 (next milestone). The current `int` tags on REQ-DAEMON-2 / REQ-UPD-3 are regression-masked and re-point at restoration; the broker becomes the always-up per-machine anchor (seed-lock + liveness + brain supervisor). Two-process supervision, generation custody, durable-deadline loop timing, broker-cursor-of-record, and readiness-gated auto-rollback all hang off this.\n    │...\n 343│\n*344│### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output\n 345│- **Failure:** a resume-mode brain (per-session `session_cursors` populated by `resume_sessions`) drives `Brain` over **blocking** `read_event` calls. Any command that loops `read_event` until its own reply — `spawn_session_pid` waiting for `Spawned`, `net_status`, `sessions`, etc. — calls `read_event` on *every* interleaved frame, so an OUTPUT frame for a **different** session is **cursor-processed** (its `session_cursors` entry snaps forward; the broker already counted it delivered via `delivered_t...\n 346│- **Invariant:** the daemon-hosted multi-session event loop must not consume a session's OUTPUT inside another session's blocking wait. The **live-agent adapter milestone must restructure the brain event pump** so command/`spawn` is non-blocking (a single demux loop owns `read_event` and routes every frame to its session's consumer), OR a blocking wait must re-queue/route the frames it reads for other sessions rather than dropping them.\n 347│- **spt-core mapping:** **unreachable today** — the supervised daemon brain hosts no PTY sessions and spawns none; a single-session seat (legacy, empty map) has no \"other session\" to starve. Surfaces the moment daemon-hosted sessions land (the live-agent adapter), which must rebuild the blocking `read_event` loop regardless (N interactive sessions cannot share one blocking reader). Recorded so that redesign inherits the constraint rather than rediscovering it. No machinery now (would be untested dead c...\n    │...\n 349│\n*350│### 6.10 Phase-significant loop timing must be a durable absolute-deadline grid, not phase-relative sleep `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n 351│- **Failure:** a periodic loop that sleeps a flat `period` each iteration (`pulse_tick` then `sleep(pulse_period)`) is **phase-relative** — every brain restart silently re-phases the grid to the restart instant. Under the seamless-update model (the supervisor respawns the brain onto a swapped binary, ADR-0018 D3-3), a routine update would shift the cadence of every phase-significant loop, and continuity cannot ride a brain→brain frame (the outgoing brain is gone before the new one starts — the same...\n 352│- **Invariant:** phase-significant periodic timing lives as durable absolute-deadline state on disk (`(anchor, interval)`), rehydrated on every brain start, with fires **derived functionally** (`next_fire = anchor + interval·⌈max(0,now−anchor)/interval⌉`) and **no per-fire write**. An **Update** restart re-reads the anchor and keeps deriving (phase preserved, lands mid-grid); a **Crash**/**Cold** restart re-bases the anchor to `now` (phase reset acceptable — the loop is idempotent catch-up). The...\n 353│- **spt-core mapping:** ADR-0018 Q4/V3/V4, restoration D5. Mechanism in `spt-daemon::deadline` (`DeadlineAnchor` periodic + `OneShotDeadline` rule-only pure helper); the pulse loop (`lifecycle::run_pulse_loop`) consumes it. The one-shot **machinery** (a durable in-daemon alarm scheduler) is the deferred alarm port (`docs/DEFERRED.md`) — the daemon has no one-shot consumer today, so building the timer now would ship untested dead code (activate-don't-pre-fail); D5 fixes the *rule* as a tested-unwired he...\n    │...\n 355│\n*356│### 6.11 Brain respawn must exec the APPLIED bytes, not the renamed old binary (Linux `current_exe` follows the apply-rename; readiness ≠ new-bytes) `[REQ-HAZARD-BRAIN-RESPAWN-PATH]`\n 357│- **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 358│- **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 359│- **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    │...\n 374│\n*375│### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`\n 376│- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.\n 377│- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.\n 378│- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.\n    │...\n 400│\n*401│### 7.6 Pump brain-IPC reads must be deadline-bounded (a blocked read wedges the whole pump)  `[REQ-HAZARD-PUMP-IPC-DEADLINE]`\n 402│<!-- [doc->REQ-HAZARD-PUMP-IPC-DEADLINE] -->\n 403│- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop...\n 404│- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a heal...\n    │...\n 407│\n*408│### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`\n 409│<!-- [doc->REQ-HAZARD-VIEWER-ISOLATION] -->\n 410│- **Failure:** the W2.5 controller/viewer model lets ANY number of read-only `--view` attachers ride one session's broker `OutputLog`. The single drain thread fans each output chunk to every attacher. If a viewer's socket is fanned out with a **blocking** write under the log lock (the controller's authoritative path), one wedged viewer (a slow terminal, a black-holed WAN peer, a hostile non-reader) stalls the drain — freezing the controller's stream and backing up the PTY child. A single watcher must n...\n 411│- **Invariant:** the drain writes the **controller** on the authoritative blocking bounded path (it alone advances `delivered_through`), but each **viewer** gets an **isolated bounded SPSC queue + a dedicated writer thread**; the drain `try_send`s under the log lock and **evicts** any viewer whose queue is `Full` (fell behind the live stream) or `Disconnected` (its writer died on a dead socket) — the drain thread **never touches a viewer socket**, so no viewer write can backpressure it. A **soft cap** ...\n    │...\n 414│\n*415│### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`\n 416│<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\n 417│- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only v...\n 418│- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`...\n    │...\n 421│\n*422│### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`\n 423│<!-- [doc->REQ-HAZARD-BROKER-SEED-WIRE-SKEW] -->\n 424│- **Failure:** the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004's no-terminate-during-update pillar forbids auto-killing it — 6.7). A self-update that changes a daemon-state WIRE FORMAT — e.g. the v0.9.0 adapter-agnostic `Seed` (the `adapter` field dropped) — therefore lands a NEW-version CLI talking to the STILL-RESIDENT OLD broker. The old broker cannot deserialize the new `Seed` (its formerly-required `adapter` is absent), so it drops the seed-...\n 425│- **Invariant:** (a) spt-core surfaces an ACTIONABLE diagnostic on the seed-ack EOF — naming the stale-broker cause + the fix (`spt daemon stop`; the broker restarts on the next `spt api` call) — never the bare io error (scoped to `UnexpectedEof` on the seed-ack path, so it never mis-fires on an unrelated error like a refused connect). (b) A daemon-state wire change requires a DELIBERATE full broker restart; this is NOT automatic — ADR-0004 forbids auto-killing the resident broker, and a brain-only...\n    │...\n 428│\n*429│### 7.10 A VIEW is independent from the endpoint — closing the launching tab must NOT reap the daemon-hosted harness  `[REQ-HAZARD-VIEWER-CLOSE-DETACH]`\n 430│<!-- [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH] -->\n 431│- **Failure (Windows):** `spt endpoint run` autostarts the daemon (`ensure_running` → `detached_no_inherit`), which INHERITS the launching terminal's Windows Job Object. Windows Terminal / VS Code place the shell AND every descendant in a Job with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; closing the tab drops the job's last handle → the OS reaps the daemon + its broker-spawned ConPTY harness subtree. The `rc` pump detaching should end ONLY the viewport; the harness must keep running + stay re-attachable...\n 432│- **Invariant:** both daemon spawn paths request `CREATE_BREAKAWAY_FROM_JOB` (0x0100_0000), best-effort with an in-job FALLBACK on `ERROR_ACCESS_DENIED`(5) / `ERROR_INVALID_PARAMETER`(87) so a breakaway-denying job NEVER regresses the spawn. **A real job CAN deny breakaway** (proven: the cargo/CI runner's own ancestor job ACCESS_DENIES it), so efficacy against the real terminal is UNKNOWN until measured. **int stage = OPERATOR MANUAL ACCEPTANCE, not CI** (doyle ruling 2026-06-18): the runner sits inside ...\n    │...\n 435│\n*436│### 7.11 A dead PTY child + a dropped operator pump must NOT wedge the broker for other clients  `[REQ-HAZARD-ATTACH-WEDGE]`\n 437│- **Failure (hypothesized, v0.12.0):** a legitimately dead PTY child (crash/kill) plus an `rc` pump dropped without a clean detach (closed tab) was thought to make the broker's loopback forward `write_all` block forever on a full 64 KB duplex, park a worker in the 2-worker net runtime, saturate both, and stall every new attach / `endpoint run` (with `daemon stop` unable to join).\n 438│- **Invariant — DISPOSITION = PROVE-DON'T-CHANGE** (doyle GATE-PASS @e883f45, 2026-06-18): the post-L0 code ALREADY prevents the wedge; NO fail-fast / worker-count code was added. (1) `serve_attach` forwards fire-and-forget (`net_stream_send` `op_id=None`) and the broker-side `send_stream` is already deadline-bounded (`bounded_block_on`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s — hazard 7.8), not forever. (2) the loopback duplex is drained broker-INTERNALLY by the operator row's OWN read pump (`nethost.rs` ...\n 439│- **spt-core mapping:** int `crates/spt/tests/attach_wedge_e2e.rs` (REAL detached daemon + dummy-harness fixture): serve the victim (rc sees its tick), abruptly kill rc (dropped pump) + kill the PTY child → a NEW endpoint still comes online + is served (no wedge), the dead endpoint is offlined within a tick (`LIVENESS_RECONCILE_OFFLINE`), `daemon stop` bounded. Leans on 7.8 (the QUIC-op deadline) + 7.7 (viewer isolation).\n    │...\n 441│\n*442│### 7.12 Controller output must NOT be written inline on the drain thread — a backed-up controller wedges the session  `[REQ-HAZARD-INJECT-CONTROL-COEXIST]`\n 443│- **Failure (v0.12.x, operator dogfooding + doyle /diagnose 2026-06-19):** `OutputLog::append` fanned each live chunk to the CONTROLLER via a SYNCHRONOUS, blocking `write_frame` held INLINE on the session's single drain thread while `Mutex<OutputLog>` was locked (viewers already had a dedicated writer thread + bounded evicting channel). A backed-up controller socket — a slow operator, or the full 64 KB `rc` loopback duplex under heavy TUI redraw — parked the drain thread WITH THE LOG LOCK HELD: outpu...\n 444│- **Invariant:** controller delivery runs OFF the drain thread — a dedicated controller writer thread + bounded channel (the 7.7 viewer_writer pattern) does the blocking socket write; the drain hands each chunk off with a BOUNDED, OFF-LOCK send (deadline → detach + `clear_controller`, never park forever, never silently evict a LIVE operator's authoritative view). The controller is AUTHORITATIVE (unlike a viewer): its writer advances the `delivered_through` resume cursor (atomic, only-on-success, mono...\n 445│- **spt-core mapping:** impl `broker.rs` `OutputLog::append`/`controller_writer`/`become_controller`/`ControllerJob::deliver`/`clear_controller`; int `crates/spt-daemon/tests/inject_control_wedge.rs` (a fully-backed-up controller keeps `KIND_SESSIONS` answering = no wedge); unit the bounded-deliver + monotonic-cursor kernels. The input-side parks (write_input `write_all` on a full buffer; DSR-answer writer-mutex contention) are BENIGN on Windows ConPTY (absorbs a large inject), real only on Unix forkpty ...\n    │...\n 454│\n*455│### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`\n 456│- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two ...\n 457│- **Invariant:** `apply_once` RELEASES the inner lock across `effect()` — reserve the key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable, mark applied). Crash-idempotency comes from the per-key reservation + the applied-set, not from holding the lock across the effect (the brain-crash guarantee is unchanged: the broker survives the brain, so reserve→effect→finalize runs uninterrupted on the broker threa...\n 458│- **spt-core mapping:** impl `effect.rs` `apply_once` (reserve/release/finalize) + `EffectKind::is_durable`; unit (`effect.rs`) a barrier proving the lock is NOT held across `effect()` (a different key's `apply_once` completes while one effect blocks) + `PtyWrite` writes no journal line while a durable kind (`NetSend`) does (the no-fsync proxy); int `crates/spt-daemon/tests/inject_control_wedge.rs` (real broker+PTY, a stalled consumer + sustained blocking input effect: a concurrent `spt rc` attach stays ...\n    │...\n 483│<!-- [doc->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] -->\n*484│### 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 485│- **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 486│- **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 487│- **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 497│<!-- [doc->REQ-HAZARD-CONTROLLER-WRITER-REORDER] -->\n*498│### 7.21 Exactly ONE `controller_writer` per brain↔broker connection — a superseded writer must write nothing further  `[REQ-HAZARD-CONTROLLER-WRITER-REORDER]`\n 499│- **Failure (doyle instrumented RACEDIAG repro, kitsubito):** on a brain-restart re-serve the handoff brain registered as controller on the SAME session TWICE over the SAME socket — `Brain::handoff` eagerly `subscribe(prior.next_seq=1)` → `become_controller(from_seq=1)`, spawning writer-A (writes seq 1); then `serve_attach` re-handled the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, spawning writer-B (writes 0,1). `become_controller` dropped the prior `Co...\n 500│- **Invariant:** on a single brain↔broker connection exactly ONE `controller_writer` is ever the LIVE writer; a SUPERSEDED writer writes no further frames after the epoch bump it observes; and **every `controller_writer` emits a strictly ASCENDING seq stream** (sorted initial batch + ascending live frames). The CORRECTNESS guarantee that falls out: a snap-above consumer over any interleaving of ascending writers — where the surviving writer (`serve_attach`'s `attach_as(sid,0)`) offers the COMPLETE ra...\n 501│- **spt-core mapping:** impl `brain.rs` `Brain::handoff` (KEEP the eager `subscribe`; seed `session_cursors`) + `Brain::subscribe_with` (resume-mode dedup-cursor reset to `from_seq`, shared by `attach`/`attach_as` — the operator-stream boundary fix) + `broker.rs` `OutputLog.controller_epoch: Arc<AtomicU64>` / `become_controller` (atomic `fetch_add`, passes the new epoch + `Arc::clone` into the writer) / `controller_writer` (epoch gate read UNDER the lock on both loops) / `mark_controller_gone` + the `C...\n    │...\n 539│\n*540│### 7.25 A perch PINNED to a DEAD session self-heals (dead-owner re-pin) instead of wedging forever; a LIVE-owner rotation still refuses  `[REQ-HAZARD-SESSION-PIN-WEDGE]`\n 541│- **Failure (F-024C/F-024D, ENLYZEAM field + clean-room repro 2026-07-02):** `authenticate()` (auth.rs) gates `api poll`/`state`/`boundary` on `proof.session_id == info.json.session_id`. If ONE boundary rotation is LOST — the departing session dies (crash / tab-close) or its `/clear`-era `boundary` call never lands — the perch stays PINNED to the dead sid, and every id-scoped hook call thereafter AUTH_REFUSES *including `boundary` itself* (the very call that would re-pin presents the NEW sid). A perm...\n 542│- **Invariant:** `authenticate()` gains a DEAD-OWNER fallback — when the caller's sid MISMATCHES the pin **AND** the perch's recorded owner pid is DEAD (`proc::is_process_alive == false`), ACCEPT the caller's sid and RE-PIN (rotate `info.json.session_id` via the locked `mutate_info` RMW + a LOUD `SESSION_REPIN` stderr line). Same trust model as `establish_perch`'s conflict gate, which already allows a rebind exactly when `owner_alive == false` (an orphaned perch accepts a new LOCAL owner). This is **AD...\n 543│- **spt-core mapping:** impl `spt/src/api/auth.rs` `authenticate` dead-owner-mismatch branch (`read_pid` → `proc::is_process_alive` → `mutate_info` rotate + `SESSION_REPIN` log); unit `auth.rs` `pinned_to_dead_sid_mismatched_poll_repins` (AUTH_REFUSED → drains+re-pins, RED pre-fix) + `live_owner_mismatch_still_refuses` (the `/clear` live-rotation squat guard, unwidened) + `token_auth_path_unchanged` (correct token wins first, no rotation).\n    │...\n 556│<!-- [doc->REQ-HAZARD-CONTROL-STAMP-LIFETIME] -->\n*557│### 7.27 A control/viewer stamp must NEVER outlive its session — every teardown path clears what attach stamped  `[REQ-HAZARD-CONTROL-STAMP-LIFETIME]`\n 558│- **Failure (F-026 #2, live evidence HFENDULEAM):** an spt-hosted endpoint stayed `ONLINE+CONTROLLED` after the operator's RC `/exit` — hours later hall-a's `info.json` still read `controlled:true` (status offline, dormant) and hfenduleam still GOSSIPED `controller_node=self` cross-node (SPT_DEV.json snapshot). ROOT: `/exit` kills the CHILD, not the controller connection, so the endpoint suspends via child termination and the teardown runs the REAP path (`broker.rs` exit-waiter → `sessions.remove(&id...\n 559│- **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 560│- **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    │...\n 575│<!-- [doc->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE] -->\n*576│### 7.29 Control/viewer stamps CONVERGE to broker session-table truth, not merely edge-trigger  `[REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]`\n 577│- **Failure (F-026 stamp-gap, hall-b + the original ball-b):** a picker-created endpoint (`endpoint run` → new) read plain `ONLINE` in the list + picker while genuinely driven — `info.json` `controlled:false` throughout. ROOT (the UPWARD companion to 7.27's downward edge-clear): the broker spawn path's `become_controller` → `stamp_driven_by` → `set_controlled(true)` fires at SPAWN time, but a FRESH endpoint has NO PERCH yet (the adapter binds it after claude boots), so `mutate_info` returns `NotF...\n 578│- **Invariant:** the broker (SINGLE writer) re-asserts each live session's control/viewer stamps to session-table TRUTH, DIVERGENCE-GATED — read the perch's recorded `(driven_by, controlled, viewer_count)`, compare to the live truth (`controller_by()` / `has_controller()` / live `viewer_count`), and write ONLY on a real difference (no per-poll fsync storm). The re-assert rides the `KIND_SESSIONS` handler (the daemon reconcile + picker poll it on cadence), so a fresh perch CONVERGES within one reconcile...\n 579│- **Coverage boundary (who MAY clear vs who MUST NOT — cross-ref 7.15):** the convergence's `driven_by`-clear leg fires ONLY when `has_controller()==false` — the DEFINITIVE broker signal (it reads its OWN controller slot, not the ambiguous `controller_by`). That is why the broker MAY clear a stale `driven_by` where the BRAIN reconcile (7.15) MUST NOT: the brain only sees `controller_by==None`, which a live LOCAL controller also reads, so a brain clear would false-heal a genuinely-driven session. This...\n    │...\n 603│\n*604│### 7.33 NO bare `.lock().unwrap()` on a broker-resident lock reachable from serve/dispatch — a poison permanently wedges every attach  `[REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]`\n 605│<!-- [doc->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON] -->\n 606│- **Failure (paid-for class):** a brain-only self-update restarts the BRAIN but keeps the BROKER process — and every one of its `Mutex`es — ALIVE by design (REQ-UPD-3). So a single panic while another thread held a broker-resident lock POISONS it permanently: the next `.lock().unwrap()` panics too, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines (`spt rc` → \"brain IPC read deadline elapsed\") while non-locked ops keep working. The effect journal (bug #16, `[REQ-HAZARD...\n 607│- **Invariant:** NO broker-resident lock reachable from serve/dispatch may use a bare `.lock().unwrap()`. Recover via the shared `recover` (`into_inner` — safe for the short, coherent-on-recovery map ops of `sessions`/`pair_holds`) or, for the `OutputLog` ring, `recover_log` — which adds a COHERENCE CLAMP on the recovery path: a panic mid-`append` can leave the ring torn (over-cap, a last seq not below `next_seq`, non-monotonic), and serving those bytes risks garbage, so `clamp_or_reset` cheap-checks...\n    │...\n 623│\n*624│### 7.36 The broker control plane and PTY fan-out must NEVER block on a single subscriber connection — a suspended brain conn must not wedge control  `[REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]`\n 625│<!-- [doc->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE] -->\n 626│- **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 627│- **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    │...\n 630│\n*631│### 7.37 The Layer-1 settle-gate must RE-ARM per delivery on an observable PTY — a mid-session reader reattach (`/clear`) re-creates the head-swallow race  `[REQ-HAZARD-INJECT-SETTLE-REARM]`\n 632│<!-- [doc->REQ-HAZARD-INJECT-SETTLE-REARM] -->\n 633│- **Failure (paid-for, field-CONFIRMED on 0.29.0 — doyle diagnosis + perri screenshot `WindowsTerminal_6iSjya8pMt.png`):** the shipped W5-A settle-gate (7-of-W5, `REQ-INJECT-MULTILINE-INTEGRITY`) gated Layer 1 behind a worker-local ONE-SHOT (`settled_once`) on the premise that the head-swallow race is a STARTUP condition (the input reader not yet attached after spawn). That premise is FALSE: a mid-session `/clear` re-enters the harness's raw-mode input reader, re-attaching it and RE-CREATING the pre-se...\n 634│- **Invariant:** the settle-gate re-arms before EVERY delivery on an OBSERVABLE (echoing/interactive) PTY — the bug-prone class, where re-settling is cheap (a tick or two once the reader answers) and necessary (a mid-session reader reattach must be re-confirmed). The steady-state settle is latch-skipped ONLY where the probe is known UNOBSERVABLE on this PTY (a non-echoing ConPTY whose DSR answer never surfaces): there is no reader-reattach race to guard, and each settle would burn the full `INJECT_SETT...\n    │...\n 637│\n*638│### 7.38 EVERY write on a physical broker connection is bounded + cancelable + poison-on-failure — no writer holds the send gate across an UNBOUNDED OS write  `[REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]`\n 639│<!-- [doc->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK] -->\n 640│- **Failure (paid-for, field capture + deterministic Windows repro 2026-07-09 — hertz RCA, UPDATE-WEDGE round-4):** `controller_writer` held the `SharedSend = Arc<Mutex<SendHalf>>` guard ACROSS a blocking `write_frame`. On Windows, `interprocess 2.4.2` routes the send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — this local-socket send path exposes **no supported write timeout** (`set_timeout` → `Unsupported`). An `rc --take` controller consumer that stopped reading blocked the write **indefi...\n 641│- **Invariant:** every write on a physical broker connection rides ONE broker-owned bounded/cancelable framed-write primitive with an **independent out-of-band aborter**. The `SendHalf` never leaves the conn object (a bypass cannot compile). An ABSOLUTE deadline stamped at write entry covers BOTH the serialized gate-wait AND OS write completion; a per-conn watchdog fires at that deadline and aborts the in-flight op — never relying on the write returning or on opportunistic stall-evict sampling. On dead..."},"isError":false,"timestamp":1784195763335}}
{"type":"message","id":"219022ec","parentId":"7475a9be","timestamp":"2026-07-16T09:56:03.337Z","message":{"role":"toolResult","toolCallId":"call_bmxBswjvehJEOqPl5JOQoLhr|fc_008390f508d1d09b016a58aab33720819a827e5496805b730d","toolName":"grep","content":[{"type":"text","text":" 38|**in-session relay**:\n*39|A thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle de...\n 40|\n 41|### Deliverable shape\n 42|\n...\n 152|\n*153|**message metadata (`json`)** — a sender may attach an opaque JSON metadata block (`--json-payload`), carried as a single attr-escaped `json=\"…\"` envelope attribute **alongside** (never replacing) the body. spt-core never interprets it — pure verbatim passthrough across every rail (spool / TCP / WAN / EVENT-PART), parsed only by the receiving adapter (its hooks and/or translation binary). Collision-proof by construction: the structured data lives **inside** the single `json` value, so it can never ...\n 154|\n 155|**resume-session seam** — two distinct forms:\n 156|- **fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> That command is **`spt api psyche-download <id> [--session-id <sid>]`**: it emits the durable resume brief (role → live-context → project-context, project resolved from the perch's bound cwd)...\n...\n 264|\n*265|A Gateway endpoint binary is revived by **existing machinery only** (settled 2026-06-12, two corrections deep): while running, the bridged device's link liveness drives ordinary **instance state** (sustained device silence → dormant; device contact → active — the driver-attach rule). Across a node restart, revival rides a **co-located shell's wake-watcher** — the Gateway typically owns a shell instance on its own gateway host; that shell's offline wake-watcher (one of the two classes of third-par...\n 266|_Avoid_: calling a Gateway a Shell or an agent; \"console\", \"remote\".\n 267|\n 268|**PresenceChannel** (broker endpoint — concept locked, impl deferred past v1):\n...\n 284|- **active** — the **actively-driven** instance: the one a bare-`id` message resolves to. (Driving `ling@laptop` makes `ling@desktop` **dormant**.)\n*285|- **dormant** — **warm** (still running / in-memory) but **not the active target** — a sibling took attention (`AttentionShift`) or the driver detached (`Detach`). Genuinely *available* (a valid routing fallback) — the picker shows it online. Decays to *suspended* via an auto-suspend timer.\n 286|- **suspended** — **cold**: the session is closed, **resumed-on-wake**, but **its node is up** (the daemon still gossips the row); still **addressable** (a `wake` must route).\n*287|- **offline** — the endpoint's **node is down** / unroutable. **Never self-gossiped** (a down node cannot gossip) — a remote viewer infers it when the node stops gossiping (epoch-lease eviction); the resolver skips it.\n 288|\n 289|The active/dormant discriminator is **running-ness + attention** (warm and which instance holds the drive); the suspended/offline discriminator is **node up vs node down**. _Avoid_: advertising a **not-running** (cold, no live session) perch as `dormant` — `dormant` requires the session to be warm/running; a cold perch on a **live** node is **suspended** (node up, endpoint cold), and a live node **never** self-gossips `offline`. (Bound-gated subtlety: an **unbound** perch reads `is_perch_alive==false` ...\n 290|\n...\n 347|<!-- [doc->REQ-CONSENT-3] -->\n*348|**per-capability approval gates** (ratified 2026-06-11, Gateway grill): the same `require_approval` enum may ride **individual capability entries** in a shell manifest — gating the dangerous *operation*, not just the spawn. Same grant store, same interactive escalation, same floor semantics. A capability may declare a **class key** so grants are scoped finer than the capability itself: the first consumer is the usbip shell's `attach`, granted per **(owner endpoint × device class × node)** — a remem...\n 349|\n 350|**instance cap (`max_instances_per_owner` + `over_cap`, manifest):** an optional ceiling on how many instances of this shell adapter **one owner endpoint** may hold. The count is **all existing instances** — online *and* offline, every non-torn-down perch (`shell teardown` frees a slot) — so offline persistent shells cannot be stockpiled to evade it. Unset ⇒ unlimited. At the cap, **`over_cap`** decides: `reject` (**default**) refuses the spawn outright; `approve` requires per-spawn approval for ea...\n 351|\n...\n 375|- owner **suspended** → revive the owner, then online the shell;\n*376|- owner **active** elsewhere → attach/online the shell;\n 377|- **no reachable instance** (home node offline) → no-op, **unless** the owner's **`shell_wake_spawn_anywhere`** settings flag is set, which pre-consents shells to **fresh-spawn the owner on any available node** to wake it (the flag *is* the consent for this otherwise-deferred instantiate-anywhere path).\n 378|\n 379|The same `spt endpoint shutdown` / `api owner-shutdown` / avatar-wake machinery composes into a full sleep/wake cycle driven from either end.\n 380|\n*381|**remote attach (`spt rc`)** (ratified 2026-06-12 — own milestone, rides the built R-TERM substrate):\n*382|The user-facing remote-terminal product surface: `spt rc [subnet:]<id>[@node]` attaches across the subnet to an spt-hosted session's broker surface — scrollback replay + live bytes down, keystrokes up. Address-gated like messaging, **plus a consent gate** (driving a session ≠ watching it). **Controller/viewer model:** one interactive **controller** at a time — its viewport sets the PTY size (resize fires on attach + controller window changes; ConPTY repaint cost is why resize stays controller-exclu...\n*383|_Avoid_: \"screen share\"; resize-per-viewer; silent controller displacement.\n 384|\n*385|<!-- [doc->REQ-RCVIEW-1] [doc->REQ-KICK-1] [doc->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT] [doc->REQ-HAZARD-VIEWER-RING-ROLL-SNAP] -->\n*386|**BUILT (M12 W2.5).** The controller/viewer model is implemented end-to-end. Attach intent is **three-valued** (`AttachIntent = Viewer | Control | Take`, wire-default `Control`): `Control` to a FREE endpoint becomes controller; `Control` to a CONTROLLED endpoint is **refused with guidance** (`--view` to watch, `--take` to control) — never auto-viewer, never silent-displace; `Take` (`spt rc --take` / picker \"Kick\") kicks the incumbent with a **loud `Displaced{by}` notice** and full detach (not demote). ...\n 387|\n 388|<!-- [doc->REQ-RC-KEY-VT-TRANSLATE] -->\n*389|**rc keyboard input (Windows VT translation, v0.13.0 bug 2).** On **Windows** an interactive `spt rc` console reads crossterm **key events** and translates each to **standard xterm VT** (`translate_key_event`) — arrows / Home / End / PgUp/Dn / Insert / Delete / F-keys + modifiers all reach the harness as the universal terminal contract (**agnostic**, NOT win32-input-mode; the legacy console delivers those keys as events, not bytes, so the old byte-pump left them DEAD). **Unix passes through** (its raw-...\n 390|\n 391|**spt-hosted bringup picker (`spt endpoint run`)** (M12-W2):\n 392|<!-- [doc->REQ-RUN-PICKER] -->\n...\n 395|non-interactive bringup path (`--adapter <a[:profile]> --id <id> --create|--resume <session>\n*396|--start|--attach|--view`), untouched — a picker selection bakes exactly that path. **Layer 1**\n 397|picks the kind (*Create new* | *Pick existing*). **Create-new** chooses a registered\n 398|`kind=\"harness\"` adapter with its shipped+local **profiles tree-nested**, then a\n 399|charset-validated id, then — **on a node that holds ≥2 member subnets** — a **home-subnet\n...\n 408|**status square** (online green ■ / offline gray ▢ — the blue *attached* tri-state + *Kick* are\n*409|**W2.5**, a dedicated broker attach-presence slice), **type-to-filter** (`/`), a pinned keybind\n 410|legend, and a **two-pane** right-half description (harness `adapter:profile` · best-effort\n 411|project history newest→oldest · `endpoint description`). The **confirm** layer offers\n*412|status-dependent options — Attach/Start/View (the rc pump / bringup) · Instantiate-locally\n 413|(remote) · Change-harness-adapter (offline) · Fork · **Resume-from-history** (offline+LOCAL\n 414|only — enumerates the per-endpoint session ledger, titles `<project> @ <ts> (…id5)`, resumes\n 415|that session id). **Invariant:** the picker is a pure front-end — every terminal action routes\n...\n 442|project root that bakes the current selection's **non-interactive** flags (terminal actions\n*443|only: adapter[:profile] + id + create|resume + start|attach|view; the interactive-only branches\n 444|— Kick/Instantiate/Change-adapter/Fork — are not bakeable). The **basename is a parameter**:\n 445|harness-agnostic spt-core defaults to **`spt`** (→ `spt-<id>`, e.g. `spt-doyle`); an\n 446|adapter/flow **overrides** it (spt-claude-code → `cc`, giving `cc-<id>`) — the Claude-Code-ness\n...\n 461|\n*462|## Terminal wrapper\n 463|\n 464|The multiplatform terminal wrapper is the backbone for hosting agent sessions. Supersedes the sister project's planned \"capsule\" (which leaned on psmux). Architecturally a **process supervisor** with an abstract session surface. **The session-surface *mechanism* is delivered in M3a (`crates/spt-term`); the supervisor *process* (term-daemon) is delivered in M3b (`crates/spt-daemon`, 2026-06-03).**\n 465|\n 466|**term-daemon** (process supervisor):\n*467|A long-lived, one-per-machine daemon that owns the PTYs for all hosted sessions, multiplexes them (sessions addressable by name, tmux-style), and accepts attach/detach from frontends. A hosted agent session (a LiveAgent's Self, a ReadyAgent, etc.) runs *inside* a supervised PTY. **Headed** = a frontend is attached and rendering/driving the PTY; **headless** = the session runs with no frontend attached. The daemon's persistence is what lets a session keep running while unwatched and be re-attached \"on a w...\n 468|\n 469|**A view is independent from the endpoint** (invariant):\n*470|<!-- [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH] -->\n 471|An spt-hosted endpoint runs in a **daemon-owned PTY, decoupled from whatever terminal launched it**. Closing the\n*472|tab/window where `spt endpoint run` was invoked detaches only the `spt rc` attach pump — the endpoint keeps running under\n 473|the daemon and stays re-attachable via `spt rc <id>`. A view is a transient frontend over a daemon-owned session, never\n 474|the session's lifeline. *Implementation:* the daemon must never live inside the launching terminal's process grouping —\n 475|on Windows the cold-started daemon is launched **job-neutral**: a job-neutral creator (**WMI `Win32_Process.Create`**,\n...\n 487|**input injection**:\n*488|Two granularities, both first-class. `send-keys` — raw byte stream including escape sequences (e.g. `Ctrl-C`). `send-line` — cooked line-level input flushed on newline (the common case; matches how user input reaches an agent prompt). **PTY input is single-writer (v0.13.0 P0, KNOWN-HAZARDS 7.17):** broker-side, every write to a hosted session's PTY goes through ONE per-session input-writer thread — the sole caller of the blocking `write_input`, fed by a bounded FIFO. All callers (operator keystroke...\n 489|\n 490|**scrollback**:\n 491|In-memory ring buffer per session for v1. **On-disk spillover for long sessions is an intended, tracked feature** (deferred past v1, not forgotten) — see `docs/DEFERRED.md`.\n...\n 562|\n*563|## Self-update\n 564|\n 565|<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->\n 566|Seamless, realtime self-updating is a day-one pillar. After a plugin performs the initial spt-core bootstrap, `spt.exe` self-updates from then on and **ripple-updates registered adapters** via each adapter's manifest update declaration. An adapter may include a `[update].message` — a plain human notice (markdown-rendered) that `spt adapter update` prints to stdout only when a new version is **actually applied** (never on a no-op). Useful for post-update operator actions, e.g. `\"Run \\`/reload-plugins\\` ...\n...\n 587|\n*588|**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 589|\n*590|**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 591|\n 592|**broker update frequency** — rare by design. Triggers: broker↔brain IPC contract change (versioned so a newer brain talks to an older broker — broker stays put as the brain updates), held-resource-type change, OS PTY/socket API change, or a broker bugfix. When the broker *must* update (the one case that can disturb held endpoints), it is a small well-scoped place that can use FD-passing or a rare planned endpoint-cycle — not the whole daemon.\n 593|\n...\n 644|\n*645|<!-- [doc->REQ-RC-CROSS-NODE-ATTACH] -->\n 646|**Remote-control vs local operation (two distinct modes — not the same as instances):**\n 647|- **Operate locally:** drive the native instance on *your* machine (its local files, its synced mind). The normal case.\n*648|- **Remote-control (Shell-like):** attach a control/view surface to an instance *running on another node* — compute + files stay remote; you are a viewport (the byte-stream terminal attach, daemon-to-daemon over Iroh). Used when you specifically want *that machine's* environment. This is effectively a Shell (a driven surface, user→agent direction), separate from the instance concept itself.\n*649|- **Owning-node resolution (client leg):** `spt rc <id>` first looks for a live session in the **local** broker table; on a miss it **resolves the owning node from the registry** (the same refuse-and-qualify resolution the message leg uses) and **dials it**, sending the **endpoint id** — the owning node is authoritative for its own session table, so it maps endpoint → its session id **server-side** (a remote operator never knows, nor guesses, the remote session id). The UX is unchanged (`spt rc` \"jus...\n 650|- **Smooth handoff (remote-control → local):** no teardown required. `git pull` brings the files; the **fresh-with-preload resume seam** preloads the other instance's latest synced context (psyche-download); the remote instance simply goes dormant. The mind follows; the files are local.\n 651|\n 652|### Pieces the Instances model requires\n...\n 662|\n*663|<!-- [doc->REQ-ENDPOINT-UNBOUND-ATTACH] -->\n 664|**Unbound endpoint**:\n*665|The lifecycle point between *spawn* and *bind*: an spt-hosted endpoint whose broker **session + PTY are live** but whose harness has **not yet bound** its perch (the *post-spawn seam* hasn't fired — e.g. the harness is waiting on a startup prompt). On-disk status `unbound` (spawn → `unbound`; bind → `online`; session death → `offline`). An Unbound endpoint is **attachable** (a live PTY — `spt rc` and the `endpoint run` attach reach it, so an operator can see and drive the harness, including cle...\n 666|\n 667|<!-- [doc->REQ-INST-14] -->\n 668|**resource advertisement (subnet resource registry)**:\n*669|A per-endpoint **free-text blurb** describing the services/functions the endpoint can serve — an agent **yellow-pages** for service discovery, distinct from *capability declaration* (machine-readable, which endpoint *types* a node hosts) and from *endpoint visibility* (whether it's addressable at all). **Both-authored + mutable:** config seeds a default; the agent refines its own at runtime (`spt endpoint description set …`). It is **not a separate registry** — it is a field on the endpoint record ...\n 670|\n 671|**resolution policy**:\n 672|How bare `ling` resolves when multiple instances exist. Rule: local-node instance if present → else most-recently-active → explicit override always available. **Two qualifier syntaxes**, combinable: **subnet-qualified** `home:ling` and **node-qualified** `ling@hfenduleam` (→ `home:ling@hfenduleam`). **Multi-subnet ambiguity rule:** under node-global identity (endpoint ID below), a node on several subnets may see two *distinct* endpoints that share a bare name (a `home:ling` and an unrelated `work:l...\n...\n 786|**subnet attachment (attached / detached)**:\n*787|Whether this node's daemon is **actively serving** a held subnet membership right now — pairing responder reachable, rendezvous meet listener rotating, registry gossip pumping. **Attached** = serving; **detached** = the membership record (seed) is held on disk but deliberately not served (the daemon neither advertises into nor connects to that subnet). Detachment is a *chosen* per-subnet state — `spt subnet detach/attach <NAME> [--save]` (shipped M8-D2; `--save` persists the startup default in daemon...\n 788|\n 789|**endpoint description** (canonical CLI term for the resource-advertisement blurb):\n 790|The per-endpoint free-text \"yellow-pages\" line (see §resource advertisement). Surface term is **description**; \"blurb\" survives only as the internal field name.\n...\n 871|\n*872|The day-one headed frontend is a launcher/manager UX, not just a raw `attach`. The end user cares about *reaching a target agent*, not about PTY mechanics. The frontend must:\n 873|\n 874|- **List** all running and historic endpoints.\n 875|- **Launch a historic endpoint** that isn't currently running, reusing the same manifest that originally launched it.\n*876|- **Tap into** (attach to) a running endpoint.\n 877|- **Init a new endpoint** using a known adapter (e.g. claude-code / spt-plugin).\n 878|\n*879|This is the realization of the \"guided resume\" (resume by project or agent, XMB-style filtered selection) and \"management GUI\" (per-endpoint panes: latest session output, live/project context, psyche log) sketches. `attach` is one operation under this frontend, not the whole frontend.\n 880|\n 881|**guided-resume picker (`spt resume`, no-arg)**:\n*882|The CLI sibling of the frontend's guided resume — one command that lists endpoints **grouped by locality, most-recently-used within each group**: `on-node / current-project → on-node / other-project → off-node`, mirroring the *resolution policy*'s local-first preference. Selection **chains conditionally**: a **running** instance → attach/tap-in (no adapter step — already live under one); a **non-running** endpoint → into the **adapter selector** (*adapter selection*: history head = default ...\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"CONTEXT.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\CONTEXT.md","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":36,"fileCount":1,"files":["CONTEXT.md"],"fileMatches":[{"path":"CONTEXT.md","count":36}],"truncated":true,"displayContent":"  38│**in-session relay**:\n *39│A thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle de...\n  40│\n  41│### Deliverable shape\n  42│\n    │...\n 152│\n*153│**message metadata (`json`)** — a sender may attach an opaque JSON metadata block (`--json-payload`), carried as a single attr-escaped `json=\"…\"` envelope attribute **alongside** (never replacing) the body. spt-core never interprets it — pure verbatim passthrough across every rail (spool / TCP / WAN / EVENT-PART), parsed only by the receiving adapter (its hooks and/or translation binary). Collision-proof by construction: the structured data lives **inside** the single `json` value, so it can never ...\n 154│\n 155│**resume-session seam** — two distinct forms:\n 156│- **fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> That command is **`spt api psyche-download <id> [--session-id <sid>]`**: it emits the durable resume brief (role → live-context → project-context, project resolved from the perch's bound cwd)...\n    │...\n 264│\n*265│A Gateway endpoint binary is revived by **existing machinery only** (settled 2026-06-12, two corrections deep): while running, the bridged device's link liveness drives ordinary **instance state** (sustained device silence → dormant; device contact → active — the driver-attach rule). Across a node restart, revival rides a **co-located shell's wake-watcher** — the Gateway typically owns a shell instance on its own gateway host; that shell's offline wake-watcher (one of the two classes of third-par...\n 266│_Avoid_: calling a Gateway a Shell or an agent; \"console\", \"remote\".\n 267│\n 268│**PresenceChannel** (broker endpoint — concept locked, impl deferred past v1):\n    │...\n 284│- **active** — the **actively-driven** instance: the one a bare-`id` message resolves to. (Driving `ling@laptop` makes `ling@desktop` **dormant**.)\n*285│- **dormant** — **warm** (still running / in-memory) but **not the active target** — a sibling took attention (`AttentionShift`) or the driver detached (`Detach`). Genuinely *available* (a valid routing fallback) — the picker shows it online. Decays to *suspended* via an auto-suspend timer.\n 286│- **suspended** — **cold**: the session is closed, **resumed-on-wake**, but **its node is up** (the daemon still gossips the row); still **addressable** (a `wake` must route).\n*287│- **offline** — the endpoint's **node is down** / unroutable. **Never self-gossiped** (a down node cannot gossip) — a remote viewer infers it when the node stops gossiping (epoch-lease eviction); the resolver skips it.\n 288│\n 289│The active/dormant discriminator is **running-ness + attention** (warm and which instance holds the drive); the suspended/offline discriminator is **node up vs node down**. _Avoid_: advertising a **not-running** (cold, no live session) perch as `dormant` — `dormant` requires the session to be warm/running; a cold perch on a **live** node is **suspended** (node up, endpoint cold), and a live node **never** self-gossips `offline`. (Bound-gated subtlety: an **unbound** perch reads `is_perch_alive==false` ...\n 290│\n    │...\n 347│<!-- [doc->REQ-CONSENT-3] -->\n*348│**per-capability approval gates** (ratified 2026-06-11, Gateway grill): the same `require_approval` enum may ride **individual capability entries** in a shell manifest — gating the dangerous *operation*, not just the spawn. Same grant store, same interactive escalation, same floor semantics. A capability may declare a **class key** so grants are scoped finer than the capability itself: the first consumer is the usbip shell's `attach`, granted per **(owner endpoint × device class × node)** — a remem...\n 349│\n 350│**instance cap (`max_instances_per_owner` + `over_cap`, manifest):** an optional ceiling on how many instances of this shell adapter **one owner endpoint** may hold. The count is **all existing instances** — online *and* offline, every non-torn-down perch (`shell teardown` frees a slot) — so offline persistent shells cannot be stockpiled to evade it. Unset ⇒ unlimited. At the cap, **`over_cap`** decides: `reject` (**default**) refuses the spawn outright; `approve` requires per-spawn approval for ea...\n 351│\n    │...\n 375│- owner **suspended** → revive the owner, then online the shell;\n*376│- owner **active** elsewhere → attach/online the shell;\n 377│- **no reachable instance** (home node offline) → no-op, **unless** the owner's **`shell_wake_spawn_anywhere`** settings flag is set, which pre-consents shells to **fresh-spawn the owner on any available node** to wake it (the flag *is* the consent for this otherwise-deferred instantiate-anywhere path).\n 378│\n 379│The same `spt endpoint shutdown` / `api owner-shutdown` / avatar-wake machinery composes into a full sleep/wake cycle driven from either end.\n 380│\n*381│**remote attach (`spt rc`)** (ratified 2026-06-12 — own milestone, rides the built R-TERM substrate):\n*382│The user-facing remote-terminal product surface: `spt rc [subnet:]<id>[@node]` attaches across the subnet to an spt-hosted session's broker surface — scrollback replay + live bytes down, keystrokes up. Address-gated like messaging, **plus a consent gate** (driving a session ≠ watching it). **Controller/viewer model:** one interactive **controller** at a time — its viewport sets the PTY size (resize fires on attach + controller window changes; ConPTY repaint cost is why resize stays controller-exclu...\n*383│_Avoid_: \"screen share\"; resize-per-viewer; silent controller displacement.\n 384│\n*385│<!-- [doc->REQ-RCVIEW-1] [doc->REQ-KICK-1] [doc->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT] [doc->REQ-HAZARD-VIEWER-RING-ROLL-SNAP] -->\n*386│**BUILT (M12 W2.5).** The controller/viewer model is implemented end-to-end. Attach intent is **three-valued** (`AttachIntent = Viewer | Control | Take`, wire-default `Control`): `Control` to a FREE endpoint becomes controller; `Control` to a CONTROLLED endpoint is **refused with guidance** (`--view` to watch, `--take` to control) — never auto-viewer, never silent-displace; `Take` (`spt rc --take` / picker \"Kick\") kicks the incumbent with a **loud `Displaced{by}` notice** and full detach (not demote). ...\n 387│\n 388│<!-- [doc->REQ-RC-KEY-VT-TRANSLATE] -->\n*389│**rc keyboard input (Windows VT translation, v0.13.0 bug 2).** On **Windows** an interactive `spt rc` console reads crossterm **key events** and translates each to **standard xterm VT** (`translate_key_event`) — arrows / Home / End / PgUp/Dn / Insert / Delete / F-keys + modifiers all reach the harness as the universal terminal contract (**agnostic**, NOT win32-input-mode; the legacy console delivers those keys as events, not bytes, so the old byte-pump left them DEAD). **Unix passes through** (its raw-...\n 390│\n 391│**spt-hosted bringup picker (`spt endpoint run`)** (M12-W2):\n 392│<!-- [doc->REQ-RUN-PICKER] -->\n    │...\n 395│non-interactive bringup path (`--adapter <a[:profile]> --id <id> --create|--resume <session>\n*396│--start|--attach|--view`), untouched — a picker selection bakes exactly that path. **Layer 1**\n 397│picks the kind (*Create new* | *Pick existing*). **Create-new** chooses a registered\n 398│`kind=\"harness\"` adapter with its shipped+local **profiles tree-nested**, then a\n 399│charset-validated id, then — **on a node that holds ≥2 member subnets** — a **home-subnet\n    │...\n 408│**status square** (online green ■ / offline gray ▢ — the blue *attached* tri-state + *Kick* are\n*409│**W2.5**, a dedicated broker attach-presence slice), **type-to-filter** (`/`), a pinned keybind\n 410│legend, and a **two-pane** right-half description (harness `adapter:profile` · best-effort\n 411│project history newest→oldest · `endpoint description`). The **confirm** layer offers\n*412│status-dependent options — Attach/Start/View (the rc pump / bringup) · Instantiate-locally\n 413│(remote) · Change-harness-adapter (offline) · Fork · **Resume-from-history** (offline+LOCAL\n 414│only — enumerates the per-endpoint session ledger, titles `<project> @ <ts> (…id5)`, resumes\n 415│that session id). **Invariant:** the picker is a pure front-end — every terminal action routes\n    │...\n 442│project root that bakes the current selection's **non-interactive** flags (terminal actions\n*443│only: adapter[:profile] + id + create|resume + start|attach|view; the interactive-only branches\n 444│— Kick/Instantiate/Change-adapter/Fork — are not bakeable). The **basename is a parameter**:\n 445│harness-agnostic spt-core defaults to **`spt`** (→ `spt-<id>`, e.g. `spt-doyle`); an\n 446│adapter/flow **overrides** it (spt-claude-code → `cc`, giving `cc-<id>`) — the Claude-Code-ness\n    │...\n 461│\n*462│## Terminal wrapper\n 463│\n 464│The multiplatform terminal wrapper is the backbone for hosting agent sessions. Supersedes the sister project's planned \"capsule\" (which leaned on psmux). Architecturally a **process supervisor** with an abstract session surface. **The session-surface *mechanism* is delivered in M3a (`crates/spt-term`); the supervisor *process* (term-daemon) is delivered in M3b (`crates/spt-daemon`, 2026-06-03).**\n 465│\n 466│**term-daemon** (process supervisor):\n*467│A long-lived, one-per-machine daemon that owns the PTYs for all hosted sessions, multiplexes them (sessions addressable by name, tmux-style), and accepts attach/detach from frontends. A hosted agent session (a LiveAgent's Self, a ReadyAgent, etc.) runs *inside* a supervised PTY. **Headed** = a frontend is attached and rendering/driving the PTY; **headless** = the session runs with no frontend attached. The daemon's persistence is what lets a session keep running while unwatched and be re-attached \"on a w...\n 468│\n 469│**A view is independent from the endpoint** (invariant):\n*470│<!-- [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH] -->\n 471│An spt-hosted endpoint runs in a **daemon-owned PTY, decoupled from whatever terminal launched it**. Closing the\n*472│tab/window where `spt endpoint run` was invoked detaches only the `spt rc` attach pump — the endpoint keeps running under\n 473│the daemon and stays re-attachable via `spt rc <id>`. A view is a transient frontend over a daemon-owned session, never\n 474│the session's lifeline. *Implementation:* the daemon must never live inside the launching terminal's process grouping —\n 475│on Windows the cold-started daemon is launched **job-neutral**: a job-neutral creator (**WMI `Win32_Process.Create`**,\n    │...\n 487│**input injection**:\n*488│Two granularities, both first-class. `send-keys` — raw byte stream including escape sequences (e.g. `Ctrl-C`). `send-line` — cooked line-level input flushed on newline (the common case; matches how user input reaches an agent prompt). **PTY input is single-writer (v0.13.0 P0, KNOWN-HAZARDS 7.17):** broker-side, every write to a hosted session's PTY goes through ONE per-session input-writer thread — the sole caller of the blocking `write_input`, fed by a bounded FIFO. All callers (operator keystroke...\n 489│\n 490│**scrollback**:\n 491│In-memory ring buffer per session for v1. **On-disk spillover for long sessions is an intended, tracked feature** (deferred past v1, not forgotten) — see `docs/DEFERRED.md`.\n    │...\n 562│\n*563│## Self-update\n 564│\n 565│<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->\n 566│Seamless, realtime self-updating is a day-one pillar. After a plugin performs the initial spt-core bootstrap, `spt.exe` self-updates from then on and **ripple-updates registered adapters** via each adapter's manifest update declaration. An adapter may include a `[update].message` — a plain human notice (markdown-rendered) that `spt adapter update` prints to stdout only when a new version is **actually applied** (never on a no-op). Useful for post-update operator actions, e.g. `\"Run \\`/reload-plugins\\` ...\n    │...\n 587│\n*588│**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 589│\n*590│**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 591│\n 592│**broker update frequency** — rare by design. Triggers: broker↔brain IPC contract change (versioned so a newer brain talks to an older broker — broker stays put as the brain updates), held-resource-type change, OS PTY/socket API change, or a broker bugfix. When the broker *must* update (the one case that can disturb held endpoints), it is a small well-scoped place that can use FD-passing or a rare planned endpoint-cycle — not the whole daemon.\n 593│\n    │...\n 644│\n*645│<!-- [doc->REQ-RC-CROSS-NODE-ATTACH] -->\n 646│**Remote-control vs local operation (two distinct modes — not the same as instances):**\n 647│- **Operate locally:** drive the native instance on *your* machine (its local files, its synced mind). The normal case.\n*648│- **Remote-control (Shell-like):** attach a control/view surface to an instance *running on another node* — compute + files stay remote; you are a viewport (the byte-stream terminal attach, daemon-to-daemon over Iroh). Used when you specifically want *that machine's* environment. This is effectively a Shell (a driven surface, user→agent direction), separate from the instance concept itself.\n*649│- **Owning-node resolution (client leg):** `spt rc <id>` first looks for a live session in the **local** broker table; on a miss it **resolves the owning node from the registry** (the same refuse-and-qualify resolution the message leg uses) and **dials it**, sending the **endpoint id** — the owning node is authoritative for its own session table, so it maps endpoint → its session id **server-side** (a remote operator never knows, nor guesses, the remote session id). The UX is unchanged (`spt rc` \"jus...\n 650│- **Smooth handoff (remote-control → local):** no teardown required. `git pull` brings the files; the **fresh-with-preload resume seam** preloads the other instance's latest synced context (psyche-download); the remote instance simply goes dormant. The mind follows; the files are local.\n 651│\n 652│### Pieces the Instances model requires\n    │...\n 662│\n*663│<!-- [doc->REQ-ENDPOINT-UNBOUND-ATTACH] -->\n 664│**Unbound endpoint**:\n*665│The lifecycle point between *spawn* and *bind*: an spt-hosted endpoint whose broker **session + PTY are live** but whose harness has **not yet bound** its perch (the *post-spawn seam* hasn't fired — e.g. the harness is waiting on a startup prompt). On-disk status `unbound` (spawn → `unbound`; bind → `online`; session death → `offline`). An Unbound endpoint is **attachable** (a live PTY — `spt rc` and the `endpoint run` attach reach it, so an operator can see and drive the harness, including cle...\n 666│\n 667│<!-- [doc->REQ-INST-14] -->\n 668│**resource advertisement (subnet resource registry)**:\n*669│A per-endpoint **free-text blurb** describing the services/functions the endpoint can serve — an agent **yellow-pages** for service discovery, distinct from *capability declaration* (machine-readable, which endpoint *types* a node hosts) and from *endpoint visibility* (whether it's addressable at all). **Both-authored + mutable:** config seeds a default; the agent refines its own at runtime (`spt endpoint description set …`). It is **not a separate registry** — it is a field on the endpoint record ...\n 670│\n 671│**resolution policy**:\n 672│How bare `ling` resolves when multiple instances exist. Rule: local-node instance if present → else most-recently-active → explicit override always available. **Two qualifier syntaxes**, combinable: **subnet-qualified** `home:ling` and **node-qualified** `ling@hfenduleam` (→ `home:ling@hfenduleam`). **Multi-subnet ambiguity rule:** under node-global identity (endpoint ID below), a node on several subnets may see two *distinct* endpoints that share a bare name (a `home:ling` and an unrelated `work:l...\n    │...\n 786│**subnet attachment (attached / detached)**:\n*787│Whether this node's daemon is **actively serving** a held subnet membership right now — pairing responder reachable, rendezvous meet listener rotating, registry gossip pumping. **Attached** = serving; **detached** = the membership record (seed) is held on disk but deliberately not served (the daemon neither advertises into nor connects to that subnet). Detachment is a *chosen* per-subnet state — `spt subnet detach/attach <NAME> [--save]` (shipped M8-D2; `--save` persists the startup default in daemon...\n 788│\n 789│**endpoint description** (canonical CLI term for the resource-advertisement blurb):\n 790│The per-endpoint free-text \"yellow-pages\" line (see §resource advertisement). Surface term is **description**; \"blurb\" survives only as the internal field name.\n    │...\n 871│\n*872│The day-one headed frontend is a launcher/manager UX, not just a raw `attach`. The end user cares about *reaching a target agent*, not about PTY mechanics. The frontend must:\n 873│\n 874│- **List** all running and historic endpoints.\n 875│- **Launch a historic endpoint** that isn't currently running, reusing the same manifest that originally launched it.\n*876│- **Tap into** (attach to) a running endpoint.\n 877│- **Init a new endpoint** using a known adapter (e.g. claude-code / spt-plugin).\n 878│\n*879│This is the realization of the \"guided resume\" (resume by project or agent, XMB-style filtered selection) and \"management GUI\" (per-endpoint panes: latest session output, live/project context, psyche log) sketches. `attach` is one operation under this frontend, not the whole frontend.\n 880│\n 881│**guided-resume picker (`spt resume`, no-arg)**:\n*882│The CLI sibling of the frontend's guided resume — one command that lists endpoints **grouped by locality, most-recently-used within each group**: `on-node / current-project → on-node / other-project → off-node`, mirroring the *resolution policy*'s local-first preference. Selection **chains conditionally**: a **running** instance → attach/tap-in (no adapter step — already live under one); a **non-running** endpoint → into the **adapter selector** (*adapter selection*: history head = default ...","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784195763337}}
{"type":"message","id":"2f59d50b","parentId":"219022ec","timestamp":"2026-07-16T09:56:03.346Z","message":{"role":"toolResult","toolCallId":"call_WlKkSYPQ1XlLSvbx40qyZlUz|fc_008390f508d1d09b016a58aab33780819a8df10b8af7c8bebc","toolName":"read","content":[{"type":"text","text":"//! Bounded, cancelable, poison-on-failure framed writes for one physical\n//! broker connection (UPDATE-WEDGE round-4).\n//!\n//! ## The failure class this kills\n//!\n//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard\n//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes\n//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\n//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\n//! A connected consumer that stops reading (an `rc --take` whose process\n//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n//! logical stall-evict (`stall_evict_controller`) releases only the controller\n//! ROLE — it never cancels the in-flight pipe write or closes the physical\n//! connection, so the writer (and every write queued behind the same gate)\n//! stays wedged until the peer process dies (~128 s in the field capture, on a\n//! brain restart).\n//!\n//! ## The primitive\n//!\n//! [`BrokerConn`] owns the send half outright — it never leaves this module,\n//! so an unbounded bypass write cannot compile. Every write:\n//!\n//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE\n//!    deadline stamped at write entry covers BOTH the gate-wait and the OS\n//!    write);\n//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute\n//!    deadline, then performs the blocking [`write_frame`] OUT of every lock;\n//! 3. on success, returns the half to the gate.\n//!\n//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\n//! write returning, nor on opportunistic stall-evict sampling: it poisons the\n//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\n//! completion APC makes the wedged `SleepEx` loop return with\n//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\n//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\n//! the same abort (the stuck op it waited behind is the wedge).\n//!\n//! ## Poison / retire invariant (both OSes)\n//!\n//! On deadline, write failure, or cancellation: (1) the WHOLE physical\n//! connection is poisoned, idempotently — a length-prefixed frame may have\n//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\n//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\n//! (3) the buffer of the canceled op is released only after the op reports\n//! completion (`write_frame` returns only after the completion APC — the\n//! synchronous return IS the completion handshake); (4) the retired half is\n//! parked in a graveyard slot — never dropped while the watchdog might still\n//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\n//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\n//! pipe is then severed (`DisconnectNamedPipe` — the server end forces the\n//! client off, so the old client's next access fails, a re-issued server read\n//! fails, and the `interprocess` drop-limbo flush cannot block on unread\n//! data), followed by one more `CancelIoEx` covering a read re-issued in the\n//! cancel→disconnect window.\n//!\n//! No new output queue (the bounded per-sink queues + isolated writer threads\n//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\n//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\n//! healthy connections; only the poison/retire path is new.\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n\nuse std::io;\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\nuse std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};\nuse std::thread::{self, JoinHandle};\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n\nuse interprocess::local_socket::SendHalf;\n\nuse crate::codec::write_frame;\nuse crate::frame::Envelope;\n\n/// Stable per-physical-conn identity mint — a monotonic u64 stamped at\n/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\nstatic CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n\n/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n/// startup ([`init_log_anchor`], both broker and brain entries) so every\n/// record in one process shares one base; lazily initialized on first stamp\n/// otherwise (tests, in-process brokers) — correlation needs a COMMON base,\n/// not an absolute one.\nstatic MONO_ANCHOR: OnceLock<Instant> = OnceLock::new();\n\n/// Anchor the monotonic half of [`log_stamp`] at process start — called first\n/// thing by both daemon entries, next to `scrub_identity_env`.\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\npub(crate) fn init_log_anchor() {\n    let _ = MONO_ANCHOR.set(Instant::now());\n}\n\n/// Wall-clock + monotonic stamp for daemon-stderr correlation records\n/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\n/// stderr has no per-line timestamps and broker+brain share one file — without\n/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\n/// to field incident times (wall).\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\npub(crate) fn log_stamp() -> String {\n    let wall_ms = SystemTime::now()\n        .duration_since(UNIX_EPOCH)\n        .map(|d| d.as_millis())\n        .unwrap_or(0);\n    let mono_ms = MONO_ANCHOR.get_or_init(Instant::now).elapsed().as_millis();\n    format!(\"wall_ms={wall_ms} mono_ms={mono_ms}\")\n}\n\n/// Attribution-label cap: facts accrete per role attach (a long-lived brain\n/// conn can be controller + viewer + presence subscriber over its life), and\n/// the label must stay BOUNDED — one truncation marker, then no further growth.\nconst LABEL_CAP: usize = 240;\n\n/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\n#[derive(Debug, PartialEq, Eq)]\npub(crate) enum ConnWrite {\n    /// The frame was fully written.\n    Done,\n    /// The pre-write gate closure declined (e.g. a superseded controller\n    /// writer's epoch check) — nothing was written; the conn stays healthy.\n    Superseded,\n}\n\n/// The serialized write gate. `half: Some` = free; `None` = a write is in\n/// flight (checked out) or the conn has retired (then `poisoned` is set).\nstruct Gate {\n    half: Option<SendHalf>,\n}\n\n/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\nstruct Dog {\n    /// `Some((op_id, deadline))` while a write is in flight.\n    inflight: Option<(u64, Instant)>,\n    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n    shutdown: bool,\n}\n\n/// The state shared with the watchdog thread (no `Arc<BrokerConn>` cycle —\n/// the watchdog must not keep the conn alive).\nstruct Inner {\n    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n    id: u64,\n    /// Accreted attribution facts (subscriber role, endpoint/session where\n    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n    label: Mutex<String>,\n    /// Latch: the once-per-conn `write-start` lifecycle record fired.\n    first_write_logged: AtomicBool,\n    gate: Mutex<Gate>,\n    gate_cv: Condvar,\n    dog: Mutex<Dog>,\n    dog_cv: Condvar,\n    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\n    /// conn refuses every future write and is never re-gated.\n    poisoned: AtomicBool,\n    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\n    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\n    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\n    /// count the wedge (the W2 stall-evict tally) without conflating it with\n    /// an ordinary disconnect.\n    timed_out: AtomicBool,\n    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\n    severed: AtomicBool,\n    /// A retired (poisoned) half parks here so its OS handle stays alive\n    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\n    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\n    graveyard: Mutex<Option<SendHalf>>,\n    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\n    op_seq: AtomicU64,\n    /// The absolute per-write bound (gate-wait + OS write completion).\n    bound: Duration,\n    /// The raw OS handle/fd of the underlying stream, captured at\n    /// construction for the out-of-band abort. The recv and send halves share\n    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\n    /// aborting this handle covers a parked read too. Valid for the whole\n    /// conn lifetime: the half lives in `gate`, a writer's stack, or\n    /// `graveyard` until drop.\n    #[cfg(windows)]\n    raw: isize,\n    #[cfg(unix)]\n…\n}\n…\nimpl Inner {\n    /// The attribution snapshot every lifecycle record carries: stable conn id,\n    /// accreted role/endpoint/session facts, wall + monotonic stamps.\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    fn attribution(&self) -> String {\n        let label = recover(&self.label);\n        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\n        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\n    }\n\n    /// Render the once-per-conn retirement record — the F-039 token split\n    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n    /// already-dead counterpart, the offline-peer dial churn) emits the\n    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n    /// SAME attribution fields — quieter class, never thinner (the retired\n    /// class is what discriminates fresh-carrier churn from interleave\n    /// artifact in the field). Behavior is identical either way: poison,\n    /// abort, retire, never reuse.\n    // [impl->REQ-CONN-POISON-DIAL-SCOPE]\n    fn render_retirement(&self, timed_out: bool, cause: Option<&io::Error>) -> String {\n        if timed_out {\n            format!(\n                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\n                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\n                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\n                self.attribution(),\n                self.bound.as_millis()\n            )\n        } else {\n            let cause = match cause {\n                Some(e) => format!(\"{:?}: {e}\", e.kind()),\n                None => \"write failed\".to_string(),\n            };\n            format!(\n                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\n                 broker conn poisoned + retired, never reused (same retire behavior \\\n                 as POISONED — only the failure class differs) \\\n                 [REQ-CONN-POISON-DIAL-SCOPE]\",\n                self.attribution()\n            )\n        }\n    }\n\n    /// One BOUNDED per-conn lifecycle record (leg d, doyle-confirmed\n    /// UNCONDITIONAL): `write-start` (first write only), `transport-close`\n    /// (drop), `writer-exit` / `*-attach` / `*-replaced` (emitted by the\n    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\n    /// timeout/cancel leg is the retirement record itself.\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    fn lifecycle(&self, event: &str, extra: &str) {\n        let sep = if extra.is_empty() { \"\" } else { \" \" };\n        eprintln!(\n            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\n            self.attribution()\n        );\n    }\n\n    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\n    /// the halves share one OS object) and latch the poison. Idempotent and\n    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\n    /// no-op, and re-cancel closes the race where a write was issued a beat\n    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\n    /// `cause` (the organic caller's write error) rides the retirement record\n    /// when the retirement is NOT deadline-driven.\n    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\n        if !self.poisoned.swap(true, Ordering::AcqRel) {\n            // First poison of this conn: loud, once. The token is class-split\n            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n            eprintln!(\n                \"{}\",\n                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n            );\n        }\n        #[cfg(windows)]\n        // SAFETY: scalar kernel32 call on a handle whose owning half is kept\n        // alive until Drop (gate / writer stack / graveyard — see `raw`).\n        unsafe {\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\n        }\n        #[cfg(unix)]\n        // SAFETY: scalar shutdown(2) on an fd alive until Drop. SHUT_RDWR\n        // fails the blocked write (EPIPE), EOFs the parked read, and fails\n        // every future op — on Unix this one call IS the whole teardown.\n        unsafe {\n            let _ = libc::shutdown(self.raw, libc::SHUT_RDWR);\n        }\n        // Wake gate-waiters so they observe the poison instead of sleeping\n        // out their full deadline, and the watchdog so it re-evaluates.\n        self.gate_cv.notify_all();\n        self.dog_cv.notify_all();\n    }\n\n    /// Sever the physical connection (exactly once), AFTER the canceled op\n    /// reported completion: on Windows `DisconnectNamedPipe` forces the\n    /// client off (its next access fails), makes a re-issued server-side\n    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\n    /// new read in after the cancel), and defuses the drop-limbo flush; the\n    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\n    /// `shutdown` in [`poison_and_cancel`] already did all of this.\n    // On Unix the cfg(windows) teardown strips out and the idempotence\n    // guard's early `return` becomes the function tail — needless_return\n    // there, load-bearing here. Restructuring (e.g. `if !swapped { … }`)\n    // would leave Unix an empty if-body (`clippy::needless_if`) instead.\n    #[cfg_attr(unix, allow(clippy::needless_return))]\n    fn sever(&self) {\n        if self.severed.swap(true, Ordering::AcqRel) {\n            return;\n        }\n        #[cfg(windows)]\n        // SAFETY: as in poison_and_cancel — handle outlives the watchdog.\n        unsafe {\n            let _ = win::DisconnectNamedPipe(self.raw);\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\n        }\n    }\n\n    /// The watchdog body: park until a write is in flight, then sleep to its\n    /// absolute deadline. If the op is still in flight at the deadline,\n    /// poison + cancel, re-canceling periodically until the op reports\n    /// completion (the writer clears `inflight` the instant `write_frame`\n    /// returns — that return IS the buffer-release handshake), then sever.\n    fn watchdog(&self) {\n        let mut d = recover(&self.dog);\n        loop {\n            // Park until armed (or shutdown).\n            let (op, deadline) = loop {\n                if d.shutdown {\n                    return;\n                }\n                if let Some(armed) = d.inflight {\n                    break armed;\n                }\n                d = self\n                    .dog_cv\n                    .wait(d)\n                    .unwrap_or_else(|p| p.into_inner());\n            };\n            // Sleep toward the deadline while THIS op stays in flight.\n            let fired = loop {\n                if d.shutdown {\n                    return;\n                }\n                match d.inflight {\n                    Some((cur, _)) if cur == op => {}\n                    // Completed (or a newer op armed) before the deadline.\n                    _ => break false,\n                }\n                let now = Instant::now();\n                if now >= deadline {\n                    break true;\n                }\n                let (g, _) = self\n                    .dog_cv\n                    .wait_timeout(d, deadline - now)\n                    .unwrap_or_else(|p| p.into_inner());\n                d = g;\n            };\n            if !fired {\n                continue;\n            }\n            // Deadline: abort out of band, then wait for the canceled op to\n            // report completion before severing (re-cancel each slice — a\n            // cancel that raced the WriteFileEx issuance reaches nothing, so\n            // one shot is not enough). Latch timed_out FIRST so the aborted\n            // writer observes its failure as a DEADLINE retirement (wedge),\n            // not an organic conn death.\n            drop(d);\n            self.timed_out.store(true, Ordering::Release);\n            self.poison_and_cancel(None);\n            d = recover(&self.dog);\n            while matches!(d.inflight, Some((cur, _)) if cur == op) && !d.shutdown {\n                let (g, _) = self\n                    .dog_cv\n                    .wait_timeout(d, Duration::from_millis(100))\n                    .unwrap_or_else(|p| p.into_inner());\n                d = g;\n                drop(d);\n                self.poison_and_cancel(None);\n                d = recover(&self.dog);\n            }\n            if d.shutdown {\n                return;\n            }\n            drop(d);\n            self.sever();\n            d = recover(&self.dog);\n        }\n    }\n}\n\n/// The shared, serialized, BOUNDED send side of one physical broker\n/// connection — what [`crate::broker::SharedSend`] now aliases. Owns the\n/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\npub(crate) struct BrokerConn {\n    inner: Arc<Inner>,\n    /// Joined on drop — after `shutdown` is raised — so the raw handle the\n    /// watchdog aborts through strictly outlives every raw use.\n    dog: Option<JoinHandle<()>>,\n}\n\nimpl BrokerConn {\n    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\n    /// deadline (gate-wait + OS write completion) — the broker passes its\n    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\n        let raw = raw_of(&half);\n        let inner = Arc::new(Inner {\n            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n            label: Mutex::new(String::new()),\n            first_write_logged: AtomicBool::new(false),\n            gate: Mutex::new(Gate { half: Some(half) }),\n            gate_cv: Condvar::new(),\n            dog: Mutex::new(Dog {\n                inflight: None,\n                shutdown: false,\n            }),\n            dog_cv: Condvar::new(),\n            poisoned: AtomicBool::new(false),\n            timed_out: AtomicBool::new(false),\n            severed: AtomicBool::new(false),\n            graveyard: Mutex::new(None),\n            op_seq: AtomicU64::new(0),\n            bound,\n            raw,\n        });\n        let dog_inner = Arc::clone(&inner);\n        let dog = thread::Builder::new()\n            .name(\"conn-watchdog\".into())\n            .spawn(move || dog_inner.watchdog())\n            .ok();\n        BrokerConn { inner, dog }\n    }\n\n    /// The stable per-physical-conn id — what lifecycle records key on\n    /// (`Arc::ptr_eq` identity does not survive a log line).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn id(&self) -> u64 {\n        self.inner.id\n    }\n\n    /// Accrete an attribution fact onto this conn's label (subscriber role,\n    /// endpoint/session where known — e.g. `role=brain`,\n    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn describe(&self, fact: &str) {\n        let mut l = recover(&self.inner.label);\n        if l.ends_with('…') {\n            return;\n        }\n        if l.len() + fact.len() + 1 > LABEL_CAP {\n            l.push('…');\n            return;\n        }\n        if !l.is_empty() {\n            l.push(' ');\n        }\n        l.push_str(fact);\n    }\n\n    /// Emit a bounded per-conn lifecycle record through this conn's shared\n    /// attribution format — the broker's sink machinery uses it for\n    /// `writer-exit`, attach/replace, and detach events (leg d).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\n        self.inner.lifecycle(event, extra);\n    }\n\n    /// Bounded framed write. See [`Self::write_gated`].\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n        self.write_gated(env, || true)\n    }\n\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\n    /// serialized gate is acquired and BEFORE any byte is written — the same\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\n    /// superseded controller writer still bails atomically with respect to\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\n    pub(crate) fn write_gated(\n        &self,\n        env: &Envelope,\n        pre: impl FnOnce() -> bool,\n    ) -> io::Result<ConnWrite> {\n        let inner = &*self.inner;\n        let deadline = Instant::now() + inner.bound;\n\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\n        let mut g = recover(&inner.gate);\n        loop {\n            if inner.poisoned.load(Ordering::Acquire) {\n                return Err(poisoned_err());\n            }\n            if g.half.is_some() {\n                break;\n            }\n            let now = Instant::now();\n            if now >= deadline {\n                // The write we queued behind is stuck past OUR whole bound:\n                // that op is the wedge. Abort it out of band and fail — the\n                // watchdog's own deadline (armed at that op's entry) is also\n                // in flight; both paths converge on the same idempotent\n                // poison/cancel.\n                drop(g);\n                inner.timed_out.store(true, Ordering::Release);\n                inner.poison_and_cancel(None);\n                return Err(timeout_err());\n            }\n            let (g2, _) = inner\n                .gate_cv\n                .wait_timeout(g, deadline - now)\n                .unwrap_or_else(|p| p.into_inner());\n            g = g2;\n        }\n\n        if !pre() {\n            return Ok(ConnWrite::Superseded);\n        }\n\n        // Once-per-conn `write-start` lifecycle record (leg d): marks the conn's\n        // first committed write, so a conn that later retires can be correlated\n        // to when it went active (fresh-carrier churn shows as open→start→retire\n        // triplets with fresh ids).\n        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\n            inner.lifecycle(\"write-start\", \"\");\n        }\n\n        // ── Check out the half + arm the watchdog, then write OUT of locks. ──\n        let mut half = g.half.take().expect(\"gate half present at checkout\");\n        let op = inner.op_seq.fetch_add(1, Ordering::Relaxed) + 1;\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = Some((op, deadline));\n            inner.dog_cv.notify_all();\n        }\n        drop(g);\n\n        let res = write_frame(&mut half, env);\n\n        // Completion handshake: `write_frame` has returned, so the OS op —\n        // canceled or not — reported completion and the frame buffer is\n        // released. Clear the arm BEFORE deciding health, so the watchdog\n        // never severs while the op is formally in flight.\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = None;\n            inner.dog_cv.notify_all();\n        }\n\n        let mut g = recover(&inner.gate);\n        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\n            g.half = Some(half);\n            drop(g);\n            inner.gate_cv.notify_all();\n            return Ok(ConnWrite::Done);\n        }\n\n        // ── Retire: poison the whole conn, abort the read side so\n        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\n        // graveyard (its handle must outlive any late raw abort; never\n        // re-gated, never reused — a timed-out length-prefixed frame may be\n        // partially written). ──\n        drop(g);\n        inner.poison_and_cancel(res.as_ref().err());\n        inner.sever();\n        *recover(&inner.graveyard) = Some(half);\n        inner.gate_cv.notify_all();\n        // A DEADLINE retirement (the watchdog aborted OUR op, or the conn was\n        // already deadline-poisoned) surfaces as TimedOut — the caller can\n        // count the wedge (W2 stall-evict tally). An organic I/O failure\n        // (peer death) keeps its own error.\n        if inner.timed_out.load(Ordering::Acquire) {\n            return Err(timeout_err());\n        }\n        Err(match res {\n            Err(e) => e,\n            Ok(()) => poisoned_err(),\n        })\n    }\n}\n\n/// TEST-ONLY determinism lever: holds the serialized write gate (the half is\n/// checked out) so every writer parks AT THE GATE until the pin drops — the\n/// round-4 analog of pinning the old `Mutex<SendHalf>` across a registration\n/// window (the P1c epoch-gate keystone unit relies on it).\n#[cfg(test)]\npub(crate) struct TestGatePin {\n    inner: Arc<Inner>,\n    half: Option<SendHalf>,\n}\n\n#[cfg(test)]\nimpl Drop for TestGatePin {\n    fn drop(&mut self) {\n        let mut g = recover(&self.inner.gate);\n        g.half = self.half.take();\n        drop(g);\n        self.inner.gate_cv.notify_all();\n    }\n}\n\n#[cfg(test)]\nimpl BrokerConn {\n    /// Check the half out of the gate without arming the watchdog — writers\n    /// queue behind the gate (bounded by their own deadlines) until the\n    /// returned pin drops. Test-only; never a production path.\n    pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {\n        let mut g = recover(&self.inner.gate);\n        let half = g.half.take().expect(\"pin: gate half present\");\n        TestGatePin {\n            inner: Arc::clone(&self.inner),\n            half: Some(half),\n        }\n    }\n}\n\nimpl Drop for BrokerConn {\n    fn drop(&mut self) {\n        // Once-per-conn `transport-close` lifecycle record (leg d): the handle\n        // is about to close — the terminal record of this conn id's life, with\n        // how it ended (organic EOF vs poisoned vs deadline-poisoned).\n        self.inner.lifecycle(\n            \"transport-close\",\n            &format!(\n                \"poisoned={} timed_out={}\",\n                self.inner.poisoned.load(Ordering::Acquire),\n                self.inner.timed_out.load(Ordering::Acquire)\n            ),\n        );\n        {\n            let mut d = recover(&self.inner.dog);\n            d.shutdown = true;\n            self.inner.dog_cv.notify_all();\n        }\n        if let Some(dog) = self.dog.take() {\n            let _ = dog.join();\n        }\n        // Halves (gate or graveyard) drop AFTER the watchdog is gone — no raw\n        // handle use can race the CloseHandle/limbo path.\n    }\n}\n\nfn poisoned_err() -> io::Error {\n    io::Error::new(\n        io::ErrorKind::BrokenPipe,\n        \"broker conn poisoned (bounded write retired the physical connection)\",\n    )\n}\n\nfn timeout_err() -> io::Error {\n    io::Error::new(\n        io::ErrorKind::TimedOut,\n        \"broker conn write exceeded its bound (conn poisoned + aborted)\",\n    )\n}\n\n#[cfg(test)]\nmod tests {\n…\n}"}],"details":{"displayContent":{"text":"//! Bounded, cancelable, poison-on-failure framed writes for one physical\n//! broker connection (UPDATE-WEDGE round-4).\n//!\n//! ## The failure class this kills\n//!\n//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard\n//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes\n//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\n//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\n//! A connected consumer that stops reading (an `rc --take` whose process\n//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n//! logical stall-evict (`stall_evict_controller`) releases only the controller\n//! ROLE — it never cancels the in-flight pipe write or closes the physical\n//! connection, so the writer (and every write queued behind the same gate)\n//! stays wedged until the peer process dies (~128 s in the field capture, on a\n//! brain restart).\n//!\n//! ## The primitive\n//!\n//! [`BrokerConn`] owns the send half outright — it never leaves this module,\n//! so an unbounded bypass write cannot compile. Every write:\n//!\n//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE\n//!    deadline stamped at write entry covers BOTH the gate-wait and the OS\n//!    write);\n//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute\n//!    deadline, then performs the blocking [`write_frame`] OUT of every lock;\n//! 3. on success, returns the half to the gate.\n//!\n//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\n//! write returning, nor on opportunistic stall-evict sampling: it poisons the\n//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\n//! completion APC makes the wedged `SleepEx` loop return with\n//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\n//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\n//! the same abort (the stuck op it waited behind is the wedge).\n//!\n//! ## Poison / retire invariant (both OSes)\n//!\n//! On deadline, write failure, or cancellation: (1) the WHOLE physical\n//! connection is poisoned, idempotently — a length-prefixed frame may have\n//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\n//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\n//! (3) the buffer of the canceled op is released only after the op reports\n//! completion (`write_frame` returns only after the completion APC — the\n//! synchronous return IS the completion handshake); (4) the retired half is\n//! parked in a graveyard slot — never dropped while the watchdog might still\n//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\n//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\n//! pipe is then severed (`DisconnectNamedPipe` — the server end forces the\n//! client off, so the old client's next access fails, a re-issued server read\n//! fails, and the `interprocess` drop-limbo flush cannot block on unread\n//! data), followed by one more `CancelIoEx` covering a read re-issued in the\n//! cancel→disconnect window.\n//!\n//! No new output queue (the bounded per-sink queues + isolated writer threads\n//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\n//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\n//! healthy connections; only the poison/retire path is new.\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n\nuse std::io;\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\nuse std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};\nuse std::thread::{self, JoinHandle};\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n\nuse interprocess::local_socket::SendHalf;\n\nuse crate::codec::write_frame;\nuse crate::frame::Envelope;\n\n/// Stable per-physical-conn identity mint — a monotonic u64 stamped at\n/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\nstatic CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n\n/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n/// startup ([`init_log_anchor`], both broker and brain entries) so every\n/// record in one process shares one base; lazily initialized on first stamp\n/// otherwise (tests, in-process brokers) — correlation needs a COMMON base,\n/// not an absolute one.\nstatic MONO_ANCHOR: OnceLock<Instant> = OnceLock::new();\n\n/// Anchor the monotonic half of [`log_stamp`] at process start — called first\n/// thing by both daemon entries, next to `scrub_identity_env`.\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\npub(crate) fn init_log_anchor() {\n    let _ = MONO_ANCHOR.set(Instant::now());\n}\n\n/// Wall-clock + monotonic stamp for daemon-stderr correlation records\n/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\n/// stderr has no per-line timestamps and broker+brain share one file — without\n/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\n/// to field incident times (wall).\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\npub(crate) fn log_stamp() -> String {\n    let wall_ms = SystemTime::now()\n        .duration_since(UNIX_EPOCH)\n        .map(|d| d.as_millis())\n        .unwrap_or(0);\n    let mono_ms = MONO_ANCHOR.get_or_init(Instant::now).elapsed().as_millis();\n    format!(\"wall_ms={wall_ms} mono_ms={mono_ms}\")\n}\n\n/// Attribution-label cap: facts accrete per role attach (a long-lived brain\n/// conn can be controller + viewer + presence subscriber over its life), and\n/// the label must stay BOUNDED — one truncation marker, then no further growth.\nconst LABEL_CAP: usize = 240;\n\n/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\n#[derive(Debug, PartialEq, Eq)]\npub(crate) enum ConnWrite {\n    /// The frame was fully written.\n    Done,\n    /// The pre-write gate closure declined (e.g. a superseded controller\n    /// writer's epoch check) — nothing was written; the conn stays healthy.\n    Superseded,\n}\n\n/// The serialized write gate. `half: Some` = free; `None` = a write is in\n/// flight (checked out) or the conn has retired (then `poisoned` is set).\nstruct Gate {\n    half: Option<SendHalf>,\n}\n\n/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\nstruct Dog {\n    /// `Some((op_id, deadline))` while a write is in flight.\n    inflight: Option<(u64, Instant)>,\n    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n    shutdown: bool,\n}\n\n/// The state shared with the watchdog thread (no `Arc<BrokerConn>` cycle —\n/// the watchdog must not keep the conn alive).\nstruct Inner {\n    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n    id: u64,\n    /// Accreted attribution facts (subscriber role, endpoint/session where\n    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n    label: Mutex<String>,\n    /// Latch: the once-per-conn `write-start` lifecycle record fired.\n    first_write_logged: AtomicBool,\n    gate: Mutex<Gate>,\n    gate_cv: Condvar,\n    dog: Mutex<Dog>,\n    dog_cv: Condvar,\n    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\n    /// conn refuses every future write and is never re-gated.\n    poisoned: AtomicBool,\n    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\n    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\n    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\n    /// count the wedge (the W2 stall-evict tally) without conflating it with\n    /// an ordinary disconnect.\n    timed_out: AtomicBool,\n    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\n    severed: AtomicBool,\n    /// A retired (poisoned) half parks here so its OS handle stays alive\n    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\n    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\n    graveyard: Mutex<Option<SendHalf>>,\n    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\n    op_seq: AtomicU64,\n    /// The absolute per-write bound (gate-wait + OS write completion).\n    bound: Duration,\n    /// The raw OS handle/fd of the underlying stream, captured at\n    /// construction for the out-of-band abort. The recv and send halves share\n    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\n    /// aborting this handle covers a parked read too. Valid for the whole\n    /// conn lifetime: the half lives in `gate`, a writer's stack, or\n    /// `graveyard` until drop.\n    #[cfg(windows)]\n    raw: isize,\n    #[cfg(unix)]\n…\n}\n…\nimpl Inner {\n    /// The attribution snapshot every lifecycle record carries: stable conn id,\n    /// accreted role/endpoint/session facts, wall + monotonic stamps.\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    fn attribution(&self) -> String {\n        let label = recover(&self.label);\n        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\n        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\n    }\n\n    /// Render the once-per-conn retirement record — the F-039 token split\n    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n    /// already-dead counterpart, the offline-peer dial churn) emits the\n    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n    /// SAME attribution fields — quieter class, never thinner (the retired\n    /// class is what discriminates fresh-carrier churn from interleave\n    /// artifact in the field). Behavior is identical either way: poison,\n    /// abort, retire, never reuse.\n    // [impl->REQ-CONN-POISON-DIAL-SCOPE]\n    fn render_retirement(&self, timed_out: bool, cause: Option<&io::Error>) -> String {\n        if timed_out {\n            format!(\n                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\n                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\n                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\n                self.attribution(),\n                self.bound.as_millis()\n            )\n        } else {\n            let cause = match cause {\n                Some(e) => format!(\"{:?}: {e}\", e.kind()),\n                None => \"write failed\".to_string(),\n            };\n            format!(\n                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\n                 broker conn poisoned + retired, never reused (same retire behavior \\\n                 as POISONED — only the failure class differs) \\\n                 [REQ-CONN-POISON-DIAL-SCOPE]\",\n                self.attribution()\n            )\n        }\n    }\n\n    /// One BOUNDED per-conn lifecycle record (leg d, doyle-confirmed\n    /// UNCONDITIONAL): `write-start` (first write only), `transport-close`\n    /// (drop), `writer-exit` / `*-attach` / `*-replaced` (emitted by the\n    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\n    /// timeout/cancel leg is the retirement record itself.\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    fn lifecycle(&self, event: &str, extra: &str) {\n        let sep = if extra.is_empty() { \"\" } else { \" \" };\n        eprintln!(\n            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\n            self.attribution()\n        );\n    }\n\n    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\n    /// the halves share one OS object) and latch the poison. Idempotent and\n    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\n    /// no-op, and re-cancel closes the race where a write was issued a beat\n    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\n    /// `cause` (the organic caller's write error) rides the retirement record\n    /// when the retirement is NOT deadline-driven.\n    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\n        if !self.poisoned.swap(true, Ordering::AcqRel) {\n            // First poison of this conn: loud, once. The token is class-split\n            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n            eprintln!(\n                \"{}\",\n                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n            );\n        }\n        #[cfg(windows)]\n        // SAFETY: scalar kernel32 call on a handle whose owning half is kept\n        // alive until Drop (gate / writer stack / graveyard — see `raw`).\n        unsafe {\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\n        }\n        #[cfg(unix)]\n        // SAFETY: scalar shutdown(2) on an fd alive until Drop. SHUT_RDWR\n        // fails the blocked write (EPIPE), EOFs the parked read, and fails\n        // every future op — on Unix this one call IS the whole teardown.\n        unsafe {\n            let _ = libc::shutdown(self.raw, libc::SHUT_RDWR);\n        }\n        // Wake gate-waiters so they observe the poison instead of sleeping\n        // out their full deadline, and the watchdog so it re-evaluates.\n        self.gate_cv.notify_all();\n        self.dog_cv.notify_all();\n    }\n\n    /// Sever the physical connection (exactly once), AFTER the canceled op\n    /// reported completion: on Windows `DisconnectNamedPipe` forces the\n    /// client off (its next access fails), makes a re-issued server-side\n    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\n    /// new read in after the cancel), and defuses the drop-limbo flush; the\n    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\n    /// `shutdown` in [`poison_and_cancel`] already did all of this.\n    // On Unix the cfg(windows) teardown strips out and the idempotence\n    // guard's early `return` becomes the function tail — needless_return\n    // there, load-bearing here. Restructuring (e.g. `if !swapped { … }`)\n    // would leave Unix an empty if-body (`clippy::needless_if`) instead.\n    #[cfg_attr(unix, allow(clippy::needless_return))]\n    fn sever(&self) {\n        if self.severed.swap(true, Ordering::AcqRel) {\n            return;\n        }\n        #[cfg(windows)]\n        // SAFETY: as in poison_and_cancel — handle outlives the watchdog.\n        unsafe {\n            let _ = win::DisconnectNamedPipe(self.raw);\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\n        }\n    }\n\n    /// The watchdog body: park until a write is in flight, then sleep to its\n    /// absolute deadline. If the op is still in flight at the deadline,\n    /// poison + cancel, re-canceling periodically until the op reports\n    /// completion (the writer clears `inflight` the instant `write_frame`\n    /// returns — that return IS the buffer-release handshake), then sever.\n    fn watchdog(&self) {\n        let mut d = recover(&self.dog);\n        loop {\n            // Park until armed (or shutdown).\n            let (op, deadline) = loop {\n                if d.shutdown {\n                    return;\n                }\n                if let Some(armed) = d.inflight {\n                    break armed;\n                }\n                d = self\n                    .dog_cv\n                    .wait(d)\n                    .unwrap_or_else(|p| p.into_inner());\n            };\n            // Sleep toward the deadline while THIS op stays in flight.\n            let fired = loop {\n                if d.shutdown {\n                    return;\n                }\n                match d.inflight {\n                    Some((cur, _)) if cur == op => {}\n                    // Completed (or a newer op armed) before the deadline.\n                    _ => break false,\n                }\n                let now = Instant::now();\n                if now >= deadline {\n                    break true;\n                }\n                let (g, _) = self\n                    .dog_cv\n                    .wait_timeout(d, deadline - now)\n                    .unwrap_or_else(|p| p.into_inner());\n                d = g;\n            };\n            if !fired {\n                continue;\n            }\n            // Deadline: abort out of band, then wait for the canceled op to\n            // report completion before severing (re-cancel each slice — a\n            // cancel that raced the WriteFileEx issuance reaches nothing, so\n            // one shot is not enough). Latch timed_out FIRST so the aborted\n            // writer observes its failure as a DEADLINE retirement (wedge),\n            // not an organic conn death.\n            drop(d);\n            self.timed_out.store(true, Ordering::Release);\n            self.poison_and_cancel(None);\n            d = recover(&self.dog);\n            while matches!(d.inflight, Some((cur, _)) if cur == op) && !d.shutdown {\n                let (g, _) = self\n                    .dog_cv\n                    .wait_timeout(d, Duration::from_millis(100))\n                    .unwrap_or_else(|p| p.into_inner());\n                d = g;\n                drop(d);\n                self.poison_and_cancel(None);\n                d = recover(&self.dog);\n            }\n            if d.shutdown {\n                return;\n            }\n            drop(d);\n            self.sever();\n            d = recover(&self.dog);\n        }\n    }\n}\n\n/// The shared, serialized, BOUNDED send side of one physical broker\n/// connection — what [`crate::broker::SharedSend`] now aliases. Owns the\n/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\npub(crate) struct BrokerConn {\n    inner: Arc<Inner>,\n    /// Joined on drop — after `shutdown` is raised — so the raw handle the\n    /// watchdog aborts through strictly outlives every raw use.\n    dog: Option<JoinHandle<()>>,\n}\n\nimpl BrokerConn {\n    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\n    /// deadline (gate-wait + OS write completion) — the broker passes its\n    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\n        let raw = raw_of(&half);\n        let inner = Arc::new(Inner {\n            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n            label: Mutex::new(String::new()),\n            first_write_logged: AtomicBool::new(false),\n            gate: Mutex::new(Gate { half: Some(half) }),\n            gate_cv: Condvar::new(),\n            dog: Mutex::new(Dog {\n                inflight: None,\n                shutdown: false,\n            }),\n            dog_cv: Condvar::new(),\n            poisoned: AtomicBool::new(false),\n            timed_out: AtomicBool::new(false),\n            severed: AtomicBool::new(false),\n            graveyard: Mutex::new(None),\n            op_seq: AtomicU64::new(0),\n            bound,\n            raw,\n        });\n        let dog_inner = Arc::clone(&inner);\n        let dog = thread::Builder::new()\n            .name(\"conn-watchdog\".into())\n            .spawn(move || dog_inner.watchdog())\n            .ok();\n        BrokerConn { inner, dog }\n    }\n\n    /// The stable per-physical-conn id — what lifecycle records key on\n    /// (`Arc::ptr_eq` identity does not survive a log line).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn id(&self) -> u64 {\n        self.inner.id\n    }\n\n    /// Accrete an attribution fact onto this conn's label (subscriber role,\n    /// endpoint/session where known — e.g. `role=brain`,\n    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn describe(&self, fact: &str) {\n        let mut l = recover(&self.inner.label);\n        if l.ends_with('…') {\n            return;\n        }\n        if l.len() + fact.len() + 1 > LABEL_CAP {\n            l.push('…');\n            return;\n        }\n        if !l.is_empty() {\n            l.push(' ');\n        }\n        l.push_str(fact);\n    }\n\n    /// Emit a bounded per-conn lifecycle record through this conn's shared\n    /// attribution format — the broker's sink machinery uses it for\n    /// `writer-exit`, attach/replace, and detach events (leg d).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\n        self.inner.lifecycle(event, extra);\n    }\n\n    /// Bounded framed write. See [`Self::write_gated`].\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n        self.write_gated(env, || true)\n    }\n\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\n    /// serialized gate is acquired and BEFORE any byte is written — the same\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\n    /// superseded controller writer still bails atomically with respect to\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\n    pub(crate) fn write_gated(\n        &self,\n        env: &Envelope,\n        pre: impl FnOnce() -> bool,\n    ) -> io::Result<ConnWrite> {\n        let inner = &*self.inner;\n        let deadline = Instant::now() + inner.bound;\n\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\n        let mut g = recover(&inner.gate);\n        loop {\n            if inner.poisoned.load(Ordering::Acquire) {\n                return Err(poisoned_err());\n            }\n            if g.half.is_some() {\n                break;\n            }\n            let now = Instant::now();\n            if now >= deadline {\n                // The write we queued behind is stuck past OUR whole bound:\n                // that op is the wedge. Abort it out of band and fail — the\n                // watchdog's own deadline (armed at that op's entry) is also\n                // in flight; both paths converge on the same idempotent\n                // poison/cancel.\n                drop(g);\n                inner.timed_out.store(true, Ordering::Release);\n                inner.poison_and_cancel(None);\n                return Err(timeout_err());\n            }\n            let (g2, _) = inner\n                .gate_cv\n                .wait_timeout(g, deadline - now)\n                .unwrap_or_else(|p| p.into_inner());\n            g = g2;\n        }\n\n        if !pre() {\n            return Ok(ConnWrite::Superseded);\n        }\n\n        // Once-per-conn `write-start` lifecycle record (leg d): marks the conn's\n        // first committed write, so a conn that later retires can be correlated\n        // to when it went active (fresh-carrier churn shows as open→start→retire\n        // triplets with fresh ids).\n        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\n            inner.lifecycle(\"write-start\", \"\");\n        }\n\n        // ── Check out the half + arm the watchdog, then write OUT of locks. ──\n        let mut half = g.half.take().expect(\"gate half present at checkout\");\n        let op = inner.op_seq.fetch_add(1, Ordering::Relaxed) + 1;\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = Some((op, deadline));\n            inner.dog_cv.notify_all();\n        }\n        drop(g);\n\n        let res = write_frame(&mut half, env);\n\n        // Completion handshake: `write_frame` has returned, so the OS op —\n        // canceled or not — reported completion and the frame buffer is\n        // released. Clear the arm BEFORE deciding health, so the watchdog\n        // never severs while the op is formally in flight.\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = None;\n            inner.dog_cv.notify_all();\n        }\n\n        let mut g = recover(&inner.gate);\n        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\n            g.half = Some(half);\n            drop(g);\n            inner.gate_cv.notify_all();\n            return Ok(ConnWrite::Done);\n        }\n\n        // ── Retire: poison the whole conn, abort the read side so\n        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\n        // graveyard (its handle must outlive any late raw abort; never\n        // re-gated, never reused — a timed-out length-prefixed frame may be\n        // partially written). ──\n        drop(g);\n        inner.poison_and_cancel(res.as_ref().err());\n        inner.sever();\n        *recover(&inner.graveyard) = Some(half);\n        inner.gate_cv.notify_all();\n        // A DEADLINE retirement (the watchdog aborted OUR op, or the conn was\n        // already deadline-poisoned) surfaces as TimedOut — the caller can\n        // count the wedge (W2 stall-evict tally). An organic I/O failure\n        // (peer death) keeps its own error.\n        if inner.timed_out.load(Ordering::Acquire) {\n            return Err(timeout_err());\n        }\n        Err(match res {\n            Err(e) => e,\n            Ok(()) => poisoned_err(),\n        })\n    }\n}\n\n/// TEST-ONLY determinism lever: holds the serialized write gate (the half is\n/// checked out) so every writer parks AT THE GATE until the pin drops — the\n/// round-4 analog of pinning the old `Mutex<SendHalf>` across a registration\n/// window (the P1c epoch-gate keystone unit relies on it).\n#[cfg(test)]\npub(crate) struct TestGatePin {\n    inner: Arc<Inner>,\n    half: Option<SendHalf>,\n}\n\n#[cfg(test)]\nimpl Drop for TestGatePin {\n    fn drop(&mut self) {\n        let mut g = recover(&self.inner.gate);\n        g.half = self.half.take();\n        drop(g);\n        self.inner.gate_cv.notify_all();\n    }\n}\n\n#[cfg(test)]\nimpl BrokerConn {\n    /// Check the half out of the gate without arming the watchdog — writers\n    /// queue behind the gate (bounded by their own deadlines) until the\n    /// returned pin drops. Test-only; never a production path.\n    pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {\n        let mut g = recover(&self.inner.gate);\n        let half = g.half.take().expect(\"pin: gate half present\");\n        TestGatePin {\n            inner: Arc::clone(&self.inner),\n            half: Some(half),\n        }\n    }\n}\n\nimpl Drop for BrokerConn {\n    fn drop(&mut self) {\n        // Once-per-conn `transport-close` lifecycle record (leg d): the handle\n        // is about to close — the terminal record of this conn id's life, with\n        // how it ended (organic EOF vs poisoned vs deadline-poisoned).\n        self.inner.lifecycle(\n            \"transport-close\",\n            &format!(\n                \"poisoned={} timed_out={}\",\n                self.inner.poisoned.load(Ordering::Acquire),\n                self.inner.timed_out.load(Ordering::Acquire)\n            ),\n        );\n        {\n            let mut d = recover(&self.inner.dog);\n            d.shutdown = true;\n            self.inner.dog_cv.notify_all();\n        }\n        if let Some(dog) = self.dog.take() {\n            let _ = dog.join();\n        }\n        // Halves (gate or graveyard) drop AFTER the watchdog is gone — no raw\n        // handle use can race the CloseHandle/limbo path.\n    }\n}\n\nfn poisoned_err() -> io::Error {\n    io::Error::new(\n        io::ErrorKind::BrokenPipe,\n        \"broker conn poisoned (bounded write retired the physical connection)\",\n    )\n}\n\nfn timeout_err() -> io::Error {\n    io::Error::new(\n        io::ErrorKind::TimedOut,\n        \"broker conn write exceeded its bound (conn poisoned + aborted)\",\n    )\n}\n\n#[cfg(test)]\nmod tests {\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,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,172,173,174,175,176,177,178,179,180,null,182,null,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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,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,null,814]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\conn.rs"}}},"isError":false,"timestamp":1784195763346}}
{"type":"message","id":"20778ddd","parentId":"2f59d50b","timestamp":"2026-07-16T09:56:03.358Z","message":{"role":"toolResult","toolCallId":"call_zENxjFbVW50ls5fRuAClKQ4U|fc_008390f508d1d09b016a58aab33778819a97f5bec7b17f678d","toolName":"read","content":[{"type":"text","text":"    pub fn attach_as_viewer_snap(\n…\n    ) -> io::Result<()> {\n…\n        self.subscribe_with(session_id, from_seq, AttachIntent::Viewer, by.map(str::to_string))?;\n        self.session_cursors.insert(session_id, from_seq);\n        Ok(())\n    }\n\n    /// Cold-start multi-session resume (restoration D4-2, ADR-0018 Q6): query the\n    /// broker for **every** hosted session and re-attach **each** in resume mode\n    /// from the broker's per-session delivered cursor (`resume_seq`). This is the\n    /// production replacement for the retired single-session `BrainState` handoff\n    /// frame — a brain the supervisor respawns (crash *or* update) reconstructs\n    /// all session continuity by querying the persistent side, never a brain→brain\n    /// message. Returns the ids re-attached (empty when the broker hosts none —\n    /// the supervised daemon brain's no-op-today case).\n    ///\n    /// Each session is seeded into [`Self::session_cursors`] at its `resume_seq`,\n    /// so [`Brain::read_event`] dedups the broker's at-least-once boundary re-send\n    /// and accepts a post-eviction ring-floor jump (no reject-gap). Output is\n    /// at-least-once; input/effects stay exactly-once via the broker-owned\n    /// `EffectJournal`. The generation is **not** touched — this is a cold start\n    /// (generation custody is the broker's, ADR-0018 V2), not a `handoff`.\n    ///\n    /// Each session is re-attached as a **VIEWER**, never a controller: a caller that\n    /// DRAINS the resumed output (`Brain::read_event`) gets output-continuity from its\n    /// cursor; a viewer never seizes/stall-evicts a controller slot (UPDATE-WEDGE-2,\n    /// REQ-BRAIN-RESUME-NO-CONTROL-STEAL). Genuinely daemon-brain-driven sessions (the\n    /// live-agent adapter future) will re-attach as Control on a drained carrier; that\n    /// set is empty today.\n    ///\n    /// ⚠️ DEADLOCK WARNING — the CALLER MUST DRAIN this conn (UPDATE-WEDGE round 3,\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK). This subscribes the sessions onto THIS conn.\n    /// A NON-DRAINING consumer will SELF-DEADLOCK: an actively-streaming session backs\n    /// the conn up, the broker-side subscriber writer blocks holding the conn's single\n    /// `SharedSend` mutex ACROSS a blocking `write_frame` (broker.rs\n    /// `viewer_writer`/`controller_writer`), the dispatch reply path (`send_frame`) needs\n    /// that same lock, so any request/reply the non-draining consumer issues is starved\n    /// (dead-peer-independent — the v0.30.4 field re-wedge). The supervised daemon brain\n    /// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn\n    /// only in `net_status()`/`sessions()` drain-and-discard loops; it drives no PTY\n    /// session — brainproc.rs:184 — and nothing on its side consumes the output, §3\n    /// guard), so it MUST call [`Self::resume_session_cursors`] (cursor-only, NO\n    /// subscribe) instead. This subscribing method stays for a DRAINING caller (the\n    /// resume int tests today; the forward live-agent-adapter / Option-B drained carrier).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-DAEMON-2]\n    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    // [doc->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {\n        let reply = self.sessions()?;\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\n        for info in reply.sessions {\n            // Seed the dedup cursor at the broker's resume point BEFORE the subscribe\n            // replay starts arriving, then re-attach (Viewer) from there.\n            self.session_cursors\n                .insert(info.session_id, info.resume_seq);\n            // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n            self.subscribe_with(info.session_id, info.resume_seq, AttachIntent::Viewer, None)?;\n            resumed.push(info.session_id);\n        }\n        Ok(resumed)\n    }\n\n    /// Cursor-only resume for a NON-DRAINING caller (the supervised daemon brain): query\n    /// every hosted session and seed [`Self::session_cursors`] at each `resume_seq`,\n    /// returning the re-attached ids — but DO **NOT** subscribe. This is the safe resume\n    /// the daemon `run_brain` heartbeat uses in place of [`Self::resume_sessions`]:\n    /// carrying only request/reply on the brain's conn (no UNDRAINED subscription) makes\n    /// the `SharedSend` self-deadlock structurally impossible (UPDATE-WEDGE round 3,\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK — see the deadlock warning on\n    /// [`Self::resume_sessions`]). The daemon brain drives no broker PTY session\n    /// (brainproc.rs:184) and nothing on its side consumes session output (§3 guard), so\n    /// the observation cursor is all it needs; no attach also means no controller steal\n    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL, a fortiori). Rides the brain-swap ⇒ seamless.\n    /// When genuinely daemon-brain-driven sessions land (the live-agent adapter future),\n    /// attach THOSE on a DEDICATED drained carrier (Option B, deferred) — never on this\n    /// request/reply conn.\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    pub fn resume_session_cursors(&mut self) -> io::Result<Vec<u64>> {\n        let reply = self.sessions()?;\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\n        for info in reply.sessions {\n            self.session_cursors\n                .insert(info.session_id, info.resume_seq);\n            resumed.push(info.session_id);\n        }\n        Ok(resumed)\n    }\n…\n    pub fn net_presence_subscribe(&mut self, from_seq: u64) -> io::Result<()> {\n…\n    }\n\n    /// The next presence sequence still needed (the durable state a handed-off\n    /// brain carries to resubscribe gaplessly).\n    pub fn presence_cursor(&self) -> Option<u64> {\n        self.presence_cursor\n    }\n\n    /// PUMP-TRUTH W2 (REQ-PUMP-PEER-ISOLATION): subscribe the pump's DEDICATED\n    /// event carrier to presence in TAIL-FOLLOW mode — no history replay, and the\n    /// cursor BASELINES on the first live event rather than gap-checking from a\n    /// fixed floor. The pump has no durable cursor to resume (a supervised restart\n    /// re-dials every peer fresh); it cares only about outcomes of dials it submits\n    /// AFTER subscribing. `from_seq = u64::MAX` means the broker replays nothing\n    /// (no retained `seq >= MAX`), so a long-lived broker whose ring has evicted\n    /// old events can never hand this carrier a below-cursor gap that would falsely\n    /// trip the dead-broker restart. Contrast [`Self::net_presence_subscribe`]\n    /// (REQ-EP-4) which gap-checks from the durable cursor so a real lost liveness\n    /// event IS caught. Subscribe BEFORE the first submit so no outcome is missed.\n    // [impl->REQ-PUMP-PEER-ISOLATION]\n    pub fn net_presence_subscribe_tail(&mut self) -> io::Result<()> {\n        self.presence_cursor = None; // baseline on the first live event (no floor)\n        self.send(\n            KIND_NET_PRESENCE_SUBSCRIBE,\n            serde_json::to_value(NetPresenceSubscribeReq { from_seq: u64::MAX })\n                .expect(\"NetPresenceSubscribeReq serializes\"),\n        )\n    }\n\n    /// Re-attach to a session's output from `from_seq` as a LOCAL CONTROLLER\n    /// (intent `Control`, identity `None`) — the legacy single-session serve\n    /// [`Brain::attach`] path.\n    ///\n    /// CAUTION — a `None`-identity Control attach does NOT \"never displace\":\n    /// `resolve_subscribe` (broker.rs) silently re-takes a FREE or SAME-LOCAL\n    /// (`None==None`) controller slot via `become_controller` (only a\n    /// DIFFERENT-REMOTE incumbent falls back to a silent viewer — the gate-#7\n    /// cross-operator guard). That silent re-take fires NO `Displaced` notice and\n    /// ORPHANS the prior local controller (the broker stops writing its output but\n    /// does not close the conn), FREEZING the session immediately; a >15s\n    /// `stall_evict_controller` is a separate, later, output-dependent symptom\n    /// (only an actively-producing stolen session blocks long enough to evict).\n    /// That is correct for `attach` (a serve loop that WILL drive the session), but\n    /// it is why a brain-respawn must NOT resume-re-attach as Control — it would\n    /// silently STEAL every local/free controller (incl. the operator's local\n    /// `spt rc`) it never drains, freezing the session\n    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL). [`Brain::resume_sessions`] re-attaches\n    /// as Viewer instead; Control is reserved for sessions the daemon brain drives.\n    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {\n        self.subscribe_with(session_id, from_seq, AttachIntent::Control, None)\n    }\n\n    /// Release this connection's controller/viewer role on `session_id`\n    /// (REQ-RCVIEW-1) — the explicit detach a `serve_attach` worker sends when\n    /// the viewport ends so the broker clears `driven_by` / `viewer_count`\n    /// promptly. Best-effort: a dropped connection clears the role anyway.\n    pub fn detach_session(&mut self, session_id: u64) -> io::Result<()> {\n        self.send(\n            KIND_UNSUBSCRIBE,\n            serde_json::to_value(UnsubscribeReq { session_id })\n                .expect(\"UnsubscribeReq serializes\"),\n        )\n    }\n\n    /// The role-aware subscribe (REQ-RCVIEW-1): `intent` selects viewer/control/\n    /// take, `by` carries the operator identity (`Some(node)` remote, `None`\n    /// local). The broker replies the resolved [`SubscribeOutcome`] as a\n    /// [`BrokerEvent::Subscribed`].\n    fn subscribe_with(\n        &mut self,\n        session_id: u64,\n        from_seq: u64,\n        intent: AttachIntent,\n        by: Option<String>,\n    ) -> io::Result<()> {\n        // P1c (operator-stream boundary, kitsubito RACEDIAG): a resume-mode brain\n        // (seeded map) that subscribes from `from_seq` must RESET this session's\n        // dedup cursor to `from_seq` — the broker's replay from `from_seq` is then\n        // re-delivered (and the operator's render cursor dedups the overlap),\n        // never lost. WHY THIS, NOT just `read_event`'s `or_insert`: on a re-serve,\n        // `serve_attach` may CONSUME a handoff-replay frame at the K boundary\n        // BEFORE it is `attached` (the frame is dropped by the `if attached` gate\n        // but the snap-above cursor still advanced past K); `attach_as(sid, 0)`\n        // then re-subscribes from 0, and without this reset the broker's re-send of\n        // seq K arrives below the advanced cursor and is deduped → seq K never\n        // reaches the operator viewport → a forward gap. Resetting to `from_seq`\n        // makes the re-subscribe re-deliver everything from its floor. A cold-start\n        // brain (empty map — e.g. the production dispatch serve brain) keeps the\n        // legacy `next_seq` path; this only arms in resume mode.\n        // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n        if !self.session_cursors.is_empty() {\n            self.session_cursors.insert(session_id, from_seq);\n        }\n        self.send(\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id,\n                from_seq,\n                intent,\n                by,\n            })\n            .expect(\"SubscribeReq serializes\"),\n        )\n    }\n\n    fn send(&mut self, kind: &str, payload: serde_json::Value) -> io::Result<()> {\n        let env = Envelope::new(kind, payload);\n        match &mut self.conn {\n            BrainConn::Whole(stream) => write_frame(stream, &env),\n            BrainConn::Split { send, .. } => write_frame(send, &env),\n        }\n    }\n\n    /// Read one framed [`Envelope`], bounded by an optional wall-clock\n    /// `deadline`. The carrier dispatch (REQ-HAZARD-PUMP-IPC-DEADLINE):\n    ///\n    /// - [`BrainConn::Whole`] — the ordinary blocking client: a direct\n    ///   [`read_frame`]; `deadline` is irrelevant (a whole brain never sets\n    ///   `io_timeout`, so callers always pass `None` here).\n    /// - [`BrainConn::Split`] — the pump carrier: the reader thread already owns\n    ///   the blocking read, so a bounded read is just [`Receiver::recv_timeout`]\n    ///   on the remaining budget. A `Timeout` surfaces [`io::ErrorKind::TimedOut`]\n    ///   — the poison signal the pump escalates to a supervised restart (NEVER a\n    ///   per-peer retry). A `Disconnected` channel means the reader thread exited\n    ///   (the conn hung up); surface it as `UnexpectedEof`."}],"details":{"displayContent":{"text":"    pub fn attach_as_viewer_snap(\n…\n    ) -> io::Result<()> {\n…\n        self.subscribe_with(session_id, from_seq, AttachIntent::Viewer, by.map(str::to_string))?;\n        self.session_cursors.insert(session_id, from_seq);\n        Ok(())\n    }\n\n    /// Cold-start multi-session resume (restoration D4-2, ADR-0018 Q6): query the\n    /// broker for **every** hosted session and re-attach **each** in resume mode\n    /// from the broker's per-session delivered cursor (`resume_seq`). This is the\n    /// production replacement for the retired single-session `BrainState` handoff\n    /// frame — a brain the supervisor respawns (crash *or* update) reconstructs\n    /// all session continuity by querying the persistent side, never a brain→brain\n    /// message. Returns the ids re-attached (empty when the broker hosts none —\n    /// the supervised daemon brain's no-op-today case).\n    ///\n    /// Each session is seeded into [`Self::session_cursors`] at its `resume_seq`,\n    /// so [`Brain::read_event`] dedups the broker's at-least-once boundary re-send\n    /// and accepts a post-eviction ring-floor jump (no reject-gap). Output is\n    /// at-least-once; input/effects stay exactly-once via the broker-owned\n    /// `EffectJournal`. The generation is **not** touched — this is a cold start\n    /// (generation custody is the broker's, ADR-0018 V2), not a `handoff`.\n    ///\n    /// Each session is re-attached as a **VIEWER**, never a controller: a caller that\n    /// DRAINS the resumed output (`Brain::read_event`) gets output-continuity from its\n    /// cursor; a viewer never seizes/stall-evicts a controller slot (UPDATE-WEDGE-2,\n    /// REQ-BRAIN-RESUME-NO-CONTROL-STEAL). Genuinely daemon-brain-driven sessions (the\n    /// live-agent adapter future) will re-attach as Control on a drained carrier; that\n    /// set is empty today.\n    ///\n    /// ⚠️ DEADLOCK WARNING — the CALLER MUST DRAIN this conn (UPDATE-WEDGE round 3,\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK). This subscribes the sessions onto THIS conn.\n    /// A NON-DRAINING consumer will SELF-DEADLOCK: an actively-streaming session backs\n    /// the conn up, the broker-side subscriber writer blocks holding the conn's single\n    /// `SharedSend` mutex ACROSS a blocking `write_frame` (broker.rs\n    /// `viewer_writer`/`controller_writer`), the dispatch reply path (`send_frame`) needs\n    /// that same lock, so any request/reply the non-draining consumer issues is starved\n    /// (dead-peer-independent — the v0.30.4 field re-wedge). The supervised daemon brain\n    /// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn\n    /// only in `net_status()`/`sessions()` drain-and-discard loops; it drives no PTY\n    /// session — brainproc.rs:184 — and nothing on its side consumes the output, §3\n    /// guard), so it MUST call [`Self::resume_session_cursors`] (cursor-only, NO\n    /// subscribe) instead. This subscribing method stays for a DRAINING caller (the\n    /// resume int tests today; the forward live-agent-adapter / Option-B drained carrier).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-DAEMON-2]\n    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    // [doc->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {\n        let reply = self.sessions()?;\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\n        for info in reply.sessions {\n            // Seed the dedup cursor at the broker's resume point BEFORE the subscribe\n            // replay starts arriving, then re-attach (Viewer) from there.\n            self.session_cursors\n                .insert(info.session_id, info.resume_seq);\n            // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n            self.subscribe_with(info.session_id, info.resume_seq, AttachIntent::Viewer, None)?;\n            resumed.push(info.session_id);\n        }\n        Ok(resumed)\n    }\n\n    /// Cursor-only resume for a NON-DRAINING caller (the supervised daemon brain): query\n    /// every hosted session and seed [`Self::session_cursors`] at each `resume_seq`,\n    /// returning the re-attached ids — but DO **NOT** subscribe. This is the safe resume\n    /// the daemon `run_brain` heartbeat uses in place of [`Self::resume_sessions`]:\n    /// carrying only request/reply on the brain's conn (no UNDRAINED subscription) makes\n    /// the `SharedSend` self-deadlock structurally impossible (UPDATE-WEDGE round 3,\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK — see the deadlock warning on\n    /// [`Self::resume_sessions`]). The daemon brain drives no broker PTY session\n    /// (brainproc.rs:184) and nothing on its side consumes session output (§3 guard), so\n    /// the observation cursor is all it needs; no attach also means no controller steal\n    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL, a fortiori). Rides the brain-swap ⇒ seamless.\n    /// When genuinely daemon-brain-driven sessions land (the live-agent adapter future),\n    /// attach THOSE on a DEDICATED drained carrier (Option B, deferred) — never on this\n    /// request/reply conn.\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    pub fn resume_session_cursors(&mut self) -> io::Result<Vec<u64>> {\n        let reply = self.sessions()?;\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\n        for info in reply.sessions {\n            self.session_cursors\n                .insert(info.session_id, info.resume_seq);\n            resumed.push(info.session_id);\n        }\n        Ok(resumed)\n    }\n…\n    pub fn net_presence_subscribe(&mut self, from_seq: u64) -> io::Result<()> {\n…\n    }\n\n    /// The next presence sequence still needed (the durable state a handed-off\n    /// brain carries to resubscribe gaplessly).\n    pub fn presence_cursor(&self) -> Option<u64> {\n        self.presence_cursor\n    }\n\n    /// PUMP-TRUTH W2 (REQ-PUMP-PEER-ISOLATION): subscribe the pump's DEDICATED\n    /// event carrier to presence in TAIL-FOLLOW mode — no history replay, and the\n    /// cursor BASELINES on the first live event rather than gap-checking from a\n    /// fixed floor. The pump has no durable cursor to resume (a supervised restart\n    /// re-dials every peer fresh); it cares only about outcomes of dials it submits\n    /// AFTER subscribing. `from_seq = u64::MAX` means the broker replays nothing\n    /// (no retained `seq >= MAX`), so a long-lived broker whose ring has evicted\n    /// old events can never hand this carrier a below-cursor gap that would falsely\n    /// trip the dead-broker restart. Contrast [`Self::net_presence_subscribe`]\n    /// (REQ-EP-4) which gap-checks from the durable cursor so a real lost liveness\n    /// event IS caught. Subscribe BEFORE the first submit so no outcome is missed.\n    // [impl->REQ-PUMP-PEER-ISOLATION]\n    pub fn net_presence_subscribe_tail(&mut self) -> io::Result<()> {\n        self.presence_cursor = None; // baseline on the first live event (no floor)\n        self.send(\n            KIND_NET_PRESENCE_SUBSCRIBE,\n            serde_json::to_value(NetPresenceSubscribeReq { from_seq: u64::MAX })\n                .expect(\"NetPresenceSubscribeReq serializes\"),\n        )\n    }\n\n    /// Re-attach to a session's output from `from_seq` as a LOCAL CONTROLLER\n    /// (intent `Control`, identity `None`) — the legacy single-session serve\n    /// [`Brain::attach`] path.\n    ///\n    /// CAUTION — a `None`-identity Control attach does NOT \"never displace\":\n    /// `resolve_subscribe` (broker.rs) silently re-takes a FREE or SAME-LOCAL\n    /// (`None==None`) controller slot via `become_controller` (only a\n    /// DIFFERENT-REMOTE incumbent falls back to a silent viewer — the gate-#7\n    /// cross-operator guard). That silent re-take fires NO `Displaced` notice and\n    /// ORPHANS the prior local controller (the broker stops writing its output but\n    /// does not close the conn), FREEZING the session immediately; a >15s\n    /// `stall_evict_controller` is a separate, later, output-dependent symptom\n    /// (only an actively-producing stolen session blocks long enough to evict).\n    /// That is correct for `attach` (a serve loop that WILL drive the session), but\n    /// it is why a brain-respawn must NOT resume-re-attach as Control — it would\n    /// silently STEAL every local/free controller (incl. the operator's local\n    /// `spt rc`) it never drains, freezing the session\n    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL). [`Brain::resume_sessions`] re-attaches\n    /// as Viewer instead; Control is reserved for sessions the daemon brain drives.\n    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {\n        self.subscribe_with(session_id, from_seq, AttachIntent::Control, None)\n    }\n\n    /// Release this connection's controller/viewer role on `session_id`\n    /// (REQ-RCVIEW-1) — the explicit detach a `serve_attach` worker sends when\n    /// the viewport ends so the broker clears `driven_by` / `viewer_count`\n    /// promptly. Best-effort: a dropped connection clears the role anyway.\n    pub fn detach_session(&mut self, session_id: u64) -> io::Result<()> {\n        self.send(\n            KIND_UNSUBSCRIBE,\n            serde_json::to_value(UnsubscribeReq { session_id })\n                .expect(\"UnsubscribeReq serializes\"),\n        )\n    }\n\n    /// The role-aware subscribe (REQ-RCVIEW-1): `intent` selects viewer/control/\n    /// take, `by` carries the operator identity (`Some(node)` remote, `None`\n    /// local). The broker replies the resolved [`SubscribeOutcome`] as a\n    /// [`BrokerEvent::Subscribed`].\n    fn subscribe_with(\n        &mut self,\n        session_id: u64,\n        from_seq: u64,\n        intent: AttachIntent,\n        by: Option<String>,\n    ) -> io::Result<()> {\n        // P1c (operator-stream boundary, kitsubito RACEDIAG): a resume-mode brain\n        // (seeded map) that subscribes from `from_seq` must RESET this session's\n        // dedup cursor to `from_seq` — the broker's replay from `from_seq` is then\n        // re-delivered (and the operator's render cursor dedups the overlap),\n        // never lost. WHY THIS, NOT just `read_event`'s `or_insert`: on a re-serve,\n        // `serve_attach` may CONSUME a handoff-replay frame at the K boundary\n        // BEFORE it is `attached` (the frame is dropped by the `if attached` gate\n        // but the snap-above cursor still advanced past K); `attach_as(sid, 0)`\n        // then re-subscribes from 0, and without this reset the broker's re-send of\n        // seq K arrives below the advanced cursor and is deduped → seq K never\n        // reaches the operator viewport → a forward gap. Resetting to `from_seq`\n        // makes the re-subscribe re-deliver everything from its floor. A cold-start\n        // brain (empty map — e.g. the production dispatch serve brain) keeps the\n        // legacy `next_seq` path; this only arms in resume mode.\n        // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n        if !self.session_cursors.is_empty() {\n            self.session_cursors.insert(session_id, from_seq);\n        }\n        self.send(\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id,\n                from_seq,\n                intent,\n                by,\n            })\n            .expect(\"SubscribeReq serializes\"),\n        )\n    }\n\n    fn send(&mut self, kind: &str, payload: serde_json::Value) -> io::Result<()> {\n        let env = Envelope::new(kind, payload);\n        match &mut self.conn {\n            BrainConn::Whole(stream) => write_frame(stream, &env),\n            BrainConn::Split { send, .. } => write_frame(send, &env),\n        }\n    }\n\n    /// Read one framed [`Envelope`], bounded by an optional wall-clock\n    /// `deadline`. The carrier dispatch (REQ-HAZARD-PUMP-IPC-DEADLINE):\n    ///\n    /// - [`BrainConn::Whole`] — the ordinary blocking client: a direct\n    ///   [`read_frame`]; `deadline` is irrelevant (a whole brain never sets\n    ///   `io_timeout`, so callers always pass `None` here).\n    /// - [`BrainConn::Split`] — the pump carrier: the reader thread already owns\n    ///   the blocking read, so a bounded read is just [`Receiver::recv_timeout`]\n    ///   on the remaining budget. A `Timeout` surfaces [`io::ErrorKind::TimedOut`]\n    ///   — the poison signal the pump escalates to a supervised restart (NEVER a\n    ///   per-peer retry). A `Disconnected` channel means the reader thread exited\n    ///   (the conn hung up); surface it as `UnexpectedEof`.","startLine":992,"lineNumbers":[992,null,997,null,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,null,1553,null,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs"}}},"isError":false,"timestamp":1784195763358}}
{"type":"message","id":"898963f5","parentId":"20778ddd","timestamp":"2026-07-16T09:56:03.395Z","message":{"role":"toolResult","toolCallId":"call_J5Y7eRRn63eCDqeoYB8X9ZMB|fc_008390f508d1d09b016a58aab3376c819a8c58c2616853356c","toolName":"read","content":[{"type":"text","text":"/// `Arc<Mutex<SendHalf>>` — [`BrokerConn`] owns the half and every write is\n/// bounded + cancelable + poison-on-failure (see [`crate::conn`]). Identity\n/// matching stays `Arc::ptr_eq`.\npub(crate) type SharedSend = Arc<BrokerConn>;\n\n/// Default bounded output-log depth (chunks). Large enough that a brain restart\n/// window never evicts unreplayed output in practice; the bound is what keeps a\n/// disconnected session from buffering without limit.\nconst DEFAULT_LOG_CHUNKS: usize = 4096;\n\n/// The viewer-channel depth (frames). A bounded SPSC queue per viewer; when it\n/// OVERFLOWS the viewer is evicted — the drain `try_send`s and never blocks\n/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render\n/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n/// owns the replay batch), so a viewer attaching to a busy session is not\n/// spuriously evicted.\nconst VIEWER_CHANNEL_DEPTH: usize = 256;\n\n/// Soft cap on concurrent viewers per session (REQ-HAZARD-VIEWER-ISOLATION:\n/// bounds the writer-thread count). A `Viewer` attach beyond the cap is refused.\nconst MAX_VIEWERS: usize = 32;\n\n/// Whether a viewer's bounded-queue `try_send` result demands EVICTION\n/// (REQ-HAZARD-VIEWER-ISOLATION): a `Full` queue (the viewer can't keep up with\n/// the live stream) or a `Disconnected` one (its writer thread ended after a\n/// dead socket). The drain evicts on either and NEVER blocks on a viewer — this\n/// is the pure decision the fan-out applies under the log lock.\n// [impl->REQ-HAZARD-VIEWER-ISOLATION]\nfn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {\n    r.is_err()\n}\n\n/// One read-only viewer's isolated sink (REQ-HAZARD-VIEWER-ISOLATION). The drain\n/// thread `try_send`s output frames into `tx` (a bounded queue); a dedicated\n/// `writer` thread does the BLOCKING socket write, so a wedged viewer backs up\n/// only its own queue — never the drain. When the queue overflows the sink is\n/// dropped (the `tx` closes → the writer ends) and removed from the map.\nstruct ViewerSink {\n    /// The bounded live-frame queue to this viewer's writer thread.\n    tx: SyncSender<Envelope>,\n    /// The viewer's IPC send half — kept ONLY for identity matching on detach\n    /// (a dropped brain connection removes its viewer rows); never written here.\n    send: SharedSend,\n    /// Set TRUE when this viewer is EVICTED for overflow (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT),\n    /// distinguishing an eviction from a normal teardown (session end / conn drop).\n    /// `append` stores `true` BEFORE dropping the sink (and thus `tx`); the writer,\n    /// on its `rx.recv()` Err, reads this and — when true — emits a\n    /// [`KIND_VIEWER_EVICTED`] marker (in the writer thread, OFF the log lock) so\n    /// the serving brain skips to live instead of tearing the attach down.\n    evicted: Arc<AtomicBool>,\n    /// The writer thread handle (held for ownership; it exits when `tx` drops or\n    /// a socket write fails).\n    _writer: JoinHandle<()>,\n}\n\n/// The controller live-frame channel depth (frames). Deeper than a viewer's\n/// queue because the controller is the AUTHORITATIVE view — unlike a viewer it is\n/// NOT evicted on a SINGLE full `try_send` (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).\n/// b4: the drain does ONE non-blocking `try_send` per chunk and NEVER blocks; a\n/// merely-slow controller DROPS chunks + falls behind the ring, and only a channel\n/// that stays full for [`CONTROLLER_WRITE_DEADLINE`] (tracked via\n/// [`ControllerSink::last_ok`]) is detached. Generous so a momentary slow consumer\n/// never trips the deadline.\nconst CONTROLLER_CHANNEL_DEPTH: usize = 4096;\n\n/// How long a CONTROLLER's channel may stay FULL (no successful handoff since\n/// [`ControllerSink::last_ok`]) before the drain declares it stalled and detaches it\n/// (W1, refined by b4 / REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE). This\n/// BOUNDS the wedge: a backed-up controller falls behind for at most this long before\n/// eviction — never forever (the v0.12.x root) — and the drain NEVER blocks or sleeps\n/// (a single off-lock `try_send` per chunk; see [`OutputLog::append`]). A live but\n/// merely slow operator lands a frame before this elapses (resetting `last_ok`) and is\n/// untouched.\nconst CONTROLLER_WRITE_DEADLINE: Duration = Duration::from_secs(5);\n\n/// How long a controller's writer thread may sit BLOCKED inside a single socket\n/// write — the brain subscriber suspended / black-holed — before a take/reattach\n/// or the `KIND_SESSIONS` reap stall-evicts it (LIFECYCLE-TRUTH W2,\n/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n/// protects the fan-out against ONE per-client conn falling behind and is\n/// output-driven (the drain observes a Full channel); THIS bound is measured\n/// full-with-**zero writer progress** and covers the case the output-driven path\n/// can't — an IDLE session whose brain-subscriber conn is suspended produces no\n/// output, so `append`/`mark_controller_gone` never runs, and the stalled\n/// controller would otherwise latch `driven_by` forever (reattach refused, `--take`\n/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n/// is bursty; 15 s tolerates a normal burst-stall while still bounding the wedge.\n/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nconst BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n\n/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n/// the slot) is provable in ~1 s instead of 15 (mirrors the\n/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n/// ONLY — never relaxes an assertion.\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nfn brain_write_deadline() -> Duration {\n    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n        Ok(ms) => ms\n            .parse::<u64>()\n            .map(Duration::from_millis)\n            .unwrap_or(BRAIN_WRITE_DEADLINE),\n        Err(_) => BRAIN_WRITE_DEADLINE,\n    }\n}\n\n/// Bounded wait for a concurrent wake's session to register before a second wake for\n/// the same endpoint stops deduping and spawns itself (W4, REQ-SPAWN-COLLISION-GUARD-\n/// LIVE-DUP). A racing broker spawn registers its session in PTY-spawn time (ms), so a\n/// loser waits for the winner's session then no-op acks it (exactly one launch tree);\n/// only a spawn that OVERRAN this ceiling (presumed dead, its claim leaked) is taken\n/// over — availability over a vanishingly rare dup.\nconst WAKE_DEDUP_WAIT: Duration = Duration::from_secs(2);\n/// Poll slice while waiting on a racing wake's session to appear.\nconst WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n\n/// RAII release of a single-flight wake claim (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP):\n/// the broker holds an endpoint id in [`Broker::wake_inflight`] for the duration of a\n/// spawn so a concurrent duplicate wake stands down; the guard releases it on EVERY\n/// exit (success, an early `?` spawn/drain error, or a panic), so a failed spawn never\n/// strands the endpoint permanently claimed.\n…\nimpl OutputLog {\n…\n    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\n    fn clear_controller(&mut self) {\n        if self.controller.take().is_some() {\n            self.stamp_driven_by();\n        }\n    }\n\n    /// B-2 (REQ-CONTROLLER-LIVENESS-REAP): lazily drop a controller whose WRITER\n    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\n    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\n    /// writer exits when a socket write fails (a severed / keepalive-killed controller\n    /// conn) or its `tx` drops; while it is parked on `rx.recv()` (a LIVE but idle\n    /// controller) it is NOT finished, so a live controller is never touched (the\n    /// no-false-reap guard). Reaping here makes the broker report honest truth so the\n    /// EXISTING [`converge_perch_stamps`] clears the now-stale info.json\n    /// `driven_by`/`controlled` stamp — no new detach path, no new query.\n    ///\n    /// LOCK-SAFE: this runs UNDER the log lock in the snapshot closure, so it must do\n    /// NO file I/O (the lock-across-effect discipline, KH 7.12/5.16). It drops the\n    /// sink in-memory ONLY (like [`become_controller`]'s prior-drop) and does NOT\n    /// [`stamp_driven_by`] — the OFF-lock `converge_perch_stamps` writes the honest\n    /// stamp. Returns true iff it reaped.\n    ///\n    /// SCOPE (doyle B-2 reduction): catches a writer that FAILED A WRITE (output was\n    /// flowing when the conn severed) or whose detach was dropped by a prior wedge\n    /// (the B-1 poison class). A TRULY IDLE severed controller (writer parked on recv,\n    /// no output, conn not yet EOF'd) stays `is_finished() == false` and converges\n    /// only once output resumes or the conn EOFs (→ `handle_conn` detach) — that\n    /// harder active-probe case is the reserved [`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`],\n    /// deliberately NOT built here. KH 7.15 held by construction: only ever CLEARS,\n    /// never latches `driven_by`.\n    // [impl->REQ-CONTROLLER-LIVENESS-REAP]\n    fn reap_dead_controller(&mut self) -> bool {\n        let dead = self\n            .controller\n            .as_ref()\n            .is_some_and(|c| c._writer.is_finished());\n        if dead {\n            // In-memory drop only (ends the — already-exited — writer); NO stamp here\n            // (info.json I/O under the log lock is the KH 7.12 hazard). converge does it.\n            self.controller = None;\n            return true;\n        }\n        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller whose\n        // writer is BLOCKED (not exited) on a suspended brain conn past\n        // BRAIN_WRITE_DEADLINE is also reaped here, so the honest `KIND_SESSIONS`\n        // truth + the off-lock converge_perch_stamps release its stale `driven_by`\n        // stamp even when nobody take/reattaches. Deferred stamp: the reap runs in\n        // the multi-session snapshot closure (no I/O under the shared sessions lock).\n        self.stall_evict_controller(Instant::now(), StampMode::Deferred)\n    }\n\n    /// Deadline-evict path (W1): the drain observed a fully-stalled controller and\n    /// timed out its `send_timeout`. Detach it — but ONLY if it is still the same\n    /// controller generation the drain saw (`epoch`), so a NEWER controller that\n    /// attached during the stall is never unseated.\n    fn mark_controller_gone(&mut self, epoch: u64) {\n        if self.controller_epoch.load(Ordering::Acquire) == epoch {\n            self.clear_controller();\n        }\n    }\n\n    /// Whether the current controller's writer is WEDGED — blocked inside a single\n    /// socket write for at least `deadline` (the brain-subscriber conn suspended /\n    /// black-holed). An IDLE controller (writer parked on `rx.recv()`, nothing to\n    /// write) reads NOT stalled: its [`ControllerSink::write_blocked_since`] is `None`.\n    /// Pure over the injected `now` + `deadline` so the unit tests it with no thread.\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn controller_write_stalled(&self, now: Instant, deadline: Duration) -> bool {\n        self.controller.as_ref().is_some_and(|c| {\n            c.write_blocked_since\n                .lock()\n                .ok()\n                .and_then(|g| *g)\n                .is_some_and(|since| now.saturating_duration_since(since) >= deadline)\n        })\n    }\n\n    /// 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    /// under the log lock via [`become_controller`]) or is deferred to the off-lock\n    /// `converge_perch_stamps` (the multi-session reap closure, KH 7.12 no-I/O rule).\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn stall_evict_controller(&mut self, now: Instant, stamp: StampMode) -> bool {\n        let deadline = brain_write_deadline();\n        if !self.controller_write_stalled(now, deadline) {\n            return false;\n        }\n        // Attribution (F-039 leg b): the evicted controller's stable conn id +\n        // stamps ride the record. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        let conn = self.controller.as_ref().map(|c| c.send.id()).unwrap_or(0);\n        eprintln!(\n            \"BRAIN_SUBSCRIBER_STALL_EVICT:{} conn={conn} {} — controller writer blocked > {}ms \\\n             on a suspended/black-holed brain conn; detached, driven_by released \\\n             [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\n            self.session_id,\n            crate::conn::log_stamp(),\n            deadline.as_millis()\n        );\n        record_stall_evict();\n        match stamp {\n            // Inline: drop the sink AND re-stamp the perch (become_controller's\n            // sibling — the take/reattach path is already I/O-under-log-lock).\n            StampMode::Inline => self.clear_controller(),\n            // Deferred: in-memory drop ONLY (the reap closure forbids I/O under the\n            // shared sessions lock); converge_perch_stamps writes the release off-lock.\n            StampMode::Deferred => self.controller = None,\n        }\n        true\n    }\n\n    /// Add a read-only viewer: snapshot the ring from `from_seq` into the writer\n    /// thread (NOT through the bounded live queue — replay must not trip the\n    /// overflow eviction), seed it the current size frame first, then run the\n    /// live queue. The drain thread fans live frames in via `try_send`.\n    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {\n        let vid = self.next_viewer_id;\n        self.next_viewer_id += 1;\n        // Attribution (F-039 legs b+d): viewer role fact + attach record.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        sub.describe(&format!(\n            \"viewer session={} endpoint={} vid={vid}\",\n            self.session_id,\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint }\n        ));\n        sub.lifecycle_event(\n            \"viewer-attach\",\n            &format!(\"session={} vid={vid} from_seq={from_seq}\", self.session_id),\n        );\n        // The size frame + the initial batch (owned by the thread, not the bounded\n        // queue). A COLD viewer attach (`from_seq == 0`) gets the #6 clean repaint;\n        // a skip-to-live / snap re-attach (`from_seq > 0`, incl. `u64::MAX`) keeps\n        // the raw-ring slice (`s >= from_seq`) — empty for skip-to-live, as before.\n        let mut initial: Vec<Envelope> =\n            vec![size_envelope(self.session_id, self.size.0, self.size.1)];\n        match self.repaint_initial(from_seq) {\n            Some((seq, bytes)) => initial.push(output_envelope(self.session_id, seq, &bytes)),\n            None => initial.extend(\n                self.ring\n                    .iter()\n                    .filter(|(s, _)| *s >= from_seq)\n                    .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),\n            ),\n        }\n        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);\n        let writer_send = Arc::clone(&sub);\n        let evicted = Arc::new(AtomicBool::new(false));\n        let writer_evicted = Arc::clone(&evicted);\n        let session_id = self.session_id;\n        let writer =\n            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));\n        self.viewers.insert(\n            vid,\n            ViewerSink {\n                tx,\n                send: sub,\n                evicted,\n                _writer: writer,\n            },\n        );\n    }\n\n    /// Resolve a controller/viewer subscribe under the W2.5 model\n    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs\n    /// the controller take / viewer add / loud displace as a side effect. See the\n    /// identity matrix in the body — the `by`-keyed logic is what makes a\n    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.\n    fn resolve_subscribe(\n        &mut self,\n        sub: SharedSend,\n        from_seq: u64,\n        intent: AttachIntent,\n        by: Option<String>,\n    ) -> SubscribeOutcome {\n        if intent == AttachIntent::Viewer {\n            if self.viewers.len() >= MAX_VIEWERS {\n                // Soft cap reached — refuse the viewer (bounds thread count).\n                return SubscribeOutcome::BusyControlled {\n                    by: \"viewer cap reached\".to_string(),\n                };\n            }\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        // Control or Take.\n        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller WEDGED\n        // on a suspended/black-holed brain conn past BRAIN_WRITE_DEADLINE is\n        // stall-evicted HERE — the take/reattach self-heals against broker truth\n        // without waiting for new output (the output-driven mark_controller_gone\n        // never fires on an idle session). Inline stamp: this path already writes the\n        // perch under the log lock via become_controller. After the evict the slot\n        // reads free and the normal free/take logic below runs.\n        self.stall_evict_controller(Instant::now(), StampMode::Inline);\n        if self.controller.is_none() {\n            // Free endpoint → become controller (nothing to displace).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        let same_identity = self.controller_by() == by.as_deref();\n        if same_identity {\n            // Same operator (or both local) re-serving — silent re-take. This is\n            // the gate-#7 path: a successor re-taking the slot after a brain\n            // restart NEVER fires Displaced.\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        if by.is_none() {\n            // The LOCAL brain re-attaching while a REMOTE controls — never steal;\n            // fall back to a silent viewer (gate #7 for the local-resume path).\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        if self.controller_by().is_none() {\n            // A remote takes an UNDRIVEN session (only the local spawner held the\n            // slot) — silent control, no loud displace (there is no remote driver\n            // to notify).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        // A DIFFERENT remote controls it.\n        match intent {\n            AttachIntent::Take => {\n                // The ONLY loud-displace path: notify the incumbent, then take.\n                // W1: route the Displaced notice through the OLD controller's\n                // writer thread (best-effort, off the dispatch thread) — the\n                // incumbent we're kicking may be exactly the wedged one, so an\n                // inline write here would block the new attacher's dispatch.\n                if let Some(old) = &self.controller {\n                    let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(\"\"));\n                    let _ = old.tx.try_send(CtrlMsg::Control(frame));\n                }\n                self.become_controller(sub, by, from_seq);\n                SubscribeOutcome::TookControl\n            }\n            _ => SubscribeOutcome::BusyControlled {\n                by: self.controller_by().unwrap_or_default().to_string(),\n            },\n        }\n    }\n\n    /// Update the stored PTY size and push a `size` frame to every viewer (the\n    /// letterbox feed). Called on a controller resize. Viewer pushes are\n    /// non-blocking with eviction, like [`OutputLog::append`].\n    fn set_size_and_notify(&mut self, rows: u16, cols: u16) {\n        self.size = (rows, cols);\n        // Keep the render grid geometry in step so a post-resize repaint reflects\n        // the new viewport (#8 resize-artifact fix, ADR-0031).\n        self.grid.resize(rows, cols);\n        if self.viewers.is_empty() {\n            return;\n        }\n        let frame = size_envelope(self.session_id, rows, cols);\n        let mut evicted: Vec<u64> = Vec::new();\n        for (vid, sink) in self.viewers.iter() {\n            if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {\n                evicted.push(*vid);\n            }\n        }\n        if !evicted.is_empty() {\n            for vid in evicted {\n                // Same overflow eviction as `append` — signal skip-to-live so the\n                // viewer re-subscribes rather than dying silently on a size push\n                // (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).\n                if let Some(sink) = self.viewers.remove(&vid) {\n                    sink.evicted.store(true, Ordering::Release);\n                }\n            }\n            self.stamp_viewer_count();\n        }\n    }\n\n    /// Detach `sub` from whatever role it held: clear the controller slot iff it\n    /// is the current controller (a no-op if a newer brain took over — a late\n    /// disconnect can't unseat the live controller), and remove any viewer rows\n    /// owned by `sub`. Leaves `delivered_through` intact (D4-1). Re-stamps\n    /// `driven_by` / `viewer_count` when a role actually changed.\n    fn detach_if(&mut self, sub: &SharedSend) {\n        let is_ctrl = self\n            .controller\n            .as_ref()\n            .is_some_and(|c| Arc::ptr_eq(&c.send, sub));\n        if is_ctrl {\n            self.clear_controller(); // drops the sink (ends its writer) + unlatches\n        }\n        let before = self.viewers.len();\n        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));\n        // Bounded detach record (F-039 leg d) — only when a role actually changed.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        if is_ctrl || self.viewers.len() != before {\n            sub.lifecycle_event(\n                \"session-detach\",\n                &format!(\n                    \"session={} was_controller={is_ctrl} viewers_dropped={}\",\n                    self.session_id,\n                    before - self.viewers.len()\n                ),\n            );\n        }\n        if self.viewers.len() != before {\n            self.stamp_viewer_count();\n        }\n    }\n\n    /// Every sink currently attached (controller + viewers) — the recipients of\n    /// an `exit` frame.\n    fn all_sinks(&self) -> Vec<SharedSend> {\n        let mut v: Vec<SharedSend> = Vec::new();\n        if let Some(c) = &self.controller {\n            v.push(Arc::clone(&c.send));\n        }\n        for sink in self.viewers.values() {\n            v.push(Arc::clone(&sink.send));\n        }\n        v\n    }\n\n    /// Stamp the perch's `driven_by` to the current controller's identity (the\n    /// broker is the single writer — resolves the clear-race). The remote-drive\n    /// detection fact (REQ-REACH-1) moved here from `serve_attach` so a displaced\n    /// operator's teardown can never wipe the new controller's marker.\n    /// Best-effort: a session served without a perch (raw broker tests) has\n    /// nowhere to record.\n    // [impl->REQ-RCVIEW-1] [impl->REQ-REACH-1]\n    fn stamp_driven_by(&self) {\n        if self.endpoint.is_empty() {\n            return;\n        }\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\n        // `driven_by` is REMOTE-only: `controller_by()` is `None` for a local\n        // controller (its `by` is `None`), and it MUST stay that way — a local\n        // controller latching driven_by trips REQ-HAZARD-DRIVEN-BY-SELFHEAL (KH 7.15).\n        let _ = spt_store::info::set_driven_by(&perch, self.controller_by());\n        // #3 (REQ-GOSSIP-CONTROLLED-ANY): stamp a SEPARATE any-controller datum so a\n        // LOCALLY-driven endpoint (driven_by = None) still gossips as controlled to\n        // remote viewers. `controller_by` answers \"is a remote node driving me\";\n        // `controller.is_some()` answers \"is ANYONE driving me\".\n        // [impl->REQ-GOSSIP-CONTROLLED-ANY]\n        let _ = spt_store::info::set_controlled(&perch, self.controller.is_some());\n    }\n\n    /// Stamp the perch's `viewer_count` to the live viewer count (broker is the\n    /// single writer). Best-effort, like [`OutputLog::stamp_driven_by`].\n    // [impl->REQ-RCVIEW-1]\n    fn stamp_viewer_count(&self) {\n        if self.endpoint.is_empty() {\n            return;\n        }\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\n        let _ = spt_store::info::set_viewer_count(&perch, self.viewers.len() as u32);\n    }\n\n    /// Reap-time unlatch: a control/viewer stamp never outlives its session\n    /// (REQ-HAZARD-CONTROL-STAMP-LIFETIME). `/exit` kills the CHILD, not the\n    /// controller connection, so the OutputLog drops with the controller slot\n    /// still occupied — [`clear_controller`]'s detach path never runs, and\n    /// `controlled` / `driven_by` / `viewer_count` would otherwise latch in\n    /// info.json forever (and gossip a stale `controller_node` cross-node). The\n    /// exit-waiter calls this on reap so every teardown path clears what attach\n    /// stamped. Unconditional (idempotent when already clear) — the session is\n    /// dead, so there is no live controller to re-stamp concurrently (same\n    /// race-free argument as the W5 no-session self-heal). Best-effort, like the\n    /// sibling stampers; empty endpoint ⇒ nowhere to record.\n    // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n    fn stamp_reaped(&self) {\n        if self.endpoint.is_empty() {\n            return;\n        }\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\n        let _ = spt_store::info::set_driven_by(&perch, None);\n        let _ = spt_store::info::set_controlled(&perch, false);\n        let _ = spt_store::info::set_viewer_count(&perch, 0);\n    }\n\n    /// Whether `sub` is the current controller (the resize-exclusivity check).\n    fn is_controller(&self, sub: &SharedSend) -> bool {\n        self.controller\n            .as_ref()\n            .is_some_and(|c| Arc::ptr_eq(&c.send, sub))\n    }\n\n    /// The next seq a re-attaching controller still needs (the persisted resume\n    /// cursor surfaced in `KIND_SESSIONS` → `resume_seq`). W1: read from the\n    /// atomic the controller writer thread advances.\n    fn delivered_through(&self) -> u64 {\n        self.delivered_through.load(Ordering::Acquire)\n    }\n}\n\n/// A viewer's dedicated writer thread (REQ-HAZARD-VIEWER-ISOLATION): write the\n/// `initial` batch (size frame + ring replay) once, then drain live frames from\n/// `rx` with a BLOCKING socket write each. A failed write (dead viewer) ends the\n/// thread; the drain thread's next `try_send` then sees `Disconnected` and\n/// evicts the row. The drain thread itself never blocks on this socket.\nfn viewer_writer(\n    send: SharedSend,\n    session_id: u64,\n    initial: Vec<Envelope>,\n    rx: Receiver<Envelope>,\n    evicted: Arc<AtomicBool>,\n) {\n    // Bounded writer-exit record on every return path (F-039 leg d) — RAII so\n    // the reason is stamped exactly once however the thread ends.\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    struct ExitNote {\n        send: SharedSend,\n        session_id: u64,\n        reason: &'static str,\n    }\n    impl Drop for ExitNote {\n        fn drop(&mut self) {\n            self.send.lifecycle_event(\n                \"writer-exit\",\n                &format!(\"role=viewer session={} reason={}\", self.session_id, self.reason),\n            );\n        }\n    }\n    let mut note = ExitNote {\n        send: Arc::clone(&send),\n        session_id,\n        reason: \"write-failed\",\n    };\n    for frame in &initial {\n        // Bounded, cancelable conn write (round-4): a viewer that stops\n        // reading poisons + retires ITS conn at the deadline instead of\n        // parking this thread in the OS write forever.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        if !matches!(send.write(frame), Ok(ConnWrite::Done)) {\n            return;\n        }\n    }\n    while let Ok(frame) = rx.recv() {\n        if !matches!(send.write(&frame), Ok(ConnWrite::Done)) {\n            return;\n        }\n    }\n    note.reason = \"channel-closed\";\n    // `tx` dropped. If this was an EVICTION for overflow (append set the flag\n    // before dropping the sink), emit a KIND_VIEWER_EVICTED marker — DISTINCT from\n…\n}"}],"details":{"displayContent":{"text":"/// `Arc<Mutex<SendHalf>>` — [`BrokerConn`] owns the half and every write is\n/// bounded + cancelable + poison-on-failure (see [`crate::conn`]). Identity\n/// matching stays `Arc::ptr_eq`.\npub(crate) type SharedSend = Arc<BrokerConn>;\n\n/// Default bounded output-log depth (chunks). Large enough that a brain restart\n/// window never evicts unreplayed output in practice; the bound is what keeps a\n/// disconnected session from buffering without limit.\nconst DEFAULT_LOG_CHUNKS: usize = 4096;\n\n/// The viewer-channel depth (frames). A bounded SPSC queue per viewer; when it\n/// OVERFLOWS the viewer is evicted — the drain `try_send`s and never blocks\n/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render\n/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n/// owns the replay batch), so a viewer attaching to a busy session is not\n/// spuriously evicted.\nconst VIEWER_CHANNEL_DEPTH: usize = 256;\n\n/// Soft cap on concurrent viewers per session (REQ-HAZARD-VIEWER-ISOLATION:\n/// bounds the writer-thread count). A `Viewer` attach beyond the cap is refused.\nconst MAX_VIEWERS: usize = 32;\n\n/// Whether a viewer's bounded-queue `try_send` result demands EVICTION\n/// (REQ-HAZARD-VIEWER-ISOLATION): a `Full` queue (the viewer can't keep up with\n/// the live stream) or a `Disconnected` one (its writer thread ended after a\n/// dead socket). The drain evicts on either and NEVER blocks on a viewer — this\n/// is the pure decision the fan-out applies under the log lock.\n// [impl->REQ-HAZARD-VIEWER-ISOLATION]\nfn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {\n    r.is_err()\n}\n\n/// One read-only viewer's isolated sink (REQ-HAZARD-VIEWER-ISOLATION). The drain\n/// thread `try_send`s output frames into `tx` (a bounded queue); a dedicated\n/// `writer` thread does the BLOCKING socket write, so a wedged viewer backs up\n/// only its own queue — never the drain. When the queue overflows the sink is\n/// dropped (the `tx` closes → the writer ends) and removed from the map.\nstruct ViewerSink {\n    /// The bounded live-frame queue to this viewer's writer thread.\n    tx: SyncSender<Envelope>,\n    /// The viewer's IPC send half — kept ONLY for identity matching on detach\n    /// (a dropped brain connection removes its viewer rows); never written here.\n    send: SharedSend,\n    /// Set TRUE when this viewer is EVICTED for overflow (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT),\n    /// distinguishing an eviction from a normal teardown (session end / conn drop).\n    /// `append` stores `true` BEFORE dropping the sink (and thus `tx`); the writer,\n    /// on its `rx.recv()` Err, reads this and — when true — emits a\n    /// [`KIND_VIEWER_EVICTED`] marker (in the writer thread, OFF the log lock) so\n    /// the serving brain skips to live instead of tearing the attach down.\n    evicted: Arc<AtomicBool>,\n    /// The writer thread handle (held for ownership; it exits when `tx` drops or\n    /// a socket write fails).\n    _writer: JoinHandle<()>,\n}\n\n/// The controller live-frame channel depth (frames). Deeper than a viewer's\n/// queue because the controller is the AUTHORITATIVE view — unlike a viewer it is\n/// NOT evicted on a SINGLE full `try_send` (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).\n/// b4: the drain does ONE non-blocking `try_send` per chunk and NEVER blocks; a\n/// merely-slow controller DROPS chunks + falls behind the ring, and only a channel\n/// that stays full for [`CONTROLLER_WRITE_DEADLINE`] (tracked via\n/// [`ControllerSink::last_ok`]) is detached. Generous so a momentary slow consumer\n/// never trips the deadline.\nconst CONTROLLER_CHANNEL_DEPTH: usize = 4096;\n\n/// How long a CONTROLLER's channel may stay FULL (no successful handoff since\n/// [`ControllerSink::last_ok`]) before the drain declares it stalled and detaches it\n/// (W1, refined by b4 / REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE). This\n/// BOUNDS the wedge: a backed-up controller falls behind for at most this long before\n/// eviction — never forever (the v0.12.x root) — and the drain NEVER blocks or sleeps\n/// (a single off-lock `try_send` per chunk; see [`OutputLog::append`]). A live but\n/// merely slow operator lands a frame before this elapses (resetting `last_ok`) and is\n/// untouched.\nconst CONTROLLER_WRITE_DEADLINE: Duration = Duration::from_secs(5);\n\n/// How long a controller's writer thread may sit BLOCKED inside a single socket\n/// write — the brain subscriber suspended / black-holed — before a take/reattach\n/// or the `KIND_SESSIONS` reap stall-evicts it (LIFECYCLE-TRUTH W2,\n/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n/// protects the fan-out against ONE per-client conn falling behind and is\n/// output-driven (the drain observes a Full channel); THIS bound is measured\n/// full-with-**zero writer progress** and covers the case the output-driven path\n/// can't — an IDLE session whose brain-subscriber conn is suspended produces no\n/// output, so `append`/`mark_controller_gone` never runs, and the stalled\n/// controller would otherwise latch `driven_by` forever (reattach refused, `--take`\n/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n/// is bursty; 15 s tolerates a normal burst-stall while still bounding the wedge.\n/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nconst BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n\n/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n/// the slot) is provable in ~1 s instead of 15 (mirrors the\n/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n/// ONLY — never relaxes an assertion.\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nfn brain_write_deadline() -> Duration {\n    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n        Ok(ms) => ms\n            .parse::<u64>()\n            .map(Duration::from_millis)\n            .unwrap_or(BRAIN_WRITE_DEADLINE),\n        Err(_) => BRAIN_WRITE_DEADLINE,\n    }\n}\n\n/// Bounded wait for a concurrent wake's session to register before a second wake for\n/// the same endpoint stops deduping and spawns itself (W4, REQ-SPAWN-COLLISION-GUARD-\n/// LIVE-DUP). A racing broker spawn registers its session in PTY-spawn time (ms), so a\n/// loser waits for the winner's session then no-op acks it (exactly one launch tree);\n/// only a spawn that OVERRAN this ceiling (presumed dead, its claim leaked) is taken\n/// over — availability over a vanishingly rare dup.\nconst WAKE_DEDUP_WAIT: Duration = Duration::from_secs(2);\n/// Poll slice while waiting on a racing wake's session to appear.\nconst WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n\n/// RAII release of a single-flight wake claim (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP):\n/// the broker holds an endpoint id in [`Broker::wake_inflight`] for the duration of a\n/// spawn so a concurrent duplicate wake stands down; the guard releases it on EVERY\n/// exit (success, an early `?` spawn/drain error, or a panic), so a failed spawn never\n/// strands the endpoint permanently claimed.\n…\nimpl OutputLog {\n…\n    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\n    fn clear_controller(&mut self) {\n        if self.controller.take().is_some() {\n            self.stamp_driven_by();\n        }\n    }\n\n    /// B-2 (REQ-CONTROLLER-LIVENESS-REAP): lazily drop a controller whose WRITER\n    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\n    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\n    /// writer exits when a socket write fails (a severed / keepalive-killed controller\n    /// conn) or its `tx` drops; while it is parked on `rx.recv()` (a LIVE but idle\n    /// controller) it is NOT finished, so a live controller is never touched (the\n    /// no-false-reap guard). Reaping here makes the broker report honest truth so the\n    /// EXISTING [`converge_perch_stamps`] clears the now-stale info.json\n    /// `driven_by`/`controlled` stamp — no new detach path, no new query.\n    ///\n    /// LOCK-SAFE: this runs UNDER the log lock in the snapshot closure, so it must do\n    /// NO file I/O (the lock-across-effect discipline, KH 7.12/5.16). It drops the\n    /// sink in-memory ONLY (like [`become_controller`]'s prior-drop) and does NOT\n    /// [`stamp_driven_by`] — the OFF-lock `converge_perch_stamps` writes the honest\n    /// stamp. Returns true iff it reaped.\n    ///\n    /// SCOPE (doyle B-2 reduction): catches a writer that FAILED A WRITE (output was\n    /// flowing when the conn severed) or whose detach was dropped by a prior wedge\n    /// (the B-1 poison class). A TRULY IDLE severed controller (writer parked on recv,\n    /// no output, conn not yet EOF'd) stays `is_finished() == false` and converges\n    /// only once output resumes or the conn EOFs (→ `handle_conn` detach) — that\n    /// harder active-probe case is the reserved [`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`],\n    /// deliberately NOT built here. KH 7.15 held by construction: only ever CLEARS,\n    /// never latches `driven_by`.\n    // [impl->REQ-CONTROLLER-LIVENESS-REAP]\n    fn reap_dead_controller(&mut self) -> bool {\n        let dead = self\n            .controller\n            .as_ref()\n            .is_some_and(|c| c._writer.is_finished());\n        if dead {\n            // In-memory drop only (ends the — already-exited — writer); NO stamp here\n            // (info.json I/O under the log lock is the KH 7.12 hazard). converge does it.\n            self.controller = None;\n            return true;\n        }\n        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller whose\n        // writer is BLOCKED (not exited) on a suspended brain conn past\n        // BRAIN_WRITE_DEADLINE is also reaped here, so the honest `KIND_SESSIONS`\n        // truth + the off-lock converge_perch_stamps release its stale `driven_by`\n        // stamp even when nobody take/reattaches. Deferred stamp: the reap runs in\n        // the multi-session snapshot closure (no I/O under the shared sessions lock).\n        self.stall_evict_controller(Instant::now(), StampMode::Deferred)\n    }\n\n    /// Deadline-evict path (W1): the drain observed a fully-stalled controller and\n    /// timed out its `send_timeout`. Detach it — but ONLY if it is still the same\n    /// controller generation the drain saw (`epoch`), so a NEWER controller that\n    /// attached during the stall is never unseated.\n    fn mark_controller_gone(&mut self, epoch: u64) {\n        if self.controller_epoch.load(Ordering::Acquire) == epoch {\n            self.clear_controller();\n        }\n    }\n\n    /// Whether the current controller's writer is WEDGED — blocked inside a single\n    /// socket write for at least `deadline` (the brain-subscriber conn suspended /\n    /// black-holed). An IDLE controller (writer parked on `rx.recv()`, nothing to\n    /// write) reads NOT stalled: its [`ControllerSink::write_blocked_since`] is `None`.\n    /// Pure over the injected `now` + `deadline` so the unit tests it with no thread.\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn controller_write_stalled(&self, now: Instant, deadline: Duration) -> bool {\n        self.controller.as_ref().is_some_and(|c| {\n            c.write_blocked_since\n                .lock()\n                .ok()\n                .and_then(|g| *g)\n                .is_some_and(|since| now.saturating_duration_since(since) >= deadline)\n        })\n    }\n\n    /// 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    /// under the log lock via [`become_controller`]) or is deferred to the off-lock\n    /// `converge_perch_stamps` (the multi-session reap closure, KH 7.12 no-I/O rule).\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn stall_evict_controller(&mut self, now: Instant, stamp: StampMode) -> bool {\n        let deadline = brain_write_deadline();\n        if !self.controller_write_stalled(now, deadline) {\n            return false;\n        }\n        // Attribution (F-039 leg b): the evicted controller's stable conn id +\n        // stamps ride the record. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        let conn = self.controller.as_ref().map(|c| c.send.id()).unwrap_or(0);\n        eprintln!(\n            \"BRAIN_SUBSCRIBER_STALL_EVICT:{} conn={conn} {} — controller writer blocked > {}ms \\\n             on a suspended/black-holed brain conn; detached, driven_by released \\\n             [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\n            self.session_id,\n            crate::conn::log_stamp(),\n            deadline.as_millis()\n        );\n        record_stall_evict();\n        match stamp {\n            // Inline: drop the sink AND re-stamp the perch (become_controller's\n            // sibling — the take/reattach path is already I/O-under-log-lock).\n            StampMode::Inline => self.clear_controller(),\n            // Deferred: in-memory drop ONLY (the reap closure forbids I/O under the\n            // shared sessions lock); converge_perch_stamps writes the release off-lock.\n            StampMode::Deferred => self.controller = None,\n        }\n        true\n    }\n\n    /// Add a read-only viewer: snapshot the ring from `from_seq` into the writer\n    /// thread (NOT through the bounded live queue — replay must not trip the\n    /// overflow eviction), seed it the current size frame first, then run the\n    /// live queue. The drain thread fans live frames in via `try_send`.\n    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {\n        let vid = self.next_viewer_id;\n        self.next_viewer_id += 1;\n        // Attribution (F-039 legs b+d): viewer role fact + attach record.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        sub.describe(&format!(\n            \"viewer session={} endpoint={} vid={vid}\",\n            self.session_id,\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint }\n        ));\n        sub.lifecycle_event(\n            \"viewer-attach\",\n            &format!(\"session={} vid={vid} from_seq={from_seq}\", self.session_id),\n        );\n        // The size frame + the initial batch (owned by the thread, not the bounded\n        // queue). A COLD viewer attach (`from_seq == 0`) gets the #6 clean repaint;\n        // a skip-to-live / snap re-attach (`from_seq > 0`, incl. `u64::MAX`) keeps\n        // the raw-ring slice (`s >= from_seq`) — empty for skip-to-live, as before.\n        let mut initial: Vec<Envelope> =\n            vec![size_envelope(self.session_id, self.size.0, self.size.1)];\n        match self.repaint_initial(from_seq) {\n            Some((seq, bytes)) => initial.push(output_envelope(self.session_id, seq, &bytes)),\n            None => initial.extend(\n                self.ring\n                    .iter()\n                    .filter(|(s, _)| *s >= from_seq)\n                    .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),\n            ),\n        }\n        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);\n        let writer_send = Arc::clone(&sub);\n        let evicted = Arc::new(AtomicBool::new(false));\n        let writer_evicted = Arc::clone(&evicted);\n        let session_id = self.session_id;\n        let writer =\n            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));\n        self.viewers.insert(\n            vid,\n            ViewerSink {\n                tx,\n                send: sub,\n                evicted,\n                _writer: writer,\n            },\n        );\n    }\n\n    /// Resolve a controller/viewer subscribe under the W2.5 model\n    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs\n    /// the controller take / viewer add / loud displace as a side effect. See the\n    /// identity matrix in the body — the `by`-keyed logic is what makes a\n    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.\n    fn resolve_subscribe(\n        &mut self,\n        sub: SharedSend,\n        from_seq: u64,\n        intent: AttachIntent,\n        by: Option<String>,\n    ) -> SubscribeOutcome {\n        if intent == AttachIntent::Viewer {\n            if self.viewers.len() >= MAX_VIEWERS {\n                // Soft cap reached — refuse the viewer (bounds thread count).\n                return SubscribeOutcome::BusyControlled {\n                    by: \"viewer cap reached\".to_string(),\n                };\n            }\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        // Control or Take.\n        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller WEDGED\n        // on a suspended/black-holed brain conn past BRAIN_WRITE_DEADLINE is\n        // stall-evicted HERE — the take/reattach self-heals against broker truth\n        // without waiting for new output (the output-driven mark_controller_gone\n        // never fires on an idle session). Inline stamp: this path already writes the\n        // perch under the log lock via become_controller. After the evict the slot\n        // reads free and the normal free/take logic below runs.\n        self.stall_evict_controller(Instant::now(), StampMode::Inline);\n        if self.controller.is_none() {\n            // Free endpoint → become controller (nothing to displace).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        let same_identity = self.controller_by() == by.as_deref();\n        if same_identity {\n            // Same operator (or both local) re-serving — silent re-take. This is\n            // the gate-#7 path: a successor re-taking the slot after a brain\n            // restart NEVER fires Displaced.\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        if by.is_none() {\n            // The LOCAL brain re-attaching while a REMOTE controls — never steal;\n            // fall back to a silent viewer (gate #7 for the local-resume path).\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        if self.controller_by().is_none() {\n            // A remote takes an UNDRIVEN session (only the local spawner held the\n            // slot) — silent control, no loud displace (there is no remote driver\n            // to notify).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        // A DIFFERENT remote controls it.\n        match intent {\n            AttachIntent::Take => {\n                // The ONLY loud-displace path: notify the incumbent, then take.\n                // W1: route the Displaced notice through the OLD controller's\n                // writer thread (best-effort, off the dispatch thread) — the\n                // incumbent we're kicking may be exactly the wedged one, so an\n                // inline write here would block the new attacher's dispatch.\n                if let Some(old) = &self.controller {\n                    let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(\"\"));\n                    let _ = old.tx.try_send(CtrlMsg::Control(frame));\n                }\n                self.become_controller(sub, by, from_seq);\n                SubscribeOutcome::TookControl\n            }\n            _ => SubscribeOutcome::BusyControlled {\n                by: self.controller_by().unwrap_or_default().to_string(),\n            },\n        }\n    }\n\n    /// Update the stored PTY size and push a `size` frame to every viewer (the\n    /// letterbox feed). Called on a controller resize. Viewer pushes are\n    /// non-blocking with eviction, like [`OutputLog::append`].\n    fn set_size_and_notify(&mut self, rows: u16, cols: u16) {\n        self.size = (rows, cols);\n        // Keep the render grid geometry in step so a post-resize repaint reflects\n        // the new viewport (#8 resize-artifact fix, ADR-0031).\n        self.grid.resize(rows, cols);\n        if self.viewers.is_empty() {\n            return;\n        }\n        let frame = size_envelope(self.session_id, rows, cols);\n        let mut evicted: Vec<u64> = Vec::new();\n        for (vid, sink) in self.viewers.iter() {\n            if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {\n                evicted.push(*vid);\n            }\n        }\n        if !evicted.is_empty() {\n            for vid in evicted {\n                // Same overflow eviction as `append` — signal skip-to-live so the\n                // viewer re-subscribes rather than dying silently on a size push\n                // (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).\n                if let Some(sink) = self.viewers.remove(&vid) {\n                    sink.evicted.store(true, Ordering::Release);\n                }\n            }\n            self.stamp_viewer_count();\n        }\n    }\n\n    /// Detach `sub` from whatever role it held: clear the controller slot iff it\n    /// is the current controller (a no-op if a newer brain took over — a late\n    /// disconnect can't unseat the live controller), and remove any viewer rows\n    /// owned by `sub`. Leaves `delivered_through` intact (D4-1). Re-stamps\n    /// `driven_by` / `viewer_count` when a role actually changed.\n    fn detach_if(&mut self, sub: &SharedSend) {\n        let is_ctrl = self\n            .controller\n            .as_ref()\n            .is_some_and(|c| Arc::ptr_eq(&c.send, sub));\n        if is_ctrl {\n            self.clear_controller(); // drops the sink (ends its writer) + unlatches\n        }\n        let before = self.viewers.len();\n        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));\n        // Bounded detach record (F-039 leg d) — only when a role actually changed.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        if is_ctrl || self.viewers.len() != before {\n            sub.lifecycle_event(\n                \"session-detach\",\n                &format!(\n                    \"session={} was_controller={is_ctrl} viewers_dropped={}\",\n                    self.session_id,\n                    before - self.viewers.len()\n                ),\n            );\n        }\n        if self.viewers.len() != before {\n            self.stamp_viewer_count();\n        }\n    }\n\n    /// Every sink currently attached (controller + viewers) — the recipients of\n    /// an `exit` frame.\n    fn all_sinks(&self) -> Vec<SharedSend> {\n        let mut v: Vec<SharedSend> = Vec::new();\n        if let Some(c) = &self.controller {\n            v.push(Arc::clone(&c.send));\n        }\n        for sink in self.viewers.values() {\n            v.push(Arc::clone(&sink.send));\n        }\n        v\n    }\n\n    /// Stamp the perch's `driven_by` to the current controller's identity (the\n    /// broker is the single writer — resolves the clear-race). The remote-drive\n    /// detection fact (REQ-REACH-1) moved here from `serve_attach` so a displaced\n    /// operator's teardown can never wipe the new controller's marker.\n    /// Best-effort: a session served without a perch (raw broker tests) has\n    /// nowhere to record.\n    // [impl->REQ-RCVIEW-1] [impl->REQ-REACH-1]\n    fn stamp_driven_by(&self) {\n        if self.endpoint.is_empty() {\n            return;\n        }\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\n        // `driven_by` is REMOTE-only: `controller_by()` is `None` for a local\n        // controller (its `by` is `None`), and it MUST stay that way — a local\n        // controller latching driven_by trips REQ-HAZARD-DRIVEN-BY-SELFHEAL (KH 7.15).\n        let _ = spt_store::info::set_driven_by(&perch, self.controller_by());\n        // #3 (REQ-GOSSIP-CONTROLLED-ANY): stamp a SEPARATE any-controller datum so a\n        // LOCALLY-driven endpoint (driven_by = None) still gossips as controlled to\n        // remote viewers. `controller_by` answers \"is a remote node driving me\";\n        // `controller.is_some()` answers \"is ANYONE driving me\".\n        // [impl->REQ-GOSSIP-CONTROLLED-ANY]\n        let _ = spt_store::info::set_controlled(&perch, self.controller.is_some());\n    }\n\n    /// Stamp the perch's `viewer_count` to the live viewer count (broker is the\n    /// single writer). Best-effort, like [`OutputLog::stamp_driven_by`].\n    // [impl->REQ-RCVIEW-1]\n    fn stamp_viewer_count(&self) {\n        if self.endpoint.is_empty() {\n            return;\n        }\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\n        let _ = spt_store::info::set_viewer_count(&perch, self.viewers.len() as u32);\n    }\n\n    /// Reap-time unlatch: a control/viewer stamp never outlives its session\n    /// (REQ-HAZARD-CONTROL-STAMP-LIFETIME). `/exit` kills the CHILD, not the\n    /// controller connection, so the OutputLog drops with the controller slot\n    /// still occupied — [`clear_controller`]'s detach path never runs, and\n    /// `controlled` / `driven_by` / `viewer_count` would otherwise latch in\n    /// info.json forever (and gossip a stale `controller_node` cross-node). The\n    /// exit-waiter calls this on reap so every teardown path clears what attach\n    /// stamped. Unconditional (idempotent when already clear) — the session is\n    /// dead, so there is no live controller to re-stamp concurrently (same\n    /// race-free argument as the W5 no-session self-heal). Best-effort, like the\n    /// sibling stampers; empty endpoint ⇒ nowhere to record.\n    // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n    fn stamp_reaped(&self) {\n        if self.endpoint.is_empty() {\n            return;\n        }\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\n        let _ = spt_store::info::set_driven_by(&perch, None);\n        let _ = spt_store::info::set_controlled(&perch, false);\n        let _ = spt_store::info::set_viewer_count(&perch, 0);\n    }\n\n    /// Whether `sub` is the current controller (the resize-exclusivity check).\n    fn is_controller(&self, sub: &SharedSend) -> bool {\n        self.controller\n            .as_ref()\n            .is_some_and(|c| Arc::ptr_eq(&c.send, sub))\n    }\n\n    /// The next seq a re-attaching controller still needs (the persisted resume\n    /// cursor surfaced in `KIND_SESSIONS` → `resume_seq`). W1: read from the\n    /// atomic the controller writer thread advances.\n    fn delivered_through(&self) -> u64 {\n        self.delivered_through.load(Ordering::Acquire)\n    }\n}\n\n/// A viewer's dedicated writer thread (REQ-HAZARD-VIEWER-ISOLATION): write the\n/// `initial` batch (size frame + ring replay) once, then drain live frames from\n/// `rx` with a BLOCKING socket write each. A failed write (dead viewer) ends the\n/// thread; the drain thread's next `try_send` then sees `Disconnected` and\n/// evicts the row. The drain thread itself never blocks on this socket.\nfn viewer_writer(\n    send: SharedSend,\n    session_id: u64,\n    initial: Vec<Envelope>,\n    rx: Receiver<Envelope>,\n    evicted: Arc<AtomicBool>,\n) {\n    // Bounded writer-exit record on every return path (F-039 leg d) — RAII so\n    // the reason is stamped exactly once however the thread ends.\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    struct ExitNote {\n        send: SharedSend,\n        session_id: u64,\n        reason: &'static str,\n    }\n    impl Drop for ExitNote {\n        fn drop(&mut self) {\n            self.send.lifecycle_event(\n                \"writer-exit\",\n                &format!(\"role=viewer session={} reason={}\", self.session_id, self.reason),\n            );\n        }\n    }\n    let mut note = ExitNote {\n        send: Arc::clone(&send),\n        session_id,\n        reason: \"write-failed\",\n    };\n    for frame in &initial {\n        // Bounded, cancelable conn write (round-4): a viewer that stops\n        // reading poisons + retires ITS conn at the deadline instead of\n        // parking this thread in the OS write forever.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        if !matches!(send.write(frame), Ok(ConnWrite::Done)) {\n            return;\n        }\n    }\n    while let Ok(frame) = rx.recv() {\n        if !matches!(send.write(&frame), Ok(ConnWrite::Done)) {\n            return;\n        }\n    }\n    note.reason = \"channel-closed\";\n    // `tx` dropped. If this was an EVICTION for overflow (append set the flag\n    // before dropping the sink), emit a KIND_VIEWER_EVICTED marker — DISTINCT from\n…\n}","startLine":80,"lineNumbers":[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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,null,678,null,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,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,null,1431]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"}}},"isError":false,"timestamp":1784195763395}}
{"type":"message","id":"841cdd98","parentId":"898963f5","timestamp":"2026-07-16T09:56:03.403Z","message":{"role":"toolResult","toolCallId":"call_OecapSYePAMofRmv04AIZQ3L|fc_008390f508d1d09b016a58aab33764819a8e22f3e26fa782bc","toolName":"read","content":[{"type":"text","text":"\n/// Read one [`BrokerEvent`] for an attached CONTROLLER, RESUMING-FROM-FLOOR on a\n/// b4 drop-don't-block output-gap instead of fataling.\n///\n/// b4 made the controller a non-blocking `try_send` that DROPS frames when its\n/// bounded channel is full (a controller that falls behind its own echo under a\n/// hard flood). The next frame this reader sees is then a FORWARD gap that the\n/// strict reject-gap (`read_event`, legacy single-session path) FATALS. A\n/// controller CANNOT snap-above like a viewer (B2: it is authoritative — it\n/// advances `delivered_through`; skipping rolled frames would be a not-exactly-once\n/// resume). So instead it RE-FETCHES: re-subscribe from\n/// [`Brain::controller_resume_floor`] (`= delivered_through`, frozen at the drop by\n/// `contiguous_advance`) and the broker replays the dropped frames from the ring.\n///\n/// Exactly-once holds IFF the ring still retains the floor (`tail - delivered_through\n/// <= cap_chunks`). If it does not — the controller fell behind further than the\n/// ring holds — the re-subscribe's first frame is STILL a forward gap (floor\n/// unchanged across two resumes). That is [`ControllerIrrecoverablyBehind`]: the\n/// caller surfaces it (marked truncation), never silent-skips (B2 lie) and never\n/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\n/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\n/// Size / Exit / …) pass through unchanged.\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\npub fn read_controller_event_resuming(\n    brain: &mut Brain,\n    session_id: u64,\n    by: Option<&str>,\n) -> io::Result<BrokerEvent> {\n    let mut last_resume_floor: Option<u64> = None;\n    let mut swallow_resume_ack = false;\n    loop {\n        match brain.read_event() {\n            // Our own re-subscribe's Subscribed reply: swallow it (role is already\n            // resolved) so it cannot mask a repeated gap as forward progress.\n            Ok(BrokerEvent::Subscribed { .. }) if swallow_resume_ack => {\n                swallow_resume_ack = false;\n                continue;\n            }\n            Ok(ev) => return Ok(ev),\n            Err(e) if is_output_gap(&e) => {\n                let floor = brain.controller_resume_floor();\n                if last_resume_floor == Some(floor) {\n                    // No progress across two resumes → the ring rolled past the\n                    // floor → the dropped frames are gone.\n                    return Err(io::Error::other(ControllerIrrecoverablyBehind { floor }));\n                }\n                last_resume_floor = Some(floor);\n                // RE-FETCH from the frozen delivered_through (same operator `by` →\n                // a silent re-take, no Displaced); the broker replays the dropped\n                // frames from the ring.\n                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\n                swallow_resume_ack = true;\n            }\n            Err(e) => return Err(e),\n        }\n    }\n}\n\n/// Resolve an endpoint label to THIS node's local broker session id (the spawn's\n/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\n/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\n/// its own session table, so a remote operator — which cannot know our local\n/// session id — names the endpoint and we map it here, never trusting a wire\n/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n/// (a stale registry row routed us an attach for something gone → the caller\n/// refuses cleanly, D6).\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n    brain.sessions().ok().and_then(|reply| {\n        reply\n            .sessions\n            .into_iter()\n            .find(|s| s.endpoint == endpoint)\n            .map(|s| s.session_id)\n    })\n}\n\n/// Serve one attach stream against this brain's broker, blocking until the\n/// session exits, the operator detaches, or the gate refuses.\n///\n/// `origin_node` MUST be the handshake-proven remote id from the broker's\n/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes\n/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\n/// is being attached, for the gate. `net_from_seq` is this brain's resume\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n/// journal, and re-pumped output dedups at the operator's render cursor).\n///\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n/// round-trip inside the event loop would swallow interleaved attach records\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\n/// the wire `session_id`.\n// [impl->REQ-INST-8]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub fn serve_attach(\n    brain: &mut Brain,\n    broker_name: &str,\n    stream_id: u64,\n    endpoint: &str,\n    origin_node: &str,\n    net_from_seq: u64,\n    remote_session: Option<u64>,\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    // 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    // 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    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 = match (role, attached_session) {\n            (Some(ServeRole::Controller), Some(sid)) => {\n                match read_controller_event_resuming(brain, sid, 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                        let _ = wire.net_stream_send(stream_id, &line, None, true);\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                    Err(e) => {\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                }\n            }\n            _ => brain.read_event()?,\n        };\n        match event {\n            // ── Wire → session: requests, input, resize. ───────────────────\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Request {\n                            session_id,\n                            from_seq,\n                            intent: req_intent,\n                            // The dispatcher already resolved endpoint_id→session\n                            // (passed as remote_session) BEFORE this loop; ignore\n                            // the wire copy here (a mid-loop re-resolve would swallow\n                            // interleaved attach records — see remote_session docs).\n                            endpoint_id: _,\n                        } => {\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\n                            // the transport-proven origin. Viewing is gated\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\n                            // watching reveals full session contents).\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\n                                == AccessDecision::Deny\n                            {\n                                wire.net_stream_send(stream_id, &[], None, true)?;\n                                return Ok(AttachServeOutcome::Refused);\n                            }\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\n                            // dispatcher-resolved session id (the owning node is\n                            // authoritative for its own table); the LOCAL path uses\n                            // the wire session_id unchanged.\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\n                            let sid = remote_session.unwrap_or(session_id);\n                            intent = req_intent;\n                            attached_session = Some(sid);\n                            // Role-aware subscribe (REQ-RCVIEW-1): the broker\n                            // resolves controller / viewer / busy / take by the\n                            // intent + this handshake-proven origin, and replies a\n                            // `Subscribed`. driven_by / viewer_count are now\n                            // BROKER-owned (single writer — resolves the\n                            // clear-race), so this loop no longer stamps them.\n                            // A VIEWER attach ARMS snap-above at the floor so a\n                            // PRE-eviction ring-roll forward Output gap (the ring\n                            // rolled frames out under flood with NO eviction\n                            // marker) SNAPS to live instead of fataling the\n                            // legacy reject-gap → the viewer keeps forwarding.\n                            // The CONTROLLER keeps strict reject-gap (exactly-once\n                            // resume, B2). Composes with the POST-eviction\n                            // ViewerEvicted skip-to-live arm below.\n                            // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\n                            if req_intent == AttachIntent::Viewer {\n                                brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\n                            } else {\n                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\n                            }\n                            attached = true;\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                                    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                            // Controller → PTY resize (controller-EXCLUSIVE; the\n                            // broker rejects it on a viewer's stream). A viewer's\n                            // Resize is dropped here by the intent gate too.\n                            let _ = brain.resize(rows, cols);\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, .. } => {\n                use crate::msg::SubscribeOutcome as O;\n                match outcome {\n                    O::Controller | O::TookControl => {\n                        role = Some(ServeRole::Controller);\n                        // A driver attached — the wake edge (controller-only;\n                        // idempotent on an already-active seat).\n                        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                        for (op, b) in pending_input.drain(..) {\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\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            }\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\n            BrokerEvent::Size { rows, cols, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\n                wire.net_stream_send(stream_id, &line, None, false)?;\n            }\n            // This controller was kicked by a different operator's `--take`.\n            BrokerEvent::Displaced { by, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\n                wire.net_stream_send(stream_id, &line, None, true)?;\n                // No dormant edge: the endpoint stays driven by the taker (whose\n                // own serve loop already fired the wake).\n                return Ok(AttachServeOutcome::Displaced);\n            }\n\n            // This VIEWER overflowed its broker subscription queue and was evicted\n            // (it forwards slower than the drain fans out under flood). SKIP TO\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\n            // spin. The next live Output carries a forward seq jump; the operator's\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n            BrokerEvent::ViewerEvicted { session_id: sid }\n                if attached && Some(sid) == attached_session =>\n            {\n                if let Some(prev) = last_resubscribe {\n                    let since = prev.elapsed();\n                    if since < RESUBSCRIBE_INTERVAL {\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\n                    }\n                }\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\n                last_resubscribe = Some(Instant::now());\n            }\n\n            // The operator closed its send side: the viewport is over.\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                wire.net_stream_send(stream_id, &[], None, true)?;\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\n                // info.json, carrying the current driven_by through), THEN release\n                // the role over IPC so the broker's driven_by=None is the LAST\n                // write — otherwise a concurrent resting RMW restores the stale\n                // marker (the clear-race the broker-single-writer model averts).\n                if role == Some(ServeRole::Controller) {\n                    // The driver detached — the endpoint rests warm (controller-\n                    // only; a viewer leaving is wake-neutral).\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                if let Some(sid) = attached_session {\n                    let _ = brain.detach_session(sid); // release the role (clears markers)\n                }\n                return Ok(AttachServeOutcome::Detached);\n            }\n\n            // ── Session → wire: output and exit. ────────────────────────────\n            BrokerEvent::Output { seq, bytes, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Output {\n                    seq,\n                    data_b64: encode_bytes(&bytes),\n                });\n                // Unjournaled by design (see module docs): `seq` rides inside\n                // the record, and the operator's render cursor is the dedup.\n                wire.net_stream_send(stream_id, &line, None, false)?;\n            }\n            BrokerEvent::Exit { code, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Exit { code });\n                wire.net_stream_send(stream_id, &line, None, true)?;\n                if role == Some(ServeRole::Controller) {\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                return Ok(AttachServeOutcome::Exited(code));\n            }\n\n            BrokerEvent::Error { message } => {\n                if role == Some(ServeRole::Controller) {\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                return Err(io::Error::other(message));\n            }\n            // Applied/NetSent acks and unrelated events: not ours to handle.\n            _ => {}\n        }\n    }\n}\n\n/// Operator-side: open an attach stream on a dialed connection and send the\n/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\n/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\n/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\n/// durable id for the exactly-once stream open (safe to block for its ack —\n/// no attach traffic can be in flight before the stream exists); the Request\n/// send itself is unjournaled (see module docs — a replayed Request re-gates\n/// and re-attaches idempotently).\n// [impl->REQ-INST-8]\npub fn request_attach(\n…\n) -> io::Result<u64> {\n…\n}\n\n[Showing lines 135-553 of 627. Use :554 to continue]"}],"details":{"truncation":{"content":"\r\n/// Read one [`BrokerEvent`] for an attached CONTROLLER, RESUMING-FROM-FLOOR on a\r\n/// b4 drop-don't-block output-gap instead of fataling.\r\n///\r\n/// b4 made the controller a non-blocking `try_send` that DROPS frames when its\r\n/// bounded channel is full (a controller that falls behind its own echo under a\r\n/// hard flood). The next frame this reader sees is then a FORWARD gap that the\r\n/// strict reject-gap (`read_event`, legacy single-session path) FATALS. A\r\n/// controller CANNOT snap-above like a viewer (B2: it is authoritative — it\r\n/// advances `delivered_through`; skipping rolled frames would be a not-exactly-once\r\n/// resume). So instead it RE-FETCHES: re-subscribe from\r\n/// [`Brain::controller_resume_floor`] (`= delivered_through`, frozen at the drop by\r\n/// `contiguous_advance`) and the broker replays the dropped frames from the ring.\r\n///\r\n/// Exactly-once holds IFF the ring still retains the floor (`tail - delivered_through\r\n/// <= cap_chunks`). If it does not — the controller fell behind further than the\r\n/// ring holds — the re-subscribe's first frame is STILL a forward gap (floor\r\n/// unchanged across two resumes). That is [`ControllerIrrecoverablyBehind`]: the\r\n/// caller surfaces it (marked truncation), never silent-skips (B2 lie) and never\r\n/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\r\n/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\r\n/// Size / Exit / …) pass through unchanged.\r\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\npub fn read_controller_event_resuming(\r\n    brain: &mut Brain,\r\n    session_id: u64,\r\n    by: Option<&str>,\r\n) -> io::Result<BrokerEvent> {\r\n    let mut last_resume_floor: Option<u64> = None;\r\n    let mut swallow_resume_ack = false;\r\n    loop {\r\n        match brain.read_event() {\r\n            // Our own re-subscribe's Subscribed reply: swallow it (role is already\r\n            // resolved) so it cannot mask a repeated gap as forward progress.\r\n            Ok(BrokerEvent::Subscribed { .. }) if swallow_resume_ack => {\r\n                swallow_resume_ack = false;\r\n                continue;\r\n            }\r\n            Ok(ev) => return Ok(ev),\r\n            Err(e) if is_output_gap(&e) => {\r\n                let floor = brain.controller_resume_floor();\r\n                if last_resume_floor == Some(floor) {\r\n                    // No progress across two resumes → the ring rolled past the\r\n                    // floor → the dropped frames are gone.\r\n                    return Err(io::Error::other(ControllerIrrecoverablyBehind { floor }));\r\n                }\r\n                last_resume_floor = Some(floor);\r\n                // RE-FETCH from the frozen delivered_through (same operator `by` →\r\n                // a silent re-take, no Displaced); the broker replays the dropped\r\n                // frames from the ring.\r\n                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\r\n                swallow_resume_ack = true;\r\n            }\r\n            Err(e) => return Err(e),\r\n        }\r\n    }\r\n}\r\n\r\n/// Resolve an endpoint label to THIS node's local broker session id (the spawn's\r\n/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\r\n/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\r\n/// its own session table, so a remote operator — which cannot know our local\r\n/// session id — names the endpoint and we map it here, never trusting a wire\r\n/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\r\n/// (a stale registry row routed us an attach for something gone → the caller\r\n/// refuses cleanly, D6).\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\r\n    brain.sessions().ok().and_then(|reply| {\r\n        reply\r\n            .sessions\r\n            .into_iter()\r\n            .find(|s| s.endpoint == endpoint)\r\n            .map(|s| s.session_id)\r\n    })\r\n}\r\n\r\n/// Serve one attach stream against this brain's broker, blocking until the\r\n/// session exits, the operator detaches, or the gate refuses.\r\n///\r\n/// `origin_node` MUST be the handshake-proven remote id from the broker's\r\n/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes\r\n/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\r\n/// is being attached, for the gate. `net_from_seq` is this brain's resume\r\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\r\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\r\n/// journal, and re-pumped output dedups at the operator's render cursor).\r\n///\r\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\r\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\r\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\r\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\r\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\r\n/// round-trip inside the event loop would swallow interleaved attach records\r\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\r\n/// the wire `session_id`.\r\n// [impl->REQ-INST-8]\r\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub fn serve_attach(\r\n    brain: &mut Brain,\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    endpoint: &str,\r\n    origin_node: &str,\r\n    net_from_seq: u64,\r\n    remote_session: Option<u64>,\r\n) -> io::Result<AttachServeOutcome> {\r\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\r\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\r\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\r\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\r\n    // session output (broker→serve) — including the controller's synchronous,\r\n    // inline, multi-frame ring replay (`become_controller`), which holds the\r\n    // broker's single-threaded per-conn handler for the whole replay. If the\r\n    // forwards rode `brain` too, that stuck handler could not read them →\r\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\r\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\r\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\r\n    // two directions on independent single-threaded handlers cannot block each\r\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\r\n    // a second conn is correct. Order is preserved: this one serve thread reads\r\n    // every Output in seq order on `brain` and forwards it in that same order on\r\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\r\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\r\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\r\n    // the controller onto that async path would remove the inline-replay block at\r\n    // the source, but it touches the authoritative cursor-advancing path.)\r\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut attached = false;\r\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\r\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\r\n    // race on the input gate).\r\n    let mut intent = AttachIntent::Control;\r\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\r\n    // controller input that races ahead of the reply is BUFFERED, not dropped\r\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\r\n    // we know we control the session (the busy-refuse case).\r\n    let mut role: Option<ServeRole> = None;\r\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\r\n    // The attached session — captured from the Request so the terminal paths can\r\n    // explicitly release this worker's role (clearing driven_by / viewer_count\r\n    // promptly, not only on connection drop).\r\n    let mut attached_session: Option<u64> = None;\r\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\r\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\r\n    // intermittent live bursts, never a CPU spin.\r\n    let mut last_resubscribe: Option<Instant> = None;\r\n\r\n    loop {\r\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\r\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\r\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\r\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\r\n        // role resolves no Output flows, so a plain read is correct then.\r\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\n        let event = match (role, attached_session) {\r\n            (Some(ServeRole::Controller), Some(sid)) => {\r\n                match read_controller_event_resuming(brain, sid, Some(origin_node)) {\r\n                    Ok(ev) => ev,\r\n                    Err(e)\r\n                        if e.get_ref()\r\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\r\n                    {\r\n                        // Irrecoverable: the dropped frames rolled out of the ring.\r\n                        // SURFACE a marked truncation to the operator (never a\r\n                        // silent skip = B2 lie, never a spin), then end cleanly.\r\n                        // Graceful continuation is deferred\r\n                        // (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND).\r\n                        let notice =\r\n                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\r\n                        let line = ndjson::encode_line(&AttachRecord::Output {\r\n                            seq: brain.controller_resume_floor(),\r\n                            data_b64: encode_bytes(notice),\r\n                        });\r\n                        let _ = wire.net_stream_send(stream_id, &line, None, true);\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                        return Err(e);\r\n                    }\r\n                    Err(e) => {\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                        return Err(e);\r\n                    }\r\n                }\r\n            }\r\n            _ => brain.read_event()?,\r\n        };\r\n        match event {\r\n            // ── Wire → session: requests, input, resize. ───────────────────\r\n            BrokerEvent::NetStreamData {\r\n                stream_id: sid,\r\n                bytes,\r\n                ..\r\n            } if sid == stream_id => {\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                            intent: req_intent,\r\n                            // The dispatcher already resolved endpoint_id→session\r\n                            // (passed as remote_session) BEFORE this loop; ignore\r\n                            // the wire copy here (a mid-loop re-resolve would swallow\r\n                            // interleaved attach records — see remote_session docs).\r\n                            endpoint_id: _,\r\n                        } => {\r\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\r\n                            // the transport-proven origin. Viewing is gated\r\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\r\n                            // watching reveals full session contents).\r\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\r\n                                == AccessDecision::Deny\r\n                            {\r\n                                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                                return Ok(AttachServeOutcome::Refused);\r\n                            }\r\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\r\n                            // dispatcher-resolved session id (the owning node is\r\n                            // authoritative for its own table); the LOCAL path uses\r\n                            // the wire session_id unchanged.\r\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n                            let sid = remote_session.unwrap_or(session_id);\r\n                            intent = req_intent;\r\n                            attached_session = Some(sid);\r\n                            // Role-aware subscribe (REQ-RCVIEW-1): the broker\r\n                            // resolves controller / viewer / busy / take by the\r\n                            // intent + this handshake-proven origin, and replies a\r\n                            // `Subscribed`. driven_by / viewer_count are now\r\n                            // BROKER-owned (single writer — resolves the\r\n                            // clear-race), so this loop no longer stamps them.\r\n                            // A VIEWER attach ARMS snap-above at the floor so a\r\n                            // PRE-eviction ring-roll forward Output gap (the ring\r\n                            // rolled frames out under flood with NO eviction\r\n                            // marker) SNAPS to live instead of fataling the\r\n                            // legacy reject-gap → the viewer keeps forwarding.\r\n                            // The CONTROLLER keeps strict reject-gap (exactly-once\r\n                            // resume, B2). Composes with the POST-eviction\r\n                            // ViewerEvicted skip-to-live arm below.\r\n                            // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\r\n                            if req_intent == AttachIntent::Viewer {\r\n                                brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\r\n                            } else {\r\n                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\r\n                            }\r\n                            attached = true;\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                            match role {\r\n                                // Confirmed controller — exactly-once at the PTY\r\n                                // (the operator-minted id rides the B5 journal). NO-ACK\r\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\r\n                                // fire-and-forward (the ack is discarded here) and can\r\n                                // flood N input frames on this one conn; an acked write\r\n                                // back would fill the return direction and deadlock the\r\n                                // broker's per-conn handler. Dedup still holds at the\r\n                                // broker (op_id applied-set).\r\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n                                Some(ServeRole::Controller) => {\r\n                                    // The op_id is operator-minted (rc attach OpMinter),\r\n                                    // so it carries the `rc` journal tag (ADR-0034).\r\n                                    brain.send_effect_no_ack(\r\n                                        MintedOp::new(Minter::Rc, op_id),\r\n                                        &bytes,\r\n                                    )?\r\n                                }\r\n                                // Outcome not yet known — buffer; flushed on a\r\n                                // controller confirmation, discarded on busy/viewer.\r\n                                None => pending_input.push((op_id, bytes)),\r\n                                // A viewer never reaches here (intent gate).\r\n                                Some(ServeRole::Viewer) => {}\r\n                            }\r\n                        }\r\n                        AttachRecord::Resize { rows, cols }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\r\n                            // broker rejects it on a viewer's stream). A viewer's\r\n                            // Resize is dropped here by the intent gate too.\r\n                            let _ = brain.resize(rows, cols);\r\n                        }\r\n                        // Input/Resize before Request, a viewer's input/resize,\r\n                        // or target-direction records echoed back: noise — ignore.\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n\r\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\r\n            BrokerEvent::Subscribed { outcome, .. } => {\r\n                use crate::msg::SubscribeOutcome as O;\r\n                match outcome {\r\n                    O::Controller | O::TookControl => {\r\n                        role = Some(ServeRole::Controller);\r\n                        // A driver attached — the wake edge (controller-only;\r\n                        // idempotent on an already-active seat).\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\r\n                        // Flush any input that raced ahead of the reply (operator-\r\n                        // minted ops carry the `rc` tag).\r\n                        for (op, b) in pending_input.drain(..) {\r\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\r\n                        }\r\n                    }\r\n                    O::Viewer => {\r\n                        role = Some(ServeRole::Viewer);\r\n                        // Wake-neutral: a viewer never wakes the endpoint, never\r\n                        // keeps it awake (Gap B). Discard any racing input.\r\n                        pending_input.clear();\r\n                    }\r\n                    O::BusyControlled { .. } => {\r\n                        // A different operator controls it — refuse (no PTY input\r\n                        // ever reached it; the buffer is discarded). The rc\r\n                        // renders the busy guidance client-side from driven_by.\r\n                        wire.net_stream_send(stream_id, &[], None, true)?;\r\n                        return Ok(AttachServeOutcome::Refused);\r\n                    }\r\n                }\r\n            }\r\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\r\n            BrokerEvent::Size { rows, cols, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            // This controller was kicked by a different operator's `--take`.\r\n            BrokerEvent::Displaced { by, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                // No dormant edge: the endpoint stays driven by the taker (whose\r\n                // own serve loop already fired the wake).\r\n                return Ok(AttachServeOutcome::Displaced);\r\n            }\r\n\r\n            // This VIEWER overflowed its broker subscription queue and was evicted\r\n            // (it forwards slower than the drain fans out under flood). SKIP TO\r\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\r\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\r\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\r\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\r\n            // spin. The next live Output carries a forward seq jump; the operator's\r\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\r\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\r\n            BrokerEvent::ViewerEvicted { session_id: sid }\r\n                if attached && Some(sid) == attached_session =>\r\n            {\r\n                if let Some(prev) = last_resubscribe {\r\n                    let since = prev.elapsed();\r\n                    if since < RESUBSCRIBE_INTERVAL {\r\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\r\n                    }\r\n                }\r\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\r\n                last_resubscribe = Some(Instant::now());\r\n            }\r\n\r\n            // The operator closed its send side: the viewport is over.\r\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\r\n                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\r\n                // info.json, carrying the current driven_by through), THEN release\r\n                // the role over IPC so the broker's driven_by=None is the LAST\r\n                // write — otherwise a concurrent resting RMW restores the stale\r\n                // marker (the clear-race the broker-single-writer model averts).\r\n                if role == Some(ServeRole::Controller) {\r\n                    // The driver detached — the endpoint rests warm (controller-\r\n                    // only; a viewer leaving is wake-neutral).\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                if let Some(sid) = attached_session {\r\n                    let _ = brain.detach_session(sid); // release the role (clears markers)\r\n                }\r\n                return Ok(AttachServeOutcome::Detached);\r\n            }\r\n\r\n            // ── Session → wire: output and exit. ────────────────────────────\r\n            BrokerEvent::Output { seq, bytes, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Output {\r\n                    seq,\r\n                    data_b64: encode_bytes(&bytes),\r\n                });\r\n                // Unjournaled by design (see module docs): `seq` rides inside\r\n                // the record, and the operator's render cursor is the dedup.\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            BrokerEvent::Exit { code, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Exit { code });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Ok(AttachServeOutcome::Exited(code));\r\n            }\r\n\r\n            BrokerEvent::Error { message } => {\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Err(io::Error::other(message));\r\n            }\r\n            // Applied/NetSent acks and unrelated events: not ours to handle.\r\n            _ => {}\r\n        }\r\n    }\r\n}\r\n\r\n/// Operator-side: open an attach stream on a dialed connection and send the\r\n/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\r\n/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\r\n/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\r\n/// durable id for the exactly-once stream open (safe to block for its ack —\r\n/// no attach traffic can be in flight before the stream exists); the Request\r\n/// send itself is unjournaled (see module docs — a replayed Request re-gates\r\n/// and re-attaches idempotently).\r\n// [impl->REQ-INST-8]\r\npub fn request_attach(\r","truncated":true,"truncatedBy":"lines","totalLines":493,"totalBytes":24662,"outputLines":419,"outputBytes":24662,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"\r\n/// Read one [`BrokerEvent`] for an attached CONTROLLER, RESUMING-FROM-FLOOR on a\r\n/// b4 drop-don't-block output-gap instead of fataling.\r\n///\r\n/// b4 made the controller a non-blocking `try_send` that DROPS frames when its\r\n/// bounded channel is full (a controller that falls behind its own echo under a\r\n/// hard flood). The next frame this reader sees is then a FORWARD gap that the\r\n/// strict reject-gap (`read_event`, legacy single-session path) FATALS. A\r\n/// controller CANNOT snap-above like a viewer (B2: it is authoritative — it\r\n/// advances `delivered_through`; skipping rolled frames would be a not-exactly-once\r\n/// resume). So instead it RE-FETCHES: re-subscribe from\r\n/// [`Brain::controller_resume_floor`] (`= delivered_through`, frozen at the drop by\r\n/// `contiguous_advance`) and the broker replays the dropped frames from the ring.\r\n///\r\n/// Exactly-once holds IFF the ring still retains the floor (`tail - delivered_through\r\n/// <= cap_chunks`). If it does not — the controller fell behind further than the\r\n/// ring holds — the re-subscribe's first frame is STILL a forward gap (floor\r\n/// unchanged across two resumes). That is [`ControllerIrrecoverablyBehind`]: the\r\n/// caller surfaces it (marked truncation), never silent-skips (B2 lie) and never\r\n/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\r\n/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\r\n/// Size / Exit / …) pass through unchanged.\r\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\npub fn read_controller_event_resuming(\r\n    brain: &mut Brain,\r\n    session_id: u64,\r\n    by: Option<&str>,\r\n) -> io::Result<BrokerEvent> {\r\n    let mut last_resume_floor: Option<u64> = None;\r\n    let mut swallow_resume_ack = false;\r\n    loop {\r\n        match brain.read_event() {\r\n            // Our own re-subscribe's Subscribed reply: swallow it (role is already\r\n            // resolved) so it cannot mask a repeated gap as forward progress.\r\n            Ok(BrokerEvent::Subscribed { .. }) if swallow_resume_ack => {\r\n                swallow_resume_ack = false;\r\n                continue;\r\n            }\r\n            Ok(ev) => return Ok(ev),\r\n            Err(e) if is_output_gap(&e) => {\r\n                let floor = brain.controller_resume_floor();\r\n                if last_resume_floor == Some(floor) {\r\n                    // No progress across two resumes → the ring rolled past the\r\n                    // floor → the dropped frames are gone.\r\n                    return Err(io::Error::other(ControllerIrrecoverablyBehind { floor }));\r\n                }\r\n                last_resume_floor = Some(floor);\r\n                // RE-FETCH from the frozen delivered_through (same operator `by` →\r\n                // a silent re-take, no Displaced); the broker replays the dropped\r\n                // frames from the ring.\r\n                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\r\n                swallow_resume_ack = true;\r\n            }\r\n            Err(e) => return Err(e),\r\n        }\r\n    }\r\n}\r\n\r\n/// Resolve an endpoint label to THIS node's local broker session id (the spawn's\r\n/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\r\n/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\r\n/// its own session table, so a remote operator — which cannot know our local\r\n/// session id — names the endpoint and we map it here, never trusting a wire\r\n/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\r\n/// (a stale registry row routed us an attach for something gone → the caller\r\n/// refuses cleanly, D6).\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\r\n    brain.sessions().ok().and_then(|reply| {\r\n        reply\r\n            .sessions\r\n            .into_iter()\r\n            .find(|s| s.endpoint == endpoint)\r\n            .map(|s| s.session_id)\r\n    })\r\n}\r\n\r\n/// Serve one attach stream against this brain's broker, blocking until the\r\n/// session exits, the operator detaches, or the gate refuses.\r\n///\r\n/// `origin_node` MUST be the handshake-proven remote id from the broker's\r\n/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes\r\n/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\r\n/// is being attached, for the gate. `net_from_seq` is this brain's resume\r\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\r\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\r\n/// journal, and re-pumped output dedups at the operator's render cursor).\r\n///\r\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\r\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\r\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\r\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\r\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\r\n/// round-trip inside the event loop would swallow interleaved attach records\r\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\r\n/// the wire `session_id`.\r\n// [impl->REQ-INST-8]\r\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub fn serve_attach(\r\n    brain: &mut Brain,\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    endpoint: &str,\r\n    origin_node: &str,\r\n    net_from_seq: u64,\r\n    remote_session: Option<u64>,\r\n) -> io::Result<AttachServeOutcome> {\r\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\r\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\r\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\r\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\r\n    // session output (broker→serve) — including the controller's synchronous,\r\n    // inline, multi-frame ring replay (`become_controller`), which holds the\r\n    // broker's single-threaded per-conn handler for the whole replay. If the\r\n    // forwards rode `brain` too, that stuck handler could not read them →\r\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\r\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\r\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\r\n    // two directions on independent single-threaded handlers cannot block each\r\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\r\n    // a second conn is correct. Order is preserved: this one serve thread reads\r\n    // every Output in seq order on `brain` and forwards it in that same order on\r\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\r\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\r\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\r\n    // the controller onto that async path would remove the inline-replay block at\r\n    // the source, but it touches the authoritative cursor-advancing path.)\r\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut attached = false;\r\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\r\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\r\n    // race on the input gate).\r\n    let mut intent = AttachIntent::Control;\r\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\r\n    // controller input that races ahead of the reply is BUFFERED, not dropped\r\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\r\n    // we know we control the session (the busy-refuse case).\r\n    let mut role: Option<ServeRole> = None;\r\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\r\n    // The attached session — captured from the Request so the terminal paths can\r\n    // explicitly release this worker's role (clearing driven_by / viewer_count\r\n    // promptly, not only on connection drop).\r\n    let mut attached_session: Option<u64> = None;\r\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\r\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\r\n    // intermittent live bursts, never a CPU spin.\r\n    let mut last_resubscribe: Option<Instant> = None;\r\n\r\n    loop {\r\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\r\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\r\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\r\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\r\n        // role resolves no Output flows, so a plain read is correct then.\r\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\n        let event = match (role, attached_session) {\r\n            (Some(ServeRole::Controller), Some(sid)) => {\r\n                match read_controller_event_resuming(brain, sid, Some(origin_node)) {\r\n                    Ok(ev) => ev,\r\n                    Err(e)\r\n                        if e.get_ref()\r\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\r\n                    {\r\n                        // Irrecoverable: the dropped frames rolled out of the ring.\r\n                        // SURFACE a marked truncation to the operator (never a\r\n                        // silent skip = B2 lie, never a spin), then end cleanly.\r\n                        // Graceful continuation is deferred\r\n                        // (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND).\r\n                        let notice =\r\n                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\r\n                        let line = ndjson::encode_line(&AttachRecord::Output {\r\n                            seq: brain.controller_resume_floor(),\r\n                            data_b64: encode_bytes(notice),\r\n                        });\r\n                        let _ = wire.net_stream_send(stream_id, &line, None, true);\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                        return Err(e);\r\n                    }\r\n                    Err(e) => {\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                        return Err(e);\r\n                    }\r\n                }\r\n            }\r\n            _ => brain.read_event()?,\r\n        };\r\n        match event {\r\n            // ── Wire → session: requests, input, resize. ───────────────────\r\n            BrokerEvent::NetStreamData {\r\n                stream_id: sid,\r\n                bytes,\r\n                ..\r\n            } if sid == stream_id => {\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                            intent: req_intent,\r\n                            // The dispatcher already resolved endpoint_id→session\r\n                            // (passed as remote_session) BEFORE this loop; ignore\r\n                            // the wire copy here (a mid-loop re-resolve would swallow\r\n                            // interleaved attach records — see remote_session docs).\r\n                            endpoint_id: _,\r\n                        } => {\r\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\r\n                            // the transport-proven origin. Viewing is gated\r\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\r\n                            // watching reveals full session contents).\r\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\r\n                                == AccessDecision::Deny\r\n                            {\r\n                                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                                return Ok(AttachServeOutcome::Refused);\r\n                            }\r\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\r\n                            // dispatcher-resolved session id (the owning node is\r\n                            // authoritative for its own table); the LOCAL path uses\r\n                            // the wire session_id unchanged.\r\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n                            let sid = remote_session.unwrap_or(session_id);\r\n                            intent = req_intent;\r\n                            attached_session = Some(sid);\r\n                            // Role-aware subscribe (REQ-RCVIEW-1): the broker\r\n                            // resolves controller / viewer / busy / take by the\r\n                            // intent + this handshake-proven origin, and replies a\r\n                            // `Subscribed`. driven_by / viewer_count are now\r\n                            // BROKER-owned (single writer — resolves the\r\n                            // clear-race), so this loop no longer stamps them.\r\n                            // A VIEWER attach ARMS snap-above at the floor so a\r\n                            // PRE-eviction ring-roll forward Output gap (the ring\r\n                            // rolled frames out under flood with NO eviction\r\n                            // marker) SNAPS to live instead of fataling the\r\n                            // legacy reject-gap → the viewer keeps forwarding.\r\n                            // The CONTROLLER keeps strict reject-gap (exactly-once\r\n                            // resume, B2). Composes with the POST-eviction\r\n                            // ViewerEvicted skip-to-live arm below.\r\n                            // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\r\n                            if req_intent == AttachIntent::Viewer {\r\n                                brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\r\n                            } else {\r\n                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\r\n                            }\r\n                            attached = true;\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                            match role {\r\n                                // Confirmed controller — exactly-once at the PTY\r\n                                // (the operator-minted id rides the B5 journal). NO-ACK\r\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\r\n                                // fire-and-forward (the ack is discarded here) and can\r\n                                // flood N input frames on this one conn; an acked write\r\n                                // back would fill the return direction and deadlock the\r\n                                // broker's per-conn handler. Dedup still holds at the\r\n                                // broker (op_id applied-set).\r\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n                                Some(ServeRole::Controller) => {\r\n                                    // The op_id is operator-minted (rc attach OpMinter),\r\n                                    // so it carries the `rc` journal tag (ADR-0034).\r\n                                    brain.send_effect_no_ack(\r\n                                        MintedOp::new(Minter::Rc, op_id),\r\n                                        &bytes,\r\n                                    )?\r\n                                }\r\n                                // Outcome not yet known — buffer; flushed on a\r\n                                // controller confirmation, discarded on busy/viewer.\r\n                                None => pending_input.push((op_id, bytes)),\r\n                                // A viewer never reaches here (intent gate).\r\n                                Some(ServeRole::Viewer) => {}\r\n                            }\r\n                        }\r\n                        AttachRecord::Resize { rows, cols }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\r\n                            // broker rejects it on a viewer's stream). A viewer's\r\n                            // Resize is dropped here by the intent gate too.\r\n                            let _ = brain.resize(rows, cols);\r\n                        }\r\n                        // Input/Resize before Request, a viewer's input/resize,\r\n                        // or target-direction records echoed back: noise — ignore.\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n\r\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\r\n            BrokerEvent::Subscribed { outcome, .. } => {\r\n                use crate::msg::SubscribeOutcome as O;\r\n                match outcome {\r\n                    O::Controller | O::TookControl => {\r\n                        role = Some(ServeRole::Controller);\r\n                        // A driver attached — the wake edge (controller-only;\r\n                        // idempotent on an already-active seat).\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\r\n                        // Flush any input that raced ahead of the reply (operator-\r\n                        // minted ops carry the `rc` tag).\r\n                        for (op, b) in pending_input.drain(..) {\r\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\r\n                        }\r\n                    }\r\n                    O::Viewer => {\r\n                        role = Some(ServeRole::Viewer);\r\n                        // Wake-neutral: a viewer never wakes the endpoint, never\r\n                        // keeps it awake (Gap B). Discard any racing input.\r\n                        pending_input.clear();\r\n                    }\r\n                    O::BusyControlled { .. } => {\r\n                        // A different operator controls it — refuse (no PTY input\r\n                        // ever reached it; the buffer is discarded). The rc\r\n                        // renders the busy guidance client-side from driven_by.\r\n                        wire.net_stream_send(stream_id, &[], None, true)?;\r\n                        return Ok(AttachServeOutcome::Refused);\r\n                    }\r\n                }\r\n            }\r\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\r\n            BrokerEvent::Size { rows, cols, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            // This controller was kicked by a different operator's `--take`.\r\n            BrokerEvent::Displaced { by, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                // No dormant edge: the endpoint stays driven by the taker (whose\r\n                // own serve loop already fired the wake).\r\n                return Ok(AttachServeOutcome::Displaced);\r\n            }\r\n\r\n            // This VIEWER overflowed its broker subscription queue and was evicted\r\n            // (it forwards slower than the drain fans out under flood). SKIP TO\r\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\r\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\r\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\r\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\r\n            // spin. The next live Output carries a forward seq jump; the operator's\r\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\r\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\r\n            BrokerEvent::ViewerEvicted { session_id: sid }\r\n                if attached && Some(sid) == attached_session =>\r\n            {\r\n                if let Some(prev) = last_resubscribe {\r\n                    let since = prev.elapsed();\r\n                    if since < RESUBSCRIBE_INTERVAL {\r\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\r\n                    }\r\n                }\r\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\r\n                last_resubscribe = Some(Instant::now());\r\n            }\r\n\r\n            // The operator closed its send side: the viewport is over.\r\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\r\n                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\r\n                // info.json, carrying the current driven_by through), THEN release\r\n                // the role over IPC so the broker's driven_by=None is the LAST\r\n                // write — otherwise a concurrent resting RMW restores the stale\r\n                // marker (the clear-race the broker-single-writer model averts).\r\n                if role == Some(ServeRole::Controller) {\r\n                    // The driver detached — the endpoint rests warm (controller-\r\n                    // only; a viewer leaving is wake-neutral).\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                if let Some(sid) = attached_session {\r\n                    let _ = brain.detach_session(sid); // release the role (clears markers)\r\n                }\r\n                return Ok(AttachServeOutcome::Detached);\r\n            }\r\n\r\n            // ── Session → wire: output and exit. ────────────────────────────\r\n            BrokerEvent::Output { seq, bytes, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Output {\r\n                    seq,\r\n                    data_b64: encode_bytes(&bytes),\r\n                });\r\n                // Unjournaled by design (see module docs): `seq` rides inside\r\n                // the record, and the operator's render cursor is the dedup.\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            BrokerEvent::Exit { code, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Exit { code });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Ok(AttachServeOutcome::Exited(code));\r\n            }\r\n\r\n            BrokerEvent::Error { message } => {\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Err(io::Error::other(message));\r\n            }\r\n            // Applied/NetSent acks and unrelated events: not ours to handle.\r\n            _ => {}\r\n        }\r\n    }\r\n}\r\n\r\n/// Operator-side: open an attach stream on a dialed connection and send the\r\n/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\r\n/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\r\n/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\r\n/// durable id for the exactly-once stream open (safe to block for its ack —\r\n/// no attach traffic can be in flight before the stream exists); the Request\r\n/// send itself is unjournaled (see module docs — a replayed Request re-gates\r\n/// and re-attaches idempotently).\r\n// [impl->REQ-INST-8]\r\npub fn request_attach(\r\n…\n) -> io::Result<u64> {\n…\n}","startLine":135,"lineNumbers":[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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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,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,null,560,null,562]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\attach.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":627,"totalBytes":24662,"outputLines":419,"outputBytes":24662,"shownRange":{"start":135,"end":553},"nextOffset":554}}},"isError":false,"timestamp":1784195763403}}
{"type":"message","id":"10d501fe","parentId":"841cdd98","timestamp":"2026-07-16T09:56:03.439Z","message":{"role":"toolResult","toolCallId":"call_VigRJTsNViTUwiOeXTgggM10|fc_008390f508d1d09b016a58aab3372c819a9fe9f54b9849b191","toolName":"read","content":[{"type":"text","text":"/// takes control. Returns when the session exits, the operator detaches, or an\n/// error occurs.\n// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\npub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, false)\n}\n\n/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\n/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\n/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\n/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND\n/// stamp), yet the confirmed live session IS attachable — \"attach to a live\n/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).\n/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a\n/// session that dies after confirmation, so this is not a blank-hang reopening.\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub fn run_attach_session_confirmed(\n    endpoint_id: &str,\n    intent: AttachIntent,\n) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, true)\n}\n\nfn run_attach_inner(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    session_confirmed: bool,\n) -> Result<(), String> {\n    let view = intent == AttachIntent::Viewer;\n    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\n    // an endpoint a DIFFERENT node already controls gets actionable guidance here\n    // (the broker would also refuse on the wire — this is the friendly path). A\n    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\n    if intent == AttachIntent::Control {\n        if let Some(node) = current_driver(endpoint_id) {\n            println!(\"'{endpoint_id}' is controlled by {node}.\");\n            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\n            println!(\"  spt rc {endpoint_id} --take   to take control (kicks {node})\");\n            return Ok(());\n        }\n    }\n    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\n    // `status=offline` is not attachable — short-circuit with clean guidance\n    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\n    // instead of the broker dance that would block on a dead session (the\n    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\n    // spt-hosted perch offline, so this catches the common case before any IPC. An\n    // ABSENT or status-less perch falls through to the broker path, which already\n    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\n    // is NOT used here: it returns true for an absent info.json by interim parity.)\n    //\n    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\n    // NOT on STATUS_ONLINE (REQ-ENDPOINT-UNBOUND-ATTACH). A STATUS_UNBOUND skeleton\n    // (session spawned, perch not yet bound) is `!= offline`, so it falls through\n    // here and attaches via the live-session resolve below — the headless /\n    // clear-a-bind-prompt path. `offline` is stamped only on session DEATH, so it\n    // never co-occurs with a live session.\n    //\n    // KNOWN BOUNDARY (doyle, W4): a RESUMING perch is still `offline` during its\n    // pre-bind window — the W2 skeleton-write short-circuits on an existing perch,\n    // so resume gets NO UNBOUND stamp. A standalone `spt rc <id>` against it hits\n    // this failfast (\"offline — start it\") rather than waiting for the bind, and\n    // retries fine once the bind flips the perch online. The `endpoint run\n    // --attach` path does NOT hit it: it arrives via run_attach_session_confirmed\n    // (session already proven live by await_endpoint_session), so the gate is\n    // SKIPPED — it attaches to the confirmed live session even while the perch is\n    // still stamped offline mid-rebringup. Accepted, not contorted around.\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    let perch_path =\n        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);\n    if !session_confirmed\n        && spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.status)\n            .as_deref()\n            == Some(spt_store::liveness::STATUS_OFFLINE)\n    {\n        println!(\"Endpoint '{endpoint_id}' is offline — nothing to attach to.\");\n        println!(\"  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it\");\n        return Ok(());\n    }\n    // Establish the attach (broker conn → session resolve → dial → attach-open →\n    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\n    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\n    // keeps today's clean-exit copy on a no-target miss.\n    let est = match establish_attach(endpoint_id, intent) {\n        Ok(est) => est,\n        Err(EstablishFail::NoTarget(msg)) => {\n            println!(\"{msg}\");\n            return Ok(());\n        }\n        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\n        // rc won't birth one. Plain operator language — what's wrong, what to do —\n        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\n        // [impl->REQ-RC-RECONNECT-TRUTH]\n        Err(EstablishFail::DaemonDown) => {\n            println!(\n                \"The spt daemon isn't running — nothing to attach to. \\\n                 Start it with `spt daemon start`, then `spt rc {endpoint_id}`.\"\n            );\n            return Ok(());\n        }\n        Err(EstablishFail::Error(e)) => return Err(e),\n    };\n    let mut remote_node: Option<String> = est.remote_node.clone();\n\n    let _raw = RawGuard::enable();\n    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n    // pump and read by the Windows stdin reader to gate scroll forwarding\n    // (REQ-RC-MOUSE-FORWARD).\n    let mouse_mode = std::sync::Arc::new(MouseMode::default());\n    let rx = if view {\n        None\n    } else {\n        let (tx, rx) = mpsc::channel();\n        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\n        Some(rx)\n    };\n\n    // Controller status row (REQ-RC-IDENTITY): the operator-side identity banner\n    // on the real terminal's reserved row 1 — controller only (a viewer's viewport\n    // is read-only and may letterbox, so it owns no row). Resolved ONCE here.\n    // [impl->REQ-RC-IDMARKER-DISABLE]\n    let mut status = if status_row_active(view) {\n        let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n        Some(StatusRow {\n            text: resolve_identity_line(endpoint_id),\n            rows,\n            cols,\n        })\n    } else {\n        None\n    };\n\n    let mut stdout = std::io::stdout();\n    let outcome = attach_viewport(\n        endpoint_id,\n        intent,\n        view,\n        est,\n        rx.as_ref(),\n        &mouse_mode,\n        status.as_mut(),\n        &mut remote_node,\n        &mut stdout,\n    );\n\n    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\n    // leaving raw mode, on every pump-exit path (REQ-RC-IDENTITY).\n    if status.is_some() {\n        let _ = stdout.write_all(&StatusRow::restore_bytes());\n        let _ = stdout.flush();\n    }\n    // Out of raw mode (guard drops after) before the parting line.\n    drop(_raw);\n    match outcome {\n        Ok(PumpEnd::Exited(code)) => {\n            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Detached) => {\n            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Displaced(by)) => {\n            // The loud kick notice (REQ-KICK-1): a different operator took control.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\n            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\n            // successful re-attach. Plain operator language (Q4 UX rule): what\n            // happened, what it means, what to do next — never op/read-err lingo.\n            if daemon_down {\n                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\n                // went down and never came back — rc did NOT resurrect it. Name that\n                // plainly so the operator restarts the daemon rather than guessing.\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\n                     didn't come back within {}s. rc won't auto-start it. Bring it \\\n                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            } else {\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\n                     within {}s — the session may still be running. Try `spt rc \\\n                     {endpoint_id}` again once the node/daemon is back]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            }\n            Ok(())\n        }\n        // Reconnectable ends never escape the loop (they enter the retry arm and\n        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n        // so a future PumpEnd variant still forces a decision here.\n        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n        }\n        Ok(PumpEnd::Stalled) => {\n            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced\n            // no output at all — a dead/wedged session, not a slow one. A clear\n            // message, never the historical infinite blank.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[no output from '{endpoint_id}' — the session may be dead or \\\n                 wedged. Detached. Try `spt endpoint list` for its state, or \\\n                 `spt endpoint run` to restart it]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::NoLiveSession) => {\n            // The serve refused before anything ran (#4, REQ-RC-CROSS-NODE-ATTACH):\n            // no live session for the endpoint on the node we reached, or its gate\n            // denied us. On the cross-node leg this is the D6 stale-row case.\n            match &remote_node {\n                Some(node) => {\n                    let _ = writeln!(\n                        stdout,\n                        \"\\r\\n[no live session for '{endpoint_id}' on {node} — it may \\\n                         have ended, or the registry row is stale]\"\n                    );\n                }\n                None => {\n                    let _ = writeln!(stdout, \"\\r\\n[no live session for '{endpoint_id}']\");\n                }\n            }\n            Ok(())\n        }\n        Err(e) => Err(public_attach_failure(endpoint_id, e)),\n    }\n}\n\n/// F-1 (REQ-PUBLIC-ERROR-SURFACES): the residual attach-failure line is the one\n/// rc surface that still leaked transport lingo (\"brain IPC read deadline\n/// elapsed\") to an operator. Map the known daemon-didn't-answer class to the\n/// observable situation + next action; every OTHER error passes through\n/// verbatim (no over-matching — unknown failures keep their detail). Pure, so\n/// the mapping is unit-tested.\n// [impl->REQ-PUBLIC-ERROR-SURFACES]\nfn public_attach_failure(endpoint_id: &str, e: String) -> String {\n    if e.contains(\"brain IPC read deadline\") {\n        format!(\n            \"the daemon didn't answer in time while attaching to '{endpoint_id}' — \\\n             it may be busy or mid-restart. Try `spt rc {endpoint_id}` again; if it \\\n             keeps happening, `spt endpoint list` shows the endpoint's state\"\n        )\n    } else {\n        e\n    }\n}\n\n/// An established attach viewport: the broker conn, the open+subscribed attach\n/// stream, and the op counter that minted it. One per (re)connect — the B-3\n/// reconnect loop re-mints the WHOLE bundle (a broker restart drops the conn,\n/// not just the stream; a fresh OpMinter keeps ADR-0034 `rc` op tracing per\n/// viewport). [impl->REQ-RC-RECONNECT]\nstruct Established {\n    brain: Brain,\n    ops: OpMinter,\n    stream_id: u64,\n    /// The owning node's label for a REMOTE attach (the reconnect-banner target\n    /// + the honest-failure copy); `None` ⇒ local.\n    remote_node: Option<String>,\n}\n\n/// Why [`establish_attach`] failed.\nenum EstablishFail {\n    /// No attachable target (no live session anywhere / ambiguous / node\n    /// unreachable) — the carried message is the CLEAN first-attempt exit copy;\n    /// the reconnect loop retries it inside the window instead (the node may\n    /// still be coming back).\n    NoTarget(String),\n    /// The daemon isn't running — rc is CONNECT-ONLY and refuses to auto-start it\n    /// (REQ-RC-RECONNECT-TRUTH). On the initial attach this is a loud \"daemon\n    /// isn't running — start it\" exit; inside the reconnect loop it's just another\n    /// retry (the operator may bring the daemon back), never a resurrection.\n    /// [impl->REQ-RC-RECONNECT-TRUTH]\n    DaemonDown,\n    /// A setup error (broker connect / dial / attach-open / subscribe).\n    Error(String),\n}\n\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n///\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\n/// the broker wires an in-process loopback conn whose streams surface as the\n/// SAME net-stream events the cross-node QUIC path produces. From\n/// `request_attach` down this is byte-identical to a cross-node attach — the\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\n/// The loopback peer's origin is this node's own id, so the target's serve gates\n/// same-node.\n///\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\n/// identically to loopback — so from here down the two legs converge. Honest\n/// failure on every resolve/dial miss: a clean message, never a false attach or\n/// a hang (D2/D6).\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n    // launcher. It must NEVER auto-start the broker — the historical\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\n    // broker cold-connect below is already deadline-bounded, so a down daemon\n    // never hangs.\n    // [impl->REQ-RC-RECONNECT-TRUTH]\n    if !spt_daemon::daemon::is_running() {\n        return Err(EstablishFail::DaemonDown);\n    }\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\n    // pipes), so the single-threaded pump interleaves stdin + broker events\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\n    // sessions, attach-open ack); the pump itself passes its own short slice.\n    let mut brain = Brain::cold_start_pump(\n        &spt_daemon::endpoint::broker_socket_name(),\n        now_ms(),\n        Duration::from_secs(10),\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\n\n    let ops = OpMinter::new();\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    // resolve the OWNING node from the registry and dial it, letting THAT node\n    // resolve endpoint→its session server-side.\n    let local_session = resolve_session(&mut brain, endpoint_id)\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\n    let mut remote_node: Option<String> = None;\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\n        Some(sid) => {\n            let dialed = brain\n                .net_dial_loopback()\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\n            (dialed.conn_id, sid, None)\n        }\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\n                remote_node = Some(node);\n                // session_id is a placeholder the owning node ignores — it\n                // resolves the endpoint to its OWN session id server-side.\n                (conn_id, 0, Some(endpoint_id))\n            }\n            crate::wansend::OwnerDial::NotFound => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"No live session for '{endpoint_id}' — not running on this node \\\n                     or any visible subnet.\"\n                )))\n            }\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\n                return Err(EstablishFail::NoTarget(msg))\n            }\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\n                )))\n            }\n        },\n    };\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\n    // ONCE more. The first attempt uses the conn already established above (happy\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\n    // drops the CONN too, not just the stream, so opening against the dead conn\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\n    let mut established: Option<u64> = Some(conn_id);\n    let stream_id = spt_daemon::with_tracing_retry(\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\n        |op| {\n            let cid = match established.take() {\n                Some(c) => c,\n                None => match local_session {\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\n                    Some(_) => brain\n                        .net_dial_loopback()\n                        .map_err(|e| {\n                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\n                        })?\n                        .conn_id,\n                    // Remote: re-resolve + re-dial the owning node.\n                    None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n                        crate::wansend::OwnerDial::Dialed { conn_id, .. } => conn_id,\n                        _ => {\n                            return Err(std::io::Error::other(format!(\n                                \"'{endpoint_id}' is no longer reachable\"\n                            )))\n                        }\n                    },\n                },\n            };\n            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\n        },\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n    // Subscribe our side of the stream so the target's `Output` records surface\n    // as `NetStreamData` events (the open + Request do not auto-subscribe).\n    brain\n        .net_stream_subscribe(stream_id, 0)\n        .map_err(|e| EstablishFail::Error(format!(\"stream subscribe: {e}\")))?;\n    Ok(Established {\n        brain,\n        ops,\n        stream_id,\n        remote_node,\n    })\n}\n\n/// Drive an established viewport to its FINAL end: the B-3 reconnect loop\n/// (REQ-RC-RECONNECT) — each pass is one full viewport (initial resize → pump).\n/// A severed viewport (broker bounce, remote conn drop — the reconnectable\n/// class) paints a centered reconnect banner and RE-DRIVES [`establish_attach`]\n/// inside a bounded window; every other end is FINAL and returns to the\n/// caller's parting copy. The FAULT-MATRIX row-9 promise (\"rc reconnects\") is\n/// made TRUE here, not edited down (triage F-3).\n///\n/// The SEAM (doyle rig ruling #4): everything terminal-owning stays at the\n/// interactive caller — raw-mode guard, stdin-reader spawn, StatusRow\n/// construction/teardown, the parting copy. This fn only needs an established\n/// attach + an `out` sink, so the two-host rig can drive the REAL loop headless\n/// (`out` = a capture buffer) — no behavior change for the interactive path.\n/// `remote_node` is caller-owned so the parting copy names the LAST node the\n/// viewport was attached to (a reconnect can re-resolve it).\n#[allow(clippy::too_many_arguments)]\nfn attach_viewport(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    view: bool,\n    mut est: Established,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n    remote_node: &mut Option<String>,\n    out: &mut impl std::io::Write,\n) -> Result<PumpEnd, String> {\n    loop {\n        // Controller: send the INITIAL terminal size so the PTY matches from the\n        // start (REQ-RCVIEW-1). Best-effort — a failure just leaves the default\n        // size. When the status row is active the harness PTY gets rows-1 (we own\n        // row 1), never 0 (REQ-RC-IDENTITY). Re-sent per establish: the new\n        // viewport's PTY must match our CURRENT terminal, not the original one.\n        if !view {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                let pty_rows = if status.is_some() {\n                    rows.saturating_sub(1).max(1)\n                } else {\n                    rows\n                };\n                let _ = send_attach_resize(&mut est.brain, est.stream_id, pty_rows, cols);\n            }\n        }\n\n        let mut decoder = AttachDecoder::new();\n        let end = pump(\n            &mut est.brain,\n            est.stream_id,\n            &est.ops,\n            rx,\n            &mut decoder,\n            out,\n            !view, // controller drives resize\n            mouse_mode,\n            status.as_deref_mut(),\n        );\n\n        // Final ends (incl. hard errors) exit the loop to the parting copy.\n        let severed_kind = match &end {\n            Ok(e) if reconnect_class(e) == EndClass::Reconnect => match e {\n                PumpEnd::BrokerGone => \"the daemon connection dropped\",\n                _ => \"the connection was severed\",\n            },\n            _ => return end,\n        };\n\n        // Reconnect: full-screen centered banner (operator language — the internal\n        // detail stays off the terminal per the Q4 UX rule), then re-drive\n        // establish_attach inside the bounded window. A detach keypress mid-window\n        // aborts the retry loop honestly. The banner repaints each iteration with a\n        // live COUNTDOWN (REQ-RC-RECONNECT-TRUTH) so the operator sees rc is bounded\n        // and working, never the old apparent freeze at a static \"Reconnecting…\".\n        // establish_attach is CONNECT-ONLY: a down daemon yields DaemonDown here and\n        // is retried (the operator may bring it back) WITHOUT any WMI resurrection.\n        let target = remote_node.clone().unwrap_or_else(|| \"local daemon\".to_string());\n        let retry_started = Instant::now();\n        let reestablished = loop {\n            let elapsed = retry_started.elapsed();\n            if reconnect_expired(elapsed) {\n                break None;\n            }\n            let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n            let remaining = reconnect_remaining_secs(elapsed);\n            let _ = out.write_all(&reconnect_banner_bytes(rows, cols, &target, remaining));\n            let _ = out.flush();\n            // Operator gives up mid-reconnect: honor the detach keys.\n            if let Some(rx) = rx {\n                let mut detached = false;\n                while let Ok(msg) = rx.try_recv() {\n                    if matches!(msg, StdinMsg::Detach) {\n                        detached = true;\n                    }\n                }\n                if detached {\n                    break Some(Err(())); // detach sentinel\n                }\n            }\n            match establish_attach(endpoint_id, intent) {\n                Ok(next) => break Some(Ok(next)),\n                // Every fail shape retries until the window expires — the node/daemon\n                // may still be coming back. DaemonDown included: rc waits for the\n                // operator to restart it, it does NOT auto-start one.\n                Err(_) => std::thread::sleep(RECONNECT_PAUSE),\n            }\n        };\n        match reestablished {\n            Some(Ok(next)) => {\n                *remote_node = next.remote_node.clone();\n                est = next;\n                // Loop: fresh resize + pump. The re-serve replays the output ring,\n                // repainting the screen the banner cleared.\n            }\n            Some(Err(())) => return Ok(PumpEnd::Detached),\n            None => {\n                // Give-up copy selection (REQ-RC-RECONNECT-TRUTH): a still-down local\n                // daemon (only knowable for a LOCAL target) gets the loud \"session\n                // lost — daemon down\" copy; anything else keeps the generic\n                // didn't-reconnect copy. Remote severs never probe local daemon state.\n                let daemon_down =\n                    remote_node.is_none() && !spt_daemon::daemon::is_running();\n                return Ok(PumpEnd::ReconnectGaveUp {\n                    detail: severed_kind.to_string(),\n                    daemon_down,\n                });\n            }\n        }\n    }\n}\n\n/// How long the B-3 reconnect loop keeps re-driving `establish_attach` after a\n/// severed viewport before the plain-language give-up (REQ-RC-RECONNECT).\n/// Generous: a daemon bounce (`spt daemon stop` + the next auto-start) and a\n/// node's broker restart both land well inside it.\nconst RECONNECT_WINDOW: Duration = Duration::from_secs(30);\n/// The pause between reconnect attempts (each attempt already carries its own\n/// bounded setup deadlines — this just keeps the loop from hammering).\nconst RECONNECT_PAUSE: Duration = Duration::from_secs(1);\n\n/// The give-up decision, pulled out so it unit-tests without a 30s wait.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_expired(elapsed: Duration) -> bool {\n    elapsed >= RECONNECT_WINDOW\n}\n\n/// Seconds still left in the reconnect window, for the banner countdown\n/// (REQ-RC-RECONNECT-TRUTH). CEIL so the display reads a full `RECONNECT_WINDOW`s\n/// at the start and never flashes `0s` while the loop is still trying (0 only once\n/// the window is fully spent, where the caller has already broken to give-up).\n// [impl->REQ-RC-RECONNECT-TRUTH]\nfn reconnect_remaining_secs(elapsed: Duration) -> u64 {\n    let left = RECONNECT_WINDOW.saturating_sub(elapsed);\n    // ceil(left / 1s) via integer millis — 1..=WINDOW while any time remains, 0 at/past.\n    (left.as_millis() as u64).div_ceil(1000)\n}\n\n/// Which pump ends re-drive the viewport (B-3) vs. exit with parting copy.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum EndClass {\n    Final,\n    Reconnect,\n}\n\n/// A severed transport is RECONNECTABLE: the broker-conn EOF class (broker\n/// bounce) and the serve-side EOF after rendered output (remote conn drop).\n/// Everything else — session exit, operator detach, displacement, the stall\n/// backstop, an attach refuse — is FINAL: re-driving those would re-attach to a\n/// session the operator (or the session itself) deliberately ended.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_class(end: &PumpEnd) -> EndClass {\n    match end {\n        PumpEnd::BrokerGone | PumpEnd::Severed => EndClass::Reconnect,\n        PumpEnd::Exited(_)\n        | PumpEnd::Detached\n        | PumpEnd::Displaced(_)\n        | PumpEnd::Stalled\n        | PumpEnd::NoLiveSession\n        | PumpEnd::ReconnectGaveUp { .. } => EndClass::Final,\n    }\n}\n\n/// Classify a serve-side stream EOF: rendered output first ⇒ the conn was\n/// SEVERED under a live viewport (reconnect); nothing ever rendered ⇒ the serve\n/// REFUSED the attach (no live session / gate deny — the cross-node D6\n/// stale-row case), honest \"no live session\" instead of a false \"detached\"\n/// (#4, REQ-RC-CROSS-NODE-ATTACH).\n// [impl->REQ-RC-RECONNECT]\nfn serve_eof_end(rendered_any: bool) -> PumpEnd {\n    if rendered_any {\n        PumpEnd::Severed\n    } else {\n        PumpEnd::NoLiveSession\n    }\n}\n\n/// The full-screen reconnect banner (B-3): clear + home, then the operator-language\n/// line centered on the terminal. Pure byte-emit (like [`StatusRow`]) so the exact\n/// VT shape unit-tests directly; the caller writes the bytes. The text is plain\n/// operator language (Q4 UX rule) — the internal sever detail never paints here.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_banner_bytes(rows: u16, cols: u16, target: &str, remaining_secs: u64) -> Vec<u8> {\n    // Visible countdown (REQ-RC-RECONNECT-TRUTH): the remaining-seconds suffix turns\n    // the old static banner into a live \"rc is working, bounded\" signal — the operator\n    // never mistakes a bounded retry for the historical freeze-forever.\n    let text = format!(\"Reconnecting to {target}… {remaining_secs}s\");\n    let row = (rows / 2).max(1);\n    // Center on the CHARACTER count (the terminal cell count for this ASCII+…\n    // line), clamped so an over-wide text still starts on-screen.\n    let width = text.chars().count() as u16;\n    let col = if width >= cols { 1 } else { (cols - width) / 2 + 1 };\n    let mut out = Vec::new();\n    out.extend_from_slice(b\"\\x1b[2J\\x1b[H\");\n    out.extend_from_slice(format!(\"\\x1b[{row};{col}H\").as_bytes());\n    out.extend_from_slice(text.as_bytes());\n    out\n}\n\n#[derive(Debug)]\nenum PumpEnd {\n    Exited(Option<i32>),\n    Detached,\n    /// A different operator took control (REQ-KICK-1); carries the taker's node.\n    Displaced(String),\n    /// The broker stream was severed mid-session (broker bounce / hang-up — e.g. a\n    /// `spt daemon stop`). Recoverable, not a crash: the broker is the\n    /// daemon-lifetime anchor and returns on the next `spt api` call, so this\n    /// enters the B-3 reconnect loop (REQ-RC-RECONNECT) rather than exiting.\n    BrokerGone,\n    /// The serve side finished the stream AFTER rendering output — the remote\n    /// conn dropped (node bounce, network cut), NOT an operator detach (that\n    /// returns via the stdin `Detach` path directly) and NOT a session end (an\n    /// `Exit` record precedes a session-death EOF). Pre-B-3 this was mislabeled\n    /// \"detached — still running\"; it is the SEVERED case the reconnect loop\n    /// re-drives. [impl->REQ-RC-RECONNECT]\n    Severed,\n    /// B-3 terminal state: the reconnect window expired without a successful\n    /// re-attach. `detail` is the operator-language cause of the ORIGINAL sever\n    /// (Q4 UX rule — never read-error lingo). `daemon_down` records whether the\n    /// local daemon was still down at give-up (REQ-RC-RECONNECT-TRUTH) — it\n    /// selects the loud \"session lost — daemon down\" copy over the generic\n    /// didn't-reconnect copy. [impl->REQ-RC-RECONNECT] [impl->REQ-RC-RECONNECT-TRUTH]\n    ReconnectGaveUp { detail: String, daemon_down: bool },\n    /// The attach produced NO event whatsoever within the generous first-event\n    /// backstop window (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a healthy session —\n    /// even one still painting / mid-init — replays its buffered output (or at\n    /// least a Subscribed/Size frame) promptly, so a totally silent stream past\n    /// this bound is a dead/wedged session, not a slow one. Surfaces a clear\n    /// message instead of the historical infinite blank. The bound is deliberately\n    /// LONG (only ever bites a pathological hang; the broker-EOF-on-dead-child path\n    /// and the status-gate catch the ordinary dead cases first).\n    Stalled,\n    /// The serve side finished the stream having produced NO output — i.e. it\n    /// refused the attach (no live session for the endpoint, or the access gate\n    /// denied us). On the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH) this is\n    /// the D6 stale-row case: the registry routed us to a node that no longer has\n    /// the session. Distinct from [`Detached`](PumpEnd::Detached) (the OPERATOR\n    /// ended a live viewport) — here the REMOTE closed on us before anything ran,\n    /// so it renders an honest \"no live session\" instead of a false \"detached\".\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n    NoLiveSession,\n}\n\n\n[Showing lines 1079-1763 of 3214. Use :1764 to continue]"}],"details":{"truncation":{"content":"/// takes control. Returns when the session exits, the operator detaches, or an\r\n/// error occurs.\r\n// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\r\npub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\r\n    run_attach_inner(endpoint_id, intent, false)\r\n}\r\n\r\n/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\r\n/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\r\n/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\r\n/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\r\n/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND\r\n/// stamp), yet the confirmed live session IS attachable — \"attach to a live\r\n/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).\r\n/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a\r\n/// session that dies after confirmation, so this is not a blank-hang reopening.\r\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\r\npub fn run_attach_session_confirmed(\r\n    endpoint_id: &str,\r\n    intent: AttachIntent,\r\n) -> Result<(), String> {\r\n    run_attach_inner(endpoint_id, intent, true)\r\n}\r\n\r\nfn run_attach_inner(\r\n    endpoint_id: &str,\r\n    intent: AttachIntent,\r\n    session_confirmed: bool,\r\n) -> Result<(), String> {\r\n    let view = intent == AttachIntent::Viewer;\r\n    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\r\n    // an endpoint a DIFFERENT node already controls gets actionable guidance here\r\n    // (the broker would also refuse on the wire — this is the friendly path). A\r\n    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\r\n    if intent == AttachIntent::Control {\r\n        if let Some(node) = current_driver(endpoint_id) {\r\n            println!(\"'{endpoint_id}' is controlled by {node}.\");\r\n            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\r\n            println!(\"  spt rc {endpoint_id} --take   to take control (kicks {node})\");\r\n            return Ok(());\r\n        }\r\n    }\r\n    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\r\n    // `status=offline` is not attachable — short-circuit with clean guidance\r\n    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\r\n    // instead of the broker dance that would block on a dead session (the\r\n    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\r\n    // spt-hosted perch offline, so this catches the common case before any IPC. An\r\n    // ABSENT or status-less perch falls through to the broker path, which already\r\n    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\r\n    // is NOT used here: it returns true for an absent info.json by interim parity.)\r\n    //\r\n    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\r\n    // NOT on STATUS_ONLINE (REQ-ENDPOINT-UNBOUND-ATTACH). A STATUS_UNBOUND skeleton\r\n    // (session spawned, perch not yet bound) is `!= offline`, so it falls through\r\n    // here and attaches via the live-session resolve below — the headless /\r\n    // clear-a-bind-prompt path. `offline` is stamped only on session DEATH, so it\r\n    // never co-occurs with a live session.\r\n    //\r\n    // KNOWN BOUNDARY (doyle, W4): a RESUMING perch is still `offline` during its\r\n    // pre-bind window — the W2 skeleton-write short-circuits on an existing perch,\r\n    // so resume gets NO UNBOUND stamp. A standalone `spt rc <id>` against it hits\r\n    // this failfast (\"offline — start it\") rather than waiting for the bind, and\r\n    // retries fine once the bind flips the perch online. The `endpoint run\r\n    // --attach` path does NOT hit it: it arrives via run_attach_session_confirmed\r\n    // (session already proven live by await_endpoint_session), so the gate is\r\n    // SKIPPED — it attaches to the confirmed live session even while the perch is\r\n    // still stamped offline mid-rebringup. Accepted, not contorted around.\r\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\r\n    let perch_path =\r\n        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);\r\n    if !session_confirmed\r\n        && spt_store::info::read_info(&perch_path)\r\n            .and_then(|i| i.status)\r\n            .as_deref()\r\n            == Some(spt_store::liveness::STATUS_OFFLINE)\r\n    {\r\n        println!(\"Endpoint '{endpoint_id}' is offline — nothing to attach to.\");\r\n        println!(\"  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it\");\r\n        return Ok(());\r\n    }\r\n    // Establish the attach (broker conn → session resolve → dial → attach-open →\r\n    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\r\n    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\r\n    // keeps today's clean-exit copy on a no-target miss.\r\n    let est = match establish_attach(endpoint_id, intent) {\r\n        Ok(est) => est,\r\n        Err(EstablishFail::NoTarget(msg)) => {\r\n            println!(\"{msg}\");\r\n            return Ok(());\r\n        }\r\n        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\r\n        // rc won't birth one. Plain operator language — what's wrong, what to do —\r\n        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\r\n        // [impl->REQ-RC-RECONNECT-TRUTH]\r\n        Err(EstablishFail::DaemonDown) => {\r\n            println!(\r\n                \"The spt daemon isn't running — nothing to attach to. \\\r\n                 Start it with `spt daemon start`, then `spt rc {endpoint_id}`.\"\r\n            );\r\n            return Ok(());\r\n        }\r\n        Err(EstablishFail::Error(e)) => return Err(e),\r\n    };\r\n    let mut remote_node: Option<String> = est.remote_node.clone();\r\n\r\n    let _raw = RawGuard::enable();\r\n    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\r\n    // pump and read by the Windows stdin reader to gate scroll forwarding\r\n    // (REQ-RC-MOUSE-FORWARD).\r\n    let mouse_mode = std::sync::Arc::new(MouseMode::default());\r\n    let rx = if view {\r\n        None\r\n    } else {\r\n        let (tx, rx) = mpsc::channel();\r\n        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\r\n        Some(rx)\r\n    };\r\n\r\n    // Controller status row (REQ-RC-IDENTITY): the operator-side identity banner\r\n    // on the real terminal's reserved row 1 — controller only (a viewer's viewport\r\n    // is read-only and may letterbox, so it owns no row). Resolved ONCE here.\r\n    // [impl->REQ-RC-IDMARKER-DISABLE]\r\n    let mut status = if status_row_active(view) {\r\n        let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\r\n        Some(StatusRow {\r\n            text: resolve_identity_line(endpoint_id),\r\n            rows,\r\n            cols,\r\n        })\r\n    } else {\r\n        None\r\n    };\r\n\r\n    let mut stdout = std::io::stdout();\r\n    let outcome = attach_viewport(\r\n        endpoint_id,\r\n        intent,\r\n        view,\r\n        est,\r\n        rx.as_ref(),\r\n        &mouse_mode,\r\n        status.as_mut(),\r\n        &mut remote_node,\r\n        &mut stdout,\r\n    );\r\n\r\n    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\r\n    // leaving raw mode, on every pump-exit path (REQ-RC-IDENTITY).\r\n    if status.is_some() {\r\n        let _ = stdout.write_all(&StatusRow::restore_bytes());\r\n        let _ = stdout.flush();\r\n    }\r\n    // Out of raw mode (guard drops after) before the parting line.\r\n    drop(_raw);\r\n    match outcome {\r\n        Ok(PumpEnd::Exited(code)) => {\r\n            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\r\n            Ok(())\r\n        }\r\n        Ok(PumpEnd::Detached) => {\r\n            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\r\n            Ok(())\r\n        }\r\n        Ok(PumpEnd::Displaced(by)) => {\r\n            // The loud kick notice (REQ-KICK-1): a different operator took control.\r\n            let _ = writeln!(\r\n                stdout,\r\n                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\r\n            );\r\n            Ok(())\r\n        }\r\n        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\r\n            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\r\n            // successful re-attach. Plain operator language (Q4 UX rule): what\r\n            // happened, what it means, what to do next — never op/read-err lingo.\r\n            if daemon_down {\r\n                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\r\n                // went down and never came back — rc did NOT resurrect it. Name that\r\n                // plainly so the operator restarts the daemon rather than guessing.\r\n                let _ = writeln!(\r\n                    stdout,\r\n                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\r\n                     didn't come back within {}s. rc won't auto-start it. Bring it \\\r\n                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\r\n                    RECONNECT_WINDOW.as_secs()\r\n                );\r\n            } else {\r\n                let _ = writeln!(\r\n                    stdout,\r\n                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\r\n                     within {}s — the session may still be running. Try `spt rc \\\r\n                     {endpoint_id}` again once the node/daemon is back]\",\r\n                    RECONNECT_WINDOW.as_secs()\r\n                );\r\n            }\r\n            Ok(())\r\n        }\r\n        // Reconnectable ends never escape the loop (they enter the retry arm and\r\n        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\r\n        // so a future PumpEnd variant still forces a decision here.\r\n        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\r\n            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\r\n        }\r\n        Ok(PumpEnd::Stalled) => {\r\n            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced\r\n            // no output at all — a dead/wedged session, not a slow one. A clear\r\n            // message, never the historical infinite blank.\r\n            let _ = writeln!(\r\n                stdout,\r\n                \"\\r\\n[no output from '{endpoint_id}' — the session may be dead or \\\r\n                 wedged. Detached. Try `spt endpoint list` for its state, or \\\r\n                 `spt endpoint run` to restart it]\"\r\n            );\r\n            Ok(())\r\n        }\r\n        Ok(PumpEnd::NoLiveSession) => {\r\n            // The serve refused before anything ran (#4, REQ-RC-CROSS-NODE-ATTACH):\r\n            // no live session for the endpoint on the node we reached, or its gate\r\n            // denied us. On the cross-node leg this is the D6 stale-row case.\r\n            match &remote_node {\r\n                Some(node) => {\r\n                    let _ = writeln!(\r\n                        stdout,\r\n                        \"\\r\\n[no live session for '{endpoint_id}' on {node} — it may \\\r\n                         have ended, or the registry row is stale]\"\r\n                    );\r\n                }\r\n                None => {\r\n                    let _ = writeln!(stdout, \"\\r\\n[no live session for '{endpoint_id}']\");\r\n                }\r\n            }\r\n            Ok(())\r\n        }\r\n        Err(e) => Err(public_attach_failure(endpoint_id, e)),\r\n    }\r\n}\r\n\r\n/// F-1 (REQ-PUBLIC-ERROR-SURFACES): the residual attach-failure line is the one\r\n/// rc surface that still leaked transport lingo (\"brain IPC read deadline\r\n/// elapsed\") to an operator. Map the known daemon-didn't-answer class to the\r\n/// observable situation + next action; every OTHER error passes through\r\n/// verbatim (no over-matching — unknown failures keep their detail). Pure, so\r\n/// the mapping is unit-tested.\r\n// [impl->REQ-PUBLIC-ERROR-SURFACES]\r\nfn public_attach_failure(endpoint_id: &str, e: String) -> String {\r\n    if e.contains(\"brain IPC read deadline\") {\r\n        format!(\r\n            \"the daemon didn't answer in time while attaching to '{endpoint_id}' — \\\r\n             it may be busy or mid-restart. Try `spt rc {endpoint_id}` again; if it \\\r\n             keeps happening, `spt endpoint list` shows the endpoint's state\"\r\n        )\r\n    } else {\r\n        e\r\n    }\r\n}\r\n\r\n/// An established attach viewport: the broker conn, the open+subscribed attach\r\n/// stream, and the op counter that minted it. One per (re)connect — the B-3\r\n/// reconnect loop re-mints the WHOLE bundle (a broker restart drops the conn,\r\n/// not just the stream; a fresh OpMinter keeps ADR-0034 `rc` op tracing per\r\n/// viewport). [impl->REQ-RC-RECONNECT]\r\nstruct Established {\r\n    brain: Brain,\r\n    ops: OpMinter,\r\n    stream_id: u64,\r\n    /// The owning node's label for a REMOTE attach (the reconnect-banner target\r\n    /// + the honest-failure copy); `None` ⇒ local.\r\n    remote_node: Option<String>,\r\n}\r\n\r\n/// Why [`establish_attach`] failed.\r\nenum EstablishFail {\r\n    /// No attachable target (no live session anywhere / ambiguous / node\r\n    /// unreachable) — the carried message is the CLEAN first-attempt exit copy;\r\n    /// the reconnect loop retries it inside the window instead (the node may\r\n    /// still be coming back).\r\n    NoTarget(String),\r\n    /// The daemon isn't running — rc is CONNECT-ONLY and refuses to auto-start it\r\n    /// (REQ-RC-RECONNECT-TRUTH). On the initial attach this is a loud \"daemon\r\n    /// isn't running — start it\" exit; inside the reconnect loop it's just another\r\n    /// retry (the operator may bring the daemon back), never a resurrection.\r\n    /// [impl->REQ-RC-RECONNECT-TRUTH]\r\n    DaemonDown,\r\n    /// A setup error (broker connect / dial / attach-open / subscribe).\r\n    Error(String),\r\n}\r\n\r\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\r\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\r\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\r\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\r\n///\r\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\r\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\r\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\r\n/// the broker wires an in-process loopback conn whose streams surface as the\r\n/// SAME net-stream events the cross-node QUIC path produces. From\r\n/// `request_attach` down this is byte-identical to a cross-node attach — the\r\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\r\n/// The loopback peer's origin is this node's own id, so the target's serve gates\r\n/// same-node.\r\n///\r\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\r\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\r\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\r\n/// identically to loopback — so from here down the two legs converge. Honest\r\n/// failure on every resolve/dial miss: a clean message, never a false attach or\r\n/// a hang (D2/D6).\r\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\r\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\r\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\r\n    // launcher. It must NEVER auto-start the broker — the historical\r\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\r\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\r\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\r\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\r\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\r\n    // broker cold-connect below is already deadline-bounded, so a down daemon\r\n    // never hangs.\r\n    // [impl->REQ-RC-RECONNECT-TRUTH]\r\n    if !spt_daemon::daemon::is_running() {\r\n        return Err(EstablishFail::DaemonDown);\r\n    }\r\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\r\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\r\n    // pipes), so the single-threaded pump interleaves stdin + broker events\r\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\r\n    // sessions, attach-open ack); the pump itself passes its own short slice.\r\n    let mut brain = Brain::cold_start_pump(\r\n        &spt_daemon::endpoint::broker_socket_name(),\r\n        now_ms(),\r\n        Duration::from_secs(10),\r\n    )\r\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\r\n\r\n    let ops = OpMinter::new();\r\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\r\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\r\n    // resolve the OWNING node from the registry and dial it, letting THAT node\r\n    // resolve endpoint→its session server-side.\r\n    let local_session = resolve_session(&mut brain, endpoint_id)\r\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\r\n    let mut remote_node: Option<String> = None;\r\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\r\n        Some(sid) => {\r\n            let dialed = brain\r\n                .net_dial_loopback()\r\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\r\n            (dialed.conn_id, sid, None)\r\n        }\r\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\r\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\r\n                remote_node = Some(node);\r\n                // session_id is a placeholder the owning node ignores — it\r\n                // resolves the endpoint to its OWN session id server-side.\r\n                (conn_id, 0, Some(endpoint_id))\r\n            }\r\n            crate::wansend::OwnerDial::NotFound => {\r\n                return Err(EstablishFail::NoTarget(format!(\r\n                    \"No live session for '{endpoint_id}' — not running on this node \\\r\n                     or any visible subnet.\"\r\n                )))\r\n            }\r\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\r\n                return Err(EstablishFail::NoTarget(msg))\r\n            }\r\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\r\n                return Err(EstablishFail::NoTarget(format!(\r\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\r\n                )))\r\n            }\r\n        },\r\n    };\r\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\r\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\r\n    // ONCE more. The first attempt uses the conn already established above (happy\r\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\r\n    // drops the CONN too, not just the stream, so opening against the dead conn\r\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\r\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\r\n    let mut established: Option<u64> = Some(conn_id);\r\n    let stream_id = spt_daemon::with_tracing_retry(\r\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\r\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\r\n        |op| {\r\n            let cid = match established.take() {\r\n                Some(c) => c,\r\n                None => match local_session {\r\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\r\n                    Some(_) => brain\r\n                        .net_dial_loopback()\r\n                        .map_err(|e| {\r\n                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\r\n                        })?\r\n                        .conn_id,\r\n                    // Remote: re-resolve + re-dial the owning node.\r\n                    None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\r\n                        crate::wansend::OwnerDial::Dialed { conn_id, .. } => conn_id,\r\n                        _ => {\r\n                            return Err(std::io::Error::other(format!(\r\n                                \"'{endpoint_id}' is no longer reachable\"\r\n                            )))\r\n                        }\r\n                    },\r\n                },\r\n            };\r\n            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\r\n        },\r\n    )\r\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\r\n    // Subscribe our side of the stream so the target's `Output` records surface\r\n    // as `NetStreamData` events (the open + Request do not auto-subscribe).\r\n    brain\r\n        .net_stream_subscribe(stream_id, 0)\r\n        .map_err(|e| EstablishFail::Error(format!(\"stream subscribe: {e}\")))?;\r\n    Ok(Established {\r\n        brain,\r\n        ops,\r\n        stream_id,\r\n        remote_node,\r\n    })\r\n}\r\n\r\n/// Drive an established viewport to its FINAL end: the B-3 reconnect loop\r\n/// (REQ-RC-RECONNECT) — each pass is one full viewport (initial resize → pump).\r\n/// A severed viewport (broker bounce, remote conn drop — the reconnectable\r\n/// class) paints a centered reconnect banner and RE-DRIVES [`establish_attach`]\r\n/// inside a bounded window; every other end is FINAL and returns to the\r\n/// caller's parting copy. The FAULT-MATRIX row-9 promise (\"rc reconnects\") is\r\n/// made TRUE here, not edited down (triage F-3).\r\n///\r\n/// The SEAM (doyle rig ruling #4): everything terminal-owning stays at the\r\n/// interactive caller — raw-mode guard, stdin-reader spawn, StatusRow\r\n/// construction/teardown, the parting copy. This fn only needs an established\r\n/// attach + an `out` sink, so the two-host rig can drive the REAL loop headless\r\n/// (`out` = a capture buffer) — no behavior change for the interactive path.\r\n/// `remote_node` is caller-owned so the parting copy names the LAST node the\r\n/// viewport was attached to (a reconnect can re-resolve it).\r\n#[allow(clippy::too_many_arguments)]\r\nfn attach_viewport(\r\n    endpoint_id: &str,\r\n    intent: AttachIntent,\r\n    view: bool,\r\n    mut est: Established,\r\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\r\n    mouse_mode: &MouseMode,\r\n    mut status: Option<&mut StatusRow>,\r\n    remote_node: &mut Option<String>,\r\n    out: &mut impl std::io::Write,\r\n) -> Result<PumpEnd, String> {\r\n    loop {\r\n        // Controller: send the INITIAL terminal size so the PTY matches from the\r\n        // start (REQ-RCVIEW-1). Best-effort — a failure just leaves the default\r\n        // size. When the status row is active the harness PTY gets rows-1 (we own\r\n        // row 1), never 0 (REQ-RC-IDENTITY). Re-sent per establish: the new\r\n        // viewport's PTY must match our CURRENT terminal, not the original one.\r\n        if !view {\r\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\r\n                let pty_rows = if status.is_some() {\r\n                    rows.saturating_sub(1).max(1)\r\n                } else {\r\n                    rows\r\n                };\r\n                let _ = send_attach_resize(&mut est.brain, est.stream_id, pty_rows, cols);\r\n            }\r\n        }\r\n\r\n        let mut decoder = AttachDecoder::new();\r\n        let end = pump(\r\n            &mut est.brain,\r\n            est.stream_id,\r\n            &est.ops,\r\n            rx,\r\n            &mut decoder,\r\n            out,\r\n            !view, // controller drives resize\r\n            mouse_mode,\r\n            status.as_deref_mut(),\r\n        );\r\n\r\n        // Final ends (incl. hard errors) exit the loop to the parting copy.\r\n        let severed_kind = match &end {\r\n            Ok(e) if reconnect_class(e) == EndClass::Reconnect => match e {\r\n                PumpEnd::BrokerGone => \"the daemon connection dropped\",\r\n                _ => \"the connection was severed\",\r\n            },\r\n            _ => return end,\r\n        };\r\n\r\n        // Reconnect: full-screen centered banner (operator language — the internal\r\n        // detail stays off the terminal per the Q4 UX rule), then re-drive\r\n        // establish_attach inside the bounded window. A detach keypress mid-window\r\n        // aborts the retry loop honestly. The banner repaints each iteration with a\r\n        // live COUNTDOWN (REQ-RC-RECONNECT-TRUTH) so the operator sees rc is bounded\r\n        // and working, never the old apparent freeze at a static \"Reconnecting…\".\r\n        // establish_attach is CONNECT-ONLY: a down daemon yields DaemonDown here and\r\n        // is retried (the operator may bring it back) WITHOUT any WMI resurrection.\r\n        let target = remote_node.clone().unwrap_or_else(|| \"local daemon\".to_string());\r\n        let retry_started = Instant::now();\r\n        let reestablished = loop {\r\n            let elapsed = retry_started.elapsed();\r\n            if reconnect_expired(elapsed) {\r\n                break None;\r\n            }\r\n            let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\r\n            let remaining = reconnect_remaining_secs(elapsed);\r\n            let _ = out.write_all(&reconnect_banner_bytes(rows, cols, &target, remaining));\r\n            let _ = out.flush();\r\n            // Operator gives up mid-reconnect: honor the detach keys.\r\n            if let Some(rx) = rx {\r\n                let mut detached = false;\r\n                while let Ok(msg) = rx.try_recv() {\r\n                    if matches!(msg, StdinMsg::Detach) {\r\n                        detached = true;\r\n                    }\r\n                }\r\n                if detached {\r\n                    break Some(Err(())); // detach sentinel\r\n                }\r\n            }\r\n            match establish_attach(endpoint_id, intent) {\r\n                Ok(next) => break Some(Ok(next)),\r\n                // Every fail shape retries until the window expires — the node/daemon\r\n                // may still be coming back. DaemonDown included: rc waits for the\r\n                // operator to restart it, it does NOT auto-start one.\r\n                Err(_) => std::thread::sleep(RECONNECT_PAUSE),\r\n            }\r\n        };\r\n        match reestablished {\r\n            Some(Ok(next)) => {\r\n                *remote_node = next.remote_node.clone();\r\n                est = next;\r\n                // Loop: fresh resize + pump. The re-serve replays the output ring,\r\n                // repainting the screen the banner cleared.\r\n            }\r\n            Some(Err(())) => return Ok(PumpEnd::Detached),\r\n            None => {\r\n                // Give-up copy selection (REQ-RC-RECONNECT-TRUTH): a still-down local\r\n                // daemon (only knowable for a LOCAL target) gets the loud \"session\r\n                // lost — daemon down\" copy; anything else keeps the generic\r\n                // didn't-reconnect copy. Remote severs never probe local daemon state.\r\n                let daemon_down =\r\n                    remote_node.is_none() && !spt_daemon::daemon::is_running();\r\n                return Ok(PumpEnd::ReconnectGaveUp {\r\n                    detail: severed_kind.to_string(),\r\n                    daemon_down,\r\n                });\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// How long the B-3 reconnect loop keeps re-driving `establish_attach` after a\r\n/// severed viewport before the plain-language give-up (REQ-RC-RECONNECT).\r\n/// Generous: a daemon bounce (`spt daemon stop` + the next auto-start) and a\r\n/// node's broker restart both land well inside it.\r\nconst RECONNECT_WINDOW: Duration = Duration::from_secs(30);\r\n/// The pause between reconnect attempts (each attempt already carries its own\r\n/// bounded setup deadlines — this just keeps the loop from hammering).\r\nconst RECONNECT_PAUSE: Duration = Duration::from_secs(1);\r\n\r\n/// The give-up decision, pulled out so it unit-tests without a 30s wait.\r\n// [impl->REQ-RC-RECONNECT]\r\nfn reconnect_expired(elapsed: Duration) -> bool {\r\n    elapsed >= RECONNECT_WINDOW\r\n}\r\n\r\n/// Seconds still left in the reconnect window, for the banner countdown\r\n/// (REQ-RC-RECONNECT-TRUTH). CEIL so the display reads a full `RECONNECT_WINDOW`s\r\n/// at the start and never flashes `0s` while the loop is still trying (0 only once\r\n/// the window is fully spent, where the caller has already broken to give-up).\r\n// [impl->REQ-RC-RECONNECT-TRUTH]\r\nfn reconnect_remaining_secs(elapsed: Duration) -> u64 {\r\n    let left = RECONNECT_WINDOW.saturating_sub(elapsed);\r\n    // ceil(left / 1s) via integer millis — 1..=WINDOW while any time remains, 0 at/past.\r\n    (left.as_millis() as u64).div_ceil(1000)\r\n}\r\n\r\n/// Which pump ends re-drive the viewport (B-3) vs. exit with parting copy.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\nenum EndClass {\r\n    Final,\r\n    Reconnect,\r\n}\r\n\r\n/// A severed transport is RECONNECTABLE: the broker-conn EOF class (broker\r\n/// bounce) and the serve-side EOF after rendered output (remote conn drop).\r\n/// Everything else — session exit, operator detach, displacement, the stall\r\n/// backstop, an attach refuse — is FINAL: re-driving those would re-attach to a\r\n/// session the operator (or the session itself) deliberately ended.\r\n// [impl->REQ-RC-RECONNECT]\r\nfn reconnect_class(end: &PumpEnd) -> EndClass {\r\n    match end {\r\n        PumpEnd::BrokerGone | PumpEnd::Severed => EndClass::Reconnect,\r\n        PumpEnd::Exited(_)\r\n        | PumpEnd::Detached\r\n        | PumpEnd::Displaced(_)\r\n        | PumpEnd::Stalled\r\n        | PumpEnd::NoLiveSession\r\n        | PumpEnd::ReconnectGaveUp { .. } => EndClass::Final,\r\n    }\r\n}\r\n\r\n/// Classify a serve-side stream EOF: rendered output first ⇒ the conn was\r\n/// SEVERED under a live viewport (reconnect); nothing ever rendered ⇒ the serve\r\n/// REFUSED the attach (no live session / gate deny — the cross-node D6\r\n/// stale-row case), honest \"no live session\" instead of a false \"detached\"\r\n/// (#4, REQ-RC-CROSS-NODE-ATTACH).\r\n// [impl->REQ-RC-RECONNECT]\r\nfn serve_eof_end(rendered_any: bool) -> PumpEnd {\r\n    if rendered_any {\r\n        PumpEnd::Severed\r\n    } else {\r\n        PumpEnd::NoLiveSession\r\n    }\r\n}\r\n\r\n/// The full-screen reconnect banner (B-3): clear + home, then the operator-language\r\n/// line centered on the terminal. Pure byte-emit (like [`StatusRow`]) so the exact\r\n/// VT shape unit-tests directly; the caller writes the bytes. The text is plain\r\n/// operator language (Q4 UX rule) — the internal sever detail never paints here.\r\n// [impl->REQ-RC-RECONNECT]\r\nfn reconnect_banner_bytes(rows: u16, cols: u16, target: &str, remaining_secs: u64) -> Vec<u8> {\r\n    // Visible countdown (REQ-RC-RECONNECT-TRUTH): the remaining-seconds suffix turns\r\n    // the old static banner into a live \"rc is working, bounded\" signal — the operator\r\n    // never mistakes a bounded retry for the historical freeze-forever.\r\n    let text = format!(\"Reconnecting to {target}… {remaining_secs}s\");\r\n    let row = (rows / 2).max(1);\r\n    // Center on the CHARACTER count (the terminal cell count for this ASCII+…\r\n    // line), clamped so an over-wide text still starts on-screen.\r\n    let width = text.chars().count() as u16;\r\n    let col = if width >= cols { 1 } else { (cols - width) / 2 + 1 };\r\n    let mut out = Vec::new();\r\n    out.extend_from_slice(b\"\\x1b[2J\\x1b[H\");\r\n    out.extend_from_slice(format!(\"\\x1b[{row};{col}H\").as_bytes());\r\n    out.extend_from_slice(text.as_bytes());\r\n    out\r\n}\r\n\r\n#[derive(Debug)]\r\nenum PumpEnd {\r\n    Exited(Option<i32>),\r\n    Detached,\r\n    /// A different operator took control (REQ-KICK-1); carries the taker's node.\r\n    Displaced(String),\r\n    /// The broker stream was severed mid-session (broker bounce / hang-up — e.g. a\r\n    /// `spt daemon stop`). Recoverable, not a crash: the broker is the\r\n    /// daemon-lifetime anchor and returns on the next `spt api` call, so this\r\n    /// enters the B-3 reconnect loop (REQ-RC-RECONNECT) rather than exiting.\r\n    BrokerGone,\r\n    /// The serve side finished the stream AFTER rendering output — the remote\r\n    /// conn dropped (node bounce, network cut), NOT an operator detach (that\r\n    /// returns via the stdin `Detach` path directly) and NOT a session end (an\r\n    /// `Exit` record precedes a session-death EOF). Pre-B-3 this was mislabeled\r\n    /// \"detached — still running\"; it is the SEVERED case the reconnect loop\r\n    /// re-drives. [impl->REQ-RC-RECONNECT]\r\n    Severed,\r\n    /// B-3 terminal state: the reconnect window expired without a successful\r\n    /// re-attach. `detail` is the operator-language cause of the ORIGINAL sever\r\n    /// (Q4 UX rule — never read-error lingo). `daemon_down` records whether the\r\n    /// local daemon was still down at give-up (REQ-RC-RECONNECT-TRUTH) — it\r\n    /// selects the loud \"session lost — daemon down\" copy over the generic\r\n    /// didn't-reconnect copy. [impl->REQ-RC-RECONNECT] [impl->REQ-RC-RECONNECT-TRUTH]\r\n    ReconnectGaveUp { detail: String, daemon_down: bool },\r\n    /// The attach produced NO event whatsoever within the generous first-event\r\n    /// backstop window (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a healthy session —\r\n    /// even one still painting / mid-init — replays its buffered output (or at\r\n    /// least a Subscribed/Size frame) promptly, so a totally silent stream past\r\n    /// this bound is a dead/wedged session, not a slow one. Surfaces a clear\r\n    /// message instead of the historical infinite blank. The bound is deliberately\r\n    /// LONG (only ever bites a pathological hang; the broker-EOF-on-dead-child path\r\n    /// and the status-gate catch the ordinary dead cases first).\r\n    Stalled,\r\n    /// The serve side finished the stream having produced NO output — i.e. it\r\n    /// refused the attach (no live session for the endpoint, or the access gate\r\n    /// denied us). On the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH) this is\r\n    /// the D6 stale-row case: the registry routed us to a node that no longer has\r\n    /// the session. Distinct from [`Detached`](PumpEnd::Detached) (the OPERATOR\r\n    /// ended a live viewport) — here the REMOTE closed on us before anything ran,\r\n    /// so it renders an honest \"no live session\" instead of a false \"detached\".\r\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n    NoLiveSession,\r\n}\r\n\r","truncated":true,"truncatedBy":"lines","totalLines":2136,"totalBytes":34620,"outputLines":685,"outputBytes":34620,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// takes control. Returns when the session exits, the operator detaches, or an\r\n/// error occurs.\r\n// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\r\npub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\r\n    run_attach_inner(endpoint_id, intent, false)\r\n}\r\n\r\n/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\r\n/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\r\n/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\r\n/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\r\n/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND\r\n/// stamp), yet the confirmed live session IS attachable — \"attach to a live\r\n/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).\r\n/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a\r\n/// session that dies after confirmation, so this is not a blank-hang reopening.\r\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\r\npub fn run_attach_session_confirmed(\r\n    endpoint_id: &str,\r\n    intent: AttachIntent,\r\n) -> Result<(), String> {\r\n    run_attach_inner(endpoint_id, intent, true)\r\n}\r\n\r\nfn run_attach_inner(\r\n    endpoint_id: &str,\r\n    intent: AttachIntent,\r\n    session_confirmed: bool,\r\n) -> Result<(), String> {\r\n    let view = intent == AttachIntent::Viewer;\r\n    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\r\n    // an endpoint a DIFFERENT node already controls gets actionable guidance here\r\n    // (the broker would also refuse on the wire — this is the friendly path). A\r\n    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\r\n    if intent == AttachIntent::Control {\r\n        if let Some(node) = current_driver(endpoint_id) {\r\n            println!(\"'{endpoint_id}' is controlled by {node}.\");\r\n            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\r\n            println!(\"  spt rc {endpoint_id} --take   to take control (kicks {node})\");\r\n            return Ok(());\r\n        }\r\n    }\r\n    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\r\n    // `status=offline` is not attachable — short-circuit with clean guidance\r\n    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\r\n    // instead of the broker dance that would block on a dead session (the\r\n    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\r\n    // spt-hosted perch offline, so this catches the common case before any IPC. An\r\n    // ABSENT or status-less perch falls through to the broker path, which already\r\n    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\r\n    // is NOT used here: it returns true for an absent info.json by interim parity.)\r\n    //\r\n    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\r\n    // NOT on STATUS_ONLINE (REQ-ENDPOINT-UNBOUND-ATTACH). A STATUS_UNBOUND skeleton\r\n    // (session spawned, perch not yet bound) is `!= offline`, so it falls through\r\n    // here and attaches via the live-session resolve below — the headless /\r\n    // clear-a-bind-prompt path. `offline` is stamped only on session DEATH, so it\r\n    // never co-occurs with a live session.\r\n    //\r\n    // KNOWN BOUNDARY (doyle, W4): a RESUMING perch is still `offline` during its\r\n    // pre-bind window — the W2 skeleton-write short-circuits on an existing perch,\r\n    // so resume gets NO UNBOUND stamp. A standalone `spt rc <id>` against it hits\r\n    // this failfast (\"offline — start it\") rather than waiting for the bind, and\r\n    // retries fine once the bind flips the perch online. The `endpoint run\r\n    // --attach` path does NOT hit it: it arrives via run_attach_session_confirmed\r\n    // (session already proven live by await_endpoint_session), so the gate is\r\n    // SKIPPED — it attaches to the confirmed live session even while the perch is\r\n    // still stamped offline mid-rebringup. Accepted, not contorted around.\r\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\r\n    let perch_path =\r\n        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);\r\n    if !session_confirmed\r\n        && spt_store::info::read_info(&perch_path)\r\n            .and_then(|i| i.status)\r\n            .as_deref()\r\n            == Some(spt_store::liveness::STATUS_OFFLINE)\r\n    {\r\n        println!(\"Endpoint '{endpoint_id}' is offline — nothing to attach to.\");\r\n        println!(\"  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it\");\r\n        return Ok(());\r\n    }\r\n    // Establish the attach (broker conn → session resolve → dial → attach-open →\r\n    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\r\n    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\r\n    // keeps today's clean-exit copy on a no-target miss.\r\n    let est = match establish_attach(endpoint_id, intent) {\r\n        Ok(est) => est,\r\n        Err(EstablishFail::NoTarget(msg)) => {\r\n            println!(\"{msg}\");\r\n            return Ok(());\r\n        }\r\n        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\r\n        // rc won't birth one. Plain operator language — what's wrong, what to do —\r\n        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\r\n        // [impl->REQ-RC-RECONNECT-TRUTH]\r\n        Err(EstablishFail::DaemonDown) => {\r\n            println!(\r\n                \"The spt daemon isn't running — nothing to attach to. \\\r\n                 Start it with `spt daemon start`, then `spt rc {endpoint_id}`.\"\r\n            );\r\n            return Ok(());\r\n        }\r\n        Err(EstablishFail::Error(e)) => return Err(e),\r\n    };\r\n    let mut remote_node: Option<String> = est.remote_node.clone();\r\n\r\n    let _raw = RawGuard::enable();\r\n    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\r\n    // pump and read by the Windows stdin reader to gate scroll forwarding\r\n    // (REQ-RC-MOUSE-FORWARD).\r\n    let mouse_mode = std::sync::Arc::new(MouseMode::default());\r\n    let rx = if view {\r\n        None\r\n    } else {\r\n        let (tx, rx) = mpsc::channel();\r\n        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\r\n        Some(rx)\r\n    };\r\n\r\n    // Controller status row (REQ-RC-IDENTITY): the operator-side identity banner\r\n    // on the real terminal's reserved row 1 — controller only (a viewer's viewport\r\n    // is read-only and may letterbox, so it owns no row). Resolved ONCE here.\r\n    // [impl->REQ-RC-IDMARKER-DISABLE]\r\n    let mut status = if status_row_active(view) {\r\n        let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\r\n        Some(StatusRow {\r\n            text: resolve_identity_line(endpoint_id),\r\n            rows,\r\n            cols,\r\n        })\r\n    } else {\r\n        None\r\n    };\r\n\r\n    let mut stdout = std::io::stdout();\r\n    let outcome = attach_viewport(\r\n        endpoint_id,\r\n        intent,\r\n        view,\r\n        est,\r\n        rx.as_ref(),\r\n        &mouse_mode,\r\n        status.as_mut(),\r\n        &mut remote_node,\r\n        &mut stdout,\r\n    );\r\n\r\n    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\r\n    // leaving raw mode, on every pump-exit path (REQ-RC-IDENTITY).\r\n    if status.is_some() {\r\n        let _ = stdout.write_all(&StatusRow::restore_bytes());\r\n        let _ = stdout.flush();\r\n    }\r\n    // Out of raw mode (guard drops after) before the parting line.\r\n    drop(_raw);\r\n    match outcome {\r\n        Ok(PumpEnd::Exited(code)) => {\r\n            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\r\n            Ok(())\r\n        }\r\n        Ok(PumpEnd::Detached) => {\r\n            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\r\n            Ok(())\r\n        }\r\n        Ok(PumpEnd::Displaced(by)) => {\r\n            // The loud kick notice (REQ-KICK-1): a different operator took control.\r\n            let _ = writeln!(\r\n                stdout,\r\n                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\r\n            );\r\n            Ok(())\r\n        }\r\n        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\r\n            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\r\n            // successful re-attach. Plain operator language (Q4 UX rule): what\r\n            // happened, what it means, what to do next — never op/read-err lingo.\r\n            if daemon_down {\r\n                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\r\n                // went down and never came back — rc did NOT resurrect it. Name that\r\n                // plainly so the operator restarts the daemon rather than guessing.\r\n                let _ = writeln!(\r\n                    stdout,\r\n                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\r\n                     didn't come back within {}s. rc won't auto-start it. Bring it \\\r\n                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\r\n                    RECONNECT_WINDOW.as_secs()\r\n                );\r\n            } else {\r\n                let _ = writeln!(\r\n                    stdout,\r\n                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\r\n                     within {}s — the session may still be running. Try `spt rc \\\r\n                     {endpoint_id}` again once the node/daemon is back]\",\r\n                    RECONNECT_WINDOW.as_secs()\r\n                );\r\n            }\r\n            Ok(())\r\n        }\r\n        // Reconnectable ends never escape the loop (they enter the retry arm and\r\n        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\r\n        // so a future PumpEnd variant still forces a decision here.\r\n        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\r\n            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\r\n        }\r\n        Ok(PumpEnd::Stalled) => {\r\n            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced\r\n            // no output at all — a dead/wedged session, not a slow one. A clear\r\n            // message, never the historical infinite blank.\r\n            let _ = writeln!(\r\n                stdout,\r\n                \"\\r\\n[no output from '{endpoint_id}' — the session may be dead or \\\r\n                 wedged. Detached. Try `spt endpoint list` for its state, or \\\r\n                 `spt endpoint run` to restart it]\"\r\n            );\r\n            Ok(())\r\n        }\r\n        Ok(PumpEnd::NoLiveSession) => {\r\n            // The serve refused before anything ran (#4, REQ-RC-CROSS-NODE-ATTACH):\r\n            // no live session for the endpoint on the node we reached, or its gate\r\n            // denied us. On the cross-node leg this is the D6 stale-row case.\r\n            match &remote_node {\r\n                Some(node) => {\r\n                    let _ = writeln!(\r\n                        stdout,\r\n                        \"\\r\\n[no live session for '{endpoint_id}' on {node} — it may \\\r\n                         have ended, or the registry row is stale]\"\r\n                    );\r\n                }\r\n                None => {\r\n                    let _ = writeln!(stdout, \"\\r\\n[no live session for '{endpoint_id}']\");\r\n                }\r\n            }\r\n            Ok(())\r\n        }\r\n        Err(e) => Err(public_attach_failure(endpoint_id, e)),\r\n    }\r\n}\r\n\r\n/// F-1 (REQ-PUBLIC-ERROR-SURFACES): the residual attach-failure line is the one\r\n/// rc surface that still leaked transport lingo (\"brain IPC read deadline\r\n/// elapsed\") to an operator. Map the known daemon-didn't-answer class to the\r\n/// observable situation + next action; every OTHER error passes through\r\n/// verbatim (no over-matching — unknown failures keep their detail). Pure, so\r\n/// the mapping is unit-tested.\r\n// [impl->REQ-PUBLIC-ERROR-SURFACES]\r\nfn public_attach_failure(endpoint_id: &str, e: String) -> String {\r\n    if e.contains(\"brain IPC read deadline\") {\r\n        format!(\r\n            \"the daemon didn't answer in time while attaching to '{endpoint_id}' — \\\r\n             it may be busy or mid-restart. Try `spt rc {endpoint_id}` again; if it \\\r\n             keeps happening, `spt endpoint list` shows the endpoint's state\"\r\n        )\r\n    } else {\r\n        e\r\n    }\r\n}\r\n\r\n/// An established attach viewport: the broker conn, the open+subscribed attach\r\n/// stream, and the op counter that minted it. One per (re)connect — the B-3\r\n/// reconnect loop re-mints the WHOLE bundle (a broker restart drops the conn,\r\n/// not just the stream; a fresh OpMinter keeps ADR-0034 `rc` op tracing per\r\n/// viewport). [impl->REQ-RC-RECONNECT]\r\nstruct Established {\r\n    brain: Brain,\r\n    ops: OpMinter,\r\n    stream_id: u64,\r\n    /// The owning node's label for a REMOTE attach (the reconnect-banner target\r\n    /// + the honest-failure copy); `None` ⇒ local.\r\n    remote_node: Option<String>,\r\n}\r\n\r\n/// Why [`establish_attach`] failed.\r\nenum EstablishFail {\r\n    /// No attachable target (no live session anywhere / ambiguous / node\r\n    /// unreachable) — the carried message is the CLEAN first-attempt exit copy;\r\n    /// the reconnect loop retries it inside the window instead (the node may\r\n    /// still be coming back).\r\n    NoTarget(String),\r\n    /// The daemon isn't running — rc is CONNECT-ONLY and refuses to auto-start it\r\n    /// (REQ-RC-RECONNECT-TRUTH). On the initial attach this is a loud \"daemon\r\n    /// isn't running — start it\" exit; inside the reconnect loop it's just another\r\n    /// retry (the operator may bring the daemon back), never a resurrection.\r\n    /// [impl->REQ-RC-RECONNECT-TRUTH]\r\n    DaemonDown,\r\n    /// A setup error (broker connect / dial / attach-open / subscribe).\r\n    Error(String),\r\n}\r\n\r\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\r\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\r\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\r\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\r\n///\r\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\r\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\r\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\r\n/// the broker wires an in-process loopback conn whose streams surface as the\r\n/// SAME net-stream events the cross-node QUIC path produces. From\r\n/// `request_attach` down this is byte-identical to a cross-node attach — the\r\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\r\n/// The loopback peer's origin is this node's own id, so the target's serve gates\r\n/// same-node.\r\n///\r\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\r\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\r\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\r\n/// identically to loopback — so from here down the two legs converge. Honest\r\n/// failure on every resolve/dial miss: a clean message, never a false attach or\r\n/// a hang (D2/D6).\r\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\r\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\r\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\r\n    // launcher. It must NEVER auto-start the broker — the historical\r\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\r\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\r\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\r\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\r\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\r\n    // broker cold-connect below is already deadline-bounded, so a down daemon\r\n    // never hangs.\r\n    // [impl->REQ-RC-RECONNECT-TRUTH]\r\n    if !spt_daemon::daemon::is_running() {\r\n        return Err(EstablishFail::DaemonDown);\r\n    }\r\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\r\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\r\n    // pipes), so the single-threaded pump interleaves stdin + broker events\r\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\r\n    // sessions, attach-open ack); the pump itself passes its own short slice.\r\n    let mut brain = Brain::cold_start_pump(\r\n        &spt_daemon::endpoint::broker_socket_name(),\r\n        now_ms(),\r\n        Duration::from_secs(10),\r\n    )\r\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\r\n\r\n    let ops = OpMinter::new();\r\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\r\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\r\n    // resolve the OWNING node from the registry and dial it, letting THAT node\r\n    // resolve endpoint→its session server-side.\r\n    let local_session = resolve_session(&mut brain, endpoint_id)\r\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\r\n    let mut remote_node: Option<String> = None;\r\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\r\n        Some(sid) => {\r\n            let dialed = brain\r\n                .net_dial_loopback()\r\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\r\n            (dialed.conn_id, sid, None)\r\n        }\r\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\r\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\r\n                remote_node = Some(node);\r\n                // session_id is a placeholder the owning node ignores — it\r\n                // resolves the endpoint to its OWN session id server-side.\r\n                (conn_id, 0, Some(endpoint_id))\r\n            }\r\n            crate::wansend::OwnerDial::NotFound => {\r\n                return Err(EstablishFail::NoTarget(format!(\r\n                    \"No live session for '{endpoint_id}' — not running on this node \\\r\n                     or any visible subnet.\"\r\n                )))\r\n            }\r\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\r\n                return Err(EstablishFail::NoTarget(msg))\r\n            }\r\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\r\n                return Err(EstablishFail::NoTarget(format!(\r\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\r\n                )))\r\n            }\r\n        },\r\n    };\r\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\r\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\r\n    // ONCE more. The first attempt uses the conn already established above (happy\r\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\r\n    // drops the CONN too, not just the stream, so opening against the dead conn\r\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\r\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\r\n    let mut established: Option<u64> = Some(conn_id);\r\n    let stream_id = spt_daemon::with_tracing_retry(\r\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\r\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\r\n        |op| {\r\n            let cid = match established.take() {\r\n                Some(c) => c,\r\n                None => match local_session {\r\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\r\n                    Some(_) => brain\r\n                        .net_dial_loopback()\r\n                        .map_err(|e| {\r\n                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\r\n                        })?\r\n                        .conn_id,\r\n                    // Remote: re-resolve + re-dial the owning node.\r\n                    None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\r\n                        crate::wansend::OwnerDial::Dialed { conn_id, .. } => conn_id,\r\n                        _ => {\r\n                            return Err(std::io::Error::other(format!(\r\n                                \"'{endpoint_id}' is no longer reachable\"\r\n                            )))\r\n                        }\r\n                    },\r\n                },\r\n            };\r\n            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\r\n        },\r\n    )\r\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\r\n    // Subscribe our side of the stream so the target's `Output` records surface\r\n    // as `NetStreamData` events (the open + Request do not auto-subscribe).\r\n    brain\r\n        .net_stream_subscribe(stream_id, 0)\r\n        .map_err(|e| EstablishFail::Error(format!(\"stream subscribe: {e}\")))?;\r\n    Ok(Established {\r\n        brain,\r\n        ops,\r\n        stream_id,\r\n        remote_node,\r\n    })\r\n}\r\n\r\n/// Drive an established viewport to its FINAL end: the B-3 reconnect loop\r\n/// (REQ-RC-RECONNECT) — each pass is one full viewport (initial resize → pump).\r\n/// A severed viewport (broker bounce, remote conn drop — the reconnectable\r\n/// class) paints a centered reconnect banner and RE-DRIVES [`establish_attach`]\r\n/// inside a bounded window; every other end is FINAL and returns to the\r\n/// caller's parting copy. The FAULT-MATRIX row-9 promise (\"rc reconnects\") is\r\n/// made TRUE here, not edited down (triage F-3).\r\n///\r\n/// The SEAM (doyle rig ruling #4): everything terminal-owning stays at the\r\n/// interactive caller — raw-mode guard, stdin-reader spawn, StatusRow\r\n/// construction/teardown, the parting copy. This fn only needs an established\r\n/// attach + an `out` sink, so the two-host rig can drive the REAL loop headless\r\n/// (`out` = a capture buffer) — no behavior change for the interactive path.\r\n/// `remote_node` is caller-owned so the parting copy names the LAST node the\r\n/// viewport was attached to (a reconnect can re-resolve it).\r\n#[allow(clippy::too_many_arguments)]\r\nfn attach_viewport(\r\n    endpoint_id: &str,\r\n    intent: AttachIntent,\r\n    view: bool,\r\n    mut est: Established,\r\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\r\n    mouse_mode: &MouseMode,\r\n    mut status: Option<&mut StatusRow>,\r\n    remote_node: &mut Option<String>,\r\n    out: &mut impl std::io::Write,\r\n) -> Result<PumpEnd, String> {\r\n    loop {\r\n        // Controller: send the INITIAL terminal size so the PTY matches from the\r\n        // start (REQ-RCVIEW-1). Best-effort — a failure just leaves the default\r\n        // size. When the status row is active the harness PTY gets rows-1 (we own\r\n        // row 1), never 0 (REQ-RC-IDENTITY). Re-sent per establish: the new\r\n        // viewport's PTY must match our CURRENT terminal, not the original one.\r\n        if !view {\r\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\r\n                let pty_rows = if status.is_some() {\r\n                    rows.saturating_sub(1).max(1)\r\n                } else {\r\n                    rows\r\n                };\r\n                let _ = send_attach_resize(&mut est.brain, est.stream_id, pty_rows, cols);\r\n            }\r\n        }\r\n\r\n        let mut decoder = AttachDecoder::new();\r\n        let end = pump(\r\n            &mut est.brain,\r\n            est.stream_id,\r\n            &est.ops,\r\n            rx,\r\n            &mut decoder,\r\n            out,\r\n            !view, // controller drives resize\r\n            mouse_mode,\r\n            status.as_deref_mut(),\r\n        );\r\n\r\n        // Final ends (incl. hard errors) exit the loop to the parting copy.\r\n        let severed_kind = match &end {\r\n            Ok(e) if reconnect_class(e) == EndClass::Reconnect => match e {\r\n                PumpEnd::BrokerGone => \"the daemon connection dropped\",\r\n                _ => \"the connection was severed\",\r\n            },\r\n            _ => return end,\r\n        };\r\n\r\n        // Reconnect: full-screen centered banner (operator language — the internal\r\n        // detail stays off the terminal per the Q4 UX rule), then re-drive\r\n        // establish_attach inside the bounded window. A detach keypress mid-window\r\n        // aborts the retry loop honestly. The banner repaints each iteration with a\r\n        // live COUNTDOWN (REQ-RC-RECONNECT-TRUTH) so the operator sees rc is bounded\r\n        // and working, never the old apparent freeze at a static \"Reconnecting…\".\r\n        // establish_attach is CONNECT-ONLY: a down daemon yields DaemonDown here and\r\n        // is retried (the operator may bring it back) WITHOUT any WMI resurrection.\r\n        let target = remote_node.clone().unwrap_or_else(|| \"local daemon\".to_string());\r\n        let retry_started = Instant::now();\r\n        let reestablished = loop {\r\n            let elapsed = retry_started.elapsed();\r\n            if reconnect_expired(elapsed) {\r\n                break None;\r\n            }\r\n            let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\r\n            let remaining = reconnect_remaining_secs(elapsed);\r\n            let _ = out.write_all(&reconnect_banner_bytes(rows, cols, &target, remaining));\r\n            let _ = out.flush();\r\n            // Operator gives up mid-reconnect: honor the detach keys.\r\n            if let Some(rx) = rx {\r\n                let mut detached = false;\r\n                while let Ok(msg) = rx.try_recv() {\r\n                    if matches!(msg, StdinMsg::Detach) {\r\n                        detached = true;\r\n                    }\r\n                }\r\n                if detached {\r\n                    break Some(Err(())); // detach sentinel\r\n                }\r\n            }\r\n            match establish_attach(endpoint_id, intent) {\r\n                Ok(next) => break Some(Ok(next)),\r\n                // Every fail shape retries until the window expires — the node/daemon\r\n                // may still be coming back. DaemonDown included: rc waits for the\r\n                // operator to restart it, it does NOT auto-start one.\r\n                Err(_) => std::thread::sleep(RECONNECT_PAUSE),\r\n            }\r\n        };\r\n        match reestablished {\r\n            Some(Ok(next)) => {\r\n                *remote_node = next.remote_node.clone();\r\n                est = next;\r\n                // Loop: fresh resize + pump. The re-serve replays the output ring,\r\n                // repainting the screen the banner cleared.\r\n            }\r\n            Some(Err(())) => return Ok(PumpEnd::Detached),\r\n            None => {\r\n                // Give-up copy selection (REQ-RC-RECONNECT-TRUTH): a still-down local\r\n                // daemon (only knowable for a LOCAL target) gets the loud \"session\r\n                // lost — daemon down\" copy; anything else keeps the generic\r\n                // didn't-reconnect copy. Remote severs never probe local daemon state.\r\n                let daemon_down =\r\n                    remote_node.is_none() && !spt_daemon::daemon::is_running();\r\n                return Ok(PumpEnd::ReconnectGaveUp {\r\n                    detail: severed_kind.to_string(),\r\n                    daemon_down,\r\n                });\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// How long the B-3 reconnect loop keeps re-driving `establish_attach` after a\r\n/// severed viewport before the plain-language give-up (REQ-RC-RECONNECT).\r\n/// Generous: a daemon bounce (`spt daemon stop` + the next auto-start) and a\r\n/// node's broker restart both land well inside it.\r\nconst RECONNECT_WINDOW: Duration = Duration::from_secs(30);\r\n/// The pause between reconnect attempts (each attempt already carries its own\r\n/// bounded setup deadlines — this just keeps the loop from hammering).\r\nconst RECONNECT_PAUSE: Duration = Duration::from_secs(1);\r\n\r\n/// The give-up decision, pulled out so it unit-tests without a 30s wait.\r\n// [impl->REQ-RC-RECONNECT]\r\nfn reconnect_expired(elapsed: Duration) -> bool {\r\n    elapsed >= RECONNECT_WINDOW\r\n}\r\n\r\n/// Seconds still left in the reconnect window, for the banner countdown\r\n/// (REQ-RC-RECONNECT-TRUTH). CEIL so the display reads a full `RECONNECT_WINDOW`s\r\n/// at the start and never flashes `0s` while the loop is still trying (0 only once\r\n/// the window is fully spent, where the caller has already broken to give-up).\r\n// [impl->REQ-RC-RECONNECT-TRUTH]\r\nfn reconnect_remaining_secs(elapsed: Duration) -> u64 {\r\n    let left = RECONNECT_WINDOW.saturating_sub(elapsed);\r\n    // ceil(left / 1s) via integer millis — 1..=WINDOW while any time remains, 0 at/past.\r\n    (left.as_millis() as u64).div_ceil(1000)\r\n}\r\n\r\n/// Which pump ends re-drive the viewport (B-3) vs. exit with parting copy.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\nenum EndClass {\r\n    Final,\r\n    Reconnect,\r\n}\r\n\r\n/// A severed transport is RECONNECTABLE: the broker-conn EOF class (broker\r\n/// bounce) and the serve-side EOF after rendered output (remote conn drop).\r\n/// Everything else — session exit, operator detach, displacement, the stall\r\n/// backstop, an attach refuse — is FINAL: re-driving those would re-attach to a\r\n/// session the operator (or the session itself) deliberately ended.\r\n// [impl->REQ-RC-RECONNECT]\r\nfn reconnect_class(end: &PumpEnd) -> EndClass {\r\n    match end {\r\n        PumpEnd::BrokerGone | PumpEnd::Severed => EndClass::Reconnect,\r\n        PumpEnd::Exited(_)\r\n        | PumpEnd::Detached\r\n        | PumpEnd::Displaced(_)\r\n        | PumpEnd::Stalled\r\n        | PumpEnd::NoLiveSession\r\n        | PumpEnd::ReconnectGaveUp { .. } => EndClass::Final,\r\n    }\r\n}\r\n\r\n/// Classify a serve-side stream EOF: rendered output first ⇒ the conn was\r\n/// SEVERED under a live viewport (reconnect); nothing ever rendered ⇒ the serve\r\n/// REFUSED the attach (no live session / gate deny — the cross-node D6\r\n/// stale-row case), honest \"no live session\" instead of a false \"detached\"\r\n/// (#4, REQ-RC-CROSS-NODE-ATTACH).\r\n// [impl->REQ-RC-RECONNECT]\r\nfn serve_eof_end(rendered_any: bool) -> PumpEnd {\r\n    if rendered_any {\r\n        PumpEnd::Severed\r\n    } else {\r\n        PumpEnd::NoLiveSession\r\n    }\r\n}\r\n\r\n/// The full-screen reconnect banner (B-3): clear + home, then the operator-language\r\n/// line centered on the terminal. Pure byte-emit (like [`StatusRow`]) so the exact\r\n/// VT shape unit-tests directly; the caller writes the bytes. The text is plain\r\n/// operator language (Q4 UX rule) — the internal sever detail never paints here.\r\n// [impl->REQ-RC-RECONNECT]\r\nfn reconnect_banner_bytes(rows: u16, cols: u16, target: &str, remaining_secs: u64) -> Vec<u8> {\r\n    // Visible countdown (REQ-RC-RECONNECT-TRUTH): the remaining-seconds suffix turns\r\n    // the old static banner into a live \"rc is working, bounded\" signal — the operator\r\n    // never mistakes a bounded retry for the historical freeze-forever.\r\n    let text = format!(\"Reconnecting to {target}… {remaining_secs}s\");\r\n    let row = (rows / 2).max(1);\r\n    // Center on the CHARACTER count (the terminal cell count for this ASCII+…\r\n    // line), clamped so an over-wide text still starts on-screen.\r\n    let width = text.chars().count() as u16;\r\n    let col = if width >= cols { 1 } else { (cols - width) / 2 + 1 };\r\n    let mut out = Vec::new();\r\n    out.extend_from_slice(b\"\\x1b[2J\\x1b[H\");\r\n    out.extend_from_slice(format!(\"\\x1b[{row};{col}H\").as_bytes());\r\n    out.extend_from_slice(text.as_bytes());\r\n    out\r\n}\r\n\r\n#[derive(Debug)]\r\nenum PumpEnd {\r\n    Exited(Option<i32>),\r\n    Detached,\r\n    /// A different operator took control (REQ-KICK-1); carries the taker's node.\r\n    Displaced(String),\r\n    /// The broker stream was severed mid-session (broker bounce / hang-up — e.g. a\r\n    /// `spt daemon stop`). Recoverable, not a crash: the broker is the\r\n    /// daemon-lifetime anchor and returns on the next `spt api` call, so this\r\n    /// enters the B-3 reconnect loop (REQ-RC-RECONNECT) rather than exiting.\r\n    BrokerGone,\r\n    /// The serve side finished the stream AFTER rendering output — the remote\r\n    /// conn dropped (node bounce, network cut), NOT an operator detach (that\r\n    /// returns via the stdin `Detach` path directly) and NOT a session end (an\r\n    /// `Exit` record precedes a session-death EOF). Pre-B-3 this was mislabeled\r\n    /// \"detached — still running\"; it is the SEVERED case the reconnect loop\r\n    /// re-drives. [impl->REQ-RC-RECONNECT]\r\n    Severed,\r\n    /// B-3 terminal state: the reconnect window expired without a successful\r\n    /// re-attach. `detail` is the operator-language cause of the ORIGINAL sever\r\n    /// (Q4 UX rule — never read-error lingo). `daemon_down` records whether the\r\n    /// local daemon was still down at give-up (REQ-RC-RECONNECT-TRUTH) — it\r\n    /// selects the loud \"session lost — daemon down\" copy over the generic\r\n    /// didn't-reconnect copy. [impl->REQ-RC-RECONNECT] [impl->REQ-RC-RECONNECT-TRUTH]\r\n    ReconnectGaveUp { detail: String, daemon_down: bool },\r\n    /// The attach produced NO event whatsoever within the generous first-event\r\n    /// backstop window (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a healthy session —\r\n    /// even one still painting / mid-init — replays its buffered output (or at\r\n    /// least a Subscribed/Size frame) promptly, so a totally silent stream past\r\n    /// this bound is a dead/wedged session, not a slow one. Surfaces a clear\r\n    /// message instead of the historical infinite blank. The bound is deliberately\r\n    /// LONG (only ever bites a pathological hang; the broker-EOF-on-dead-child path\r\n    /// and the status-gate catch the ordinary dead cases first).\r\n    Stalled,\r\n    /// The serve side finished the stream having produced NO output — i.e. it\r\n    /// refused the attach (no live session for the endpoint, or the access gate\r\n    /// denied us). On the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH) this is\r\n    /// the D6 stale-row case: the registry routed us to a node that no longer has\r\n    /// the session. Distinct from [`Detached`](PumpEnd::Detached) (the OPERATOR\r\n    /// ended a live viewport) — here the REMOTE closed on us before anything ran,\r\n    /// so it renders an honest \"no live session\" instead of a false \"detached\".\r\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n    NoLiveSession,\r\n}\r\n\r","startLine":1079,"lineNumbers":[1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":3214,"totalBytes":34620,"outputLines":685,"outputBytes":34620,"shownRange":{"start":1079,"end":1763},"nextOffset":1764}}},"isError":false,"timestamp":1784195763439}}
{"type":"message","id":"15dfb39d","parentId":"10d501fe","timestamp":"2026-07-16T09:56:03.440Z","message":{"role":"toolResult","toolCallId":"call_NOOuSCB4W4WcndEri4hnbjnP|fc_008390f508d1d09b016a58aab33758819abd20e86bec490f94","toolName":"read","content":[{"type":"text","text":"enum PumpEnd {\n…\n    /// so it renders an honest \"no live session\" instead of a false \"detached\".\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n    NoLiveSession,\n}\n\n/// Generous first-event backstop for the attach pump (REQ-HAZARD-RC-ATTACH-FAILFAST\n/// path b): if NO broker event arrives within this window after attach, the\n/// session is treated as dead/wedged. Long on purpose — a fresh, mid-init alive\n/// session (empty OutputLog, still painting) MUST pass, so this only catches a\n/// true hang, never the working fresh-attach.\nconst FIRST_EVENT_GRACE: Duration = Duration::from_secs(30);\n\n/// The path-b stall decision (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach has\n/// stalled iff NO broker event has ever arrived AND the generous grace has\n/// elapsed. Pulled out so the decision is unit-testable without a 30s pump run.\n// [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\nfn first_event_stalled(seen_any: bool, elapsed: Duration) -> bool {\n    !seen_any && elapsed >= FIRST_EVENT_GRACE\n}\n\n/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ReadDisposition {\n    /// Transient poll-slice timeout — loop and poll again.\n    Retry,\n    /// Severed broker stream (broker bounced / hung up) — recoverable, surface\n    /// gracefully.\n    BrokerGone,\n    /// A genuine read failure — propagate.\n    Fatal,\n}\n\n/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\n/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\n/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\n/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\n/// gracefully rather than crashing the PTY with the raw io text (the same EOF\n/// family the v0.9.1 seed fix handles); anything else is fatal.\n// [impl->REQ-HAZARD-RC-EOF]\nfn classify_read_err(kind: std::io::ErrorKind) -> ReadDisposition {\n    use std::io::ErrorKind::*;\n    match kind {\n        WouldBlock | TimedOut => ReadDisposition::Retry,\n        UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {\n            ReadDisposition::BrokerGone\n        }\n        _ => ReadDisposition::Fatal,\n    }\n}\n\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n#[allow(clippy::too_many_arguments)]\nfn pump(\n    brain: &mut Brain,\n    stream_id: u64,\n    ops: &OpMinter,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    decoder: &mut AttachDecoder,\n    stdout: &mut impl std::io::Write,\n    controller: bool,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n) -> Result<PumpEnd, String> {\n    // Track the harness's mouse-reporting mode from its output DECSET sequences so\n    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The\n    // scanner is pump-local (single producer) and survives a sequence split across\n    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.\n    let mut mouse_scanner = MouseModeScanner::default();\n    // The reserved-row re-assert scanner (REQ-RC-IDENTITY): watches the harness\n    // output for an alt-screen enter / DECSTBM reset that would destroy our\n    // scroll-region margin, so we can re-paint the status row. Pump-local, single\n    // producer, survives a trigger split across output chunks (like mouse_scanner).\n    let mut reassert_scanner = ReassertScanner::default();\n    // Reserve the top row + paint the identity banner up front (REQ-RC-IDENTITY).\n    // Controller-only (the caller passes `Some` only for a control attach).\n    if let Some(status) = status.as_deref_mut() {\n        stdout\n            .write_all(&status.assert_bytes())\n            .map_err(|e| format!(\"status assert: {e}\"))?;\n        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\n    }\n    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n    // (the session outlives the viewport). Best-effort: an already-finished or\n    // exited stream send is harmless to attempt.\n    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\n        let _ = brain.net_stream_send(stream_id, &[], None, true);\n        PumpEnd::Detached\n    }\n    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n    // drop any `seq` already rendered (the operator render cursor, matching the\n    // local `OutputLog` replay discipline).\n    let mut cursor = 0u64;\n    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size\n    // each slice and forward a Resize when it changes (cross-platform without\n    // hooking SIGWINCH / the crossterm event loop, which would conflict with the\n    // raw-stdin reader). `None` until the first read.\n    let mut last_size: Option<(u16, u16)> = None;\n    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown\n    // as a one-line indicator when it differs from our own terminal.\n    let mut pty_size: Option<(u16, u16)> = None;\n    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged\n    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE\n    // rather than an infinite blank. Any broker event clears it.\n    let started = Instant::now();\n    let mut seen_any = false;\n    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\n    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n    // Any live session replays its buffered ring on attach, so a real attach sets\n    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\n    let mut rendered_any = false;\n    loop {\n        // ── controller window-change → resize ──────────────────────────────\n        if controller {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                if last_size != Some((rows, cols)) {\n                    last_size = Some((rows, cols));\n                    match status.as_deref_mut() {\n                        // Status active: the harness PTY owns rows-1 (we keep row 1\n                        // for the banner), and we re-assert the margin + repaint on\n                        // the new size (REQ-RC-IDENTITY). Never resize the PTY to 0.\n                        Some(status) => {\n                            status.update_size(rows, cols);\n                            let pty_rows = rows.saturating_sub(1).max(1);\n                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\n                            if stdout.write_all(&status.assert_bytes()).is_ok() {\n                                let _ = stdout.flush();\n                            }\n                        }\n                        None => {\n                            let _ = send_attach_resize(brain, stream_id, rows, cols);\n                        }\n                    }\n                }\n            }\n        }\n\n        // ── stdin → session ────────────────────────────────────────────────\n        if let Some(rx) = rx {\n            loop {\n                match rx.try_recv() {\n                    Ok(StdinMsg::Bytes(bytes)) => {\n                        send_attach_input(brain, stream_id, &bytes, ops.next())\n                            .map_err(|e| format!(\"send input: {e}\"))?;\n                    }\n                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\n                    Err(mpsc::TryRecvError::Empty) => break,\n                    Err(mpsc::TryRecvError::Disconnected) => {\n                        return Ok(detach(brain, stream_id))\n                    }\n                }\n            }\n        }\n\n        // ── session → terminal ─────────────────────────────────────────────\n        let ev = match brain.read_event_until(Some(Instant::now() + POLL_SLICE)) {\n            Ok(ev) => ev,\n            Err(e) => match classify_read_err(e.kind()) {\n                // A poll-slice timeout: no event this slice. Before looping, the\n                // first-event backstop — if NOTHING has ever arrived past the\n                // generous grace, the attach is dead/wedged (path b).\n                ReadDisposition::Retry => {\n                    if first_event_stalled(seen_any, started.elapsed()) {\n                        return Ok(PumpEnd::Stalled);\n                    }\n                    continue;\n                }\n                // A severed broker stream (broker bounce / hang-up) — surface it\n                // gracefully, never as a raw RC_FAIL crash (REQ-HAZARD-RC-EOF).\n                ReadDisposition::BrokerGone => return Ok(PumpEnd::BrokerGone),\n                ReadDisposition::Fatal => return Err(format!(\"read event: {e}\")),\n            },\n        };\n        seen_any = true; // any broker event proves the attach stream is live\n        match ev {\n            BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Output { seq, data_b64 } => {\n                            if seq < cursor {\n                                continue; // already rendered — re-serve dedup\n                            }\n                            cursor = seq + 1;\n                            rendered_any = true;\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\n                            // Track the harness's mouse-reporting mode from its output\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\n                            mouse_scanner.feed(mouse_mode, &bytes);\n…\n                        }\n…\n                    }\n                }\n            }\n…\n        }\n    }\n}\n\n[Showing lines 1759-1948 of 3214. Use :1949 to continue]"}],"details":{"truncation":{"content":"    /// so it renders an honest \"no live session\" instead of a false \"detached\".\r\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n    NoLiveSession,\r\n}\r\n\r\n/// Generous first-event backstop for the attach pump (REQ-HAZARD-RC-ATTACH-FAILFAST\r\n/// path b): if NO broker event arrives within this window after attach, the\r\n/// session is treated as dead/wedged. Long on purpose — a fresh, mid-init alive\r\n/// session (empty OutputLog, still painting) MUST pass, so this only catches a\r\n/// true hang, never the working fresh-attach.\r\nconst FIRST_EVENT_GRACE: Duration = Duration::from_secs(30);\r\n\r\n/// The path-b stall decision (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach has\r\n/// stalled iff NO broker event has ever arrived AND the generous grace has\r\n/// elapsed. Pulled out so the decision is unit-testable without a 30s pump run.\r\n// [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\r\nfn first_event_stalled(seen_any: bool, elapsed: Duration) -> bool {\r\n    !seen_any && elapsed >= FIRST_EVENT_GRACE\r\n}\r\n\r\n/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\nenum ReadDisposition {\r\n    /// Transient poll-slice timeout — loop and poll again.\r\n    Retry,\r\n    /// Severed broker stream (broker bounced / hung up) — recoverable, surface\r\n    /// gracefully.\r\n    BrokerGone,\r\n    /// A genuine read failure — propagate.\r\n    Fatal,\r\n}\r\n\r\n/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\r\n/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\r\n/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\r\n/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\r\n/// gracefully rather than crashing the PTY with the raw io text (the same EOF\r\n/// family the v0.9.1 seed fix handles); anything else is fatal.\r\n// [impl->REQ-HAZARD-RC-EOF]\r\nfn classify_read_err(kind: std::io::ErrorKind) -> ReadDisposition {\r\n    use std::io::ErrorKind::*;\r\n    match kind {\r\n        WouldBlock | TimedOut => ReadDisposition::Retry,\r\n        UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {\r\n            ReadDisposition::BrokerGone\r\n        }\r\n        _ => ReadDisposition::Fatal,\r\n    }\r\n}\r\n\r\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\r\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\r\n#[allow(clippy::too_many_arguments)]\r\nfn pump(\r\n    brain: &mut Brain,\r\n    stream_id: u64,\r\n    ops: &OpMinter,\r\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\r\n    decoder: &mut AttachDecoder,\r\n    stdout: &mut impl std::io::Write,\r\n    controller: bool,\r\n    mouse_mode: &MouseMode,\r\n    mut status: Option<&mut StatusRow>,\r\n) -> Result<PumpEnd, String> {\r\n    // Track the harness's mouse-reporting mode from its output DECSET sequences so\r\n    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The\r\n    // scanner is pump-local (single producer) and survives a sequence split across\r\n    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.\r\n    let mut mouse_scanner = MouseModeScanner::default();\r\n    // The reserved-row re-assert scanner (REQ-RC-IDENTITY): watches the harness\r\n    // output for an alt-screen enter / DECSTBM reset that would destroy our\r\n    // scroll-region margin, so we can re-paint the status row. Pump-local, single\r\n    // producer, survives a trigger split across output chunks (like mouse_scanner).\r\n    let mut reassert_scanner = ReassertScanner::default();\r\n    // Reserve the top row + paint the identity banner up front (REQ-RC-IDENTITY).\r\n    // Controller-only (the caller passes `Some` only for a control attach).\r\n    if let Some(status) = status.as_deref_mut() {\r\n        stdout\r\n            .write_all(&status.assert_bytes())\r\n            .map_err(|e| format!(\"status assert: {e}\"))?;\r\n        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\r\n    }\r\n    // Finish our send side so the target's `serve_attach` sees EOF → Detached\r\n    // (the session outlives the viewport). Best-effort: an already-finished or\r\n    // exited stream send is harmless to attempt.\r\n    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\r\n        let _ = brain.net_stream_send(stream_id, &[], None, true);\r\n        PumpEnd::Detached\r\n    }\r\n    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\r\n    // drop any `seq` already rendered (the operator render cursor, matching the\r\n    // local `OutputLog` replay discipline).\r\n    let mut cursor = 0u64;\r\n    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size\r\n    // each slice and forward a Resize when it changes (cross-platform without\r\n    // hooking SIGWINCH / the crossterm event loop, which would conflict with the\r\n    // raw-stdin reader). `None` until the first read.\r\n    let mut last_size: Option<(u16, u16)> = None;\r\n    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown\r\n    // as a one-line indicator when it differs from our own terminal.\r\n    let mut pty_size: Option<(u16, u16)> = None;\r\n    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged\r\n    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE\r\n    // rather than an infinite blank. Any broker event clears it.\r\n    let started = Instant::now();\r\n    let mut seen_any = false;\r\n    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\r\n    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\r\n    // the cross-node D6 stale-row case — not a live viewport the operator detached.\r\n    // Any live session replays its buffered ring on attach, so a real attach sets\r\n    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\r\n    let mut rendered_any = false;\r\n    loop {\r\n        // ── controller window-change → resize ──────────────────────────────\r\n        if controller {\r\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\r\n                if last_size != Some((rows, cols)) {\r\n                    last_size = Some((rows, cols));\r\n                    match status.as_deref_mut() {\r\n                        // Status active: the harness PTY owns rows-1 (we keep row 1\r\n                        // for the banner), and we re-assert the margin + repaint on\r\n                        // the new size (REQ-RC-IDENTITY). Never resize the PTY to 0.\r\n                        Some(status) => {\r\n                            status.update_size(rows, cols);\r\n                            let pty_rows = rows.saturating_sub(1).max(1);\r\n                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\r\n                            if stdout.write_all(&status.assert_bytes()).is_ok() {\r\n                                let _ = stdout.flush();\r\n                            }\r\n                        }\r\n                        None => {\r\n                            let _ = send_attach_resize(brain, stream_id, rows, cols);\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        // ── stdin → session ────────────────────────────────────────────────\r\n        if let Some(rx) = rx {\r\n            loop {\r\n                match rx.try_recv() {\r\n                    Ok(StdinMsg::Bytes(bytes)) => {\r\n                        send_attach_input(brain, stream_id, &bytes, ops.next())\r\n                            .map_err(|e| format!(\"send input: {e}\"))?;\r\n                    }\r\n                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\r\n                    Err(mpsc::TryRecvError::Empty) => break,\r\n                    Err(mpsc::TryRecvError::Disconnected) => {\r\n                        return Ok(detach(brain, stream_id))\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        // ── session → terminal ─────────────────────────────────────────────\r\n        let ev = match brain.read_event_until(Some(Instant::now() + POLL_SLICE)) {\r\n            Ok(ev) => ev,\r\n            Err(e) => match classify_read_err(e.kind()) {\r\n                // A poll-slice timeout: no event this slice. Before looping, the\r\n                // first-event backstop — if NOTHING has ever arrived past the\r\n                // generous grace, the attach is dead/wedged (path b).\r\n                ReadDisposition::Retry => {\r\n                    if first_event_stalled(seen_any, started.elapsed()) {\r\n                        return Ok(PumpEnd::Stalled);\r\n                    }\r\n                    continue;\r\n                }\r\n                // A severed broker stream (broker bounce / hang-up) — surface it\r\n                // gracefully, never as a raw RC_FAIL crash (REQ-HAZARD-RC-EOF).\r\n                ReadDisposition::BrokerGone => return Ok(PumpEnd::BrokerGone),\r\n                ReadDisposition::Fatal => return Err(format!(\"read event: {e}\")),\r\n            },\r\n        };\r\n        seen_any = true; // any broker event proves the attach stream is live\r\n        match ev {\r\n            BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {\r\n                for rec in decoder.push(&bytes) {\r\n                    match rec {\r\n                        AttachRecord::Output { seq, data_b64 } => {\r\n                            if seq < cursor {\r\n                                continue; // already rendered — re-serve dedup\r\n                            }\r\n                            cursor = seq + 1;\r\n                            rendered_any = true;\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\r\n                            // Track the harness's mouse-reporting mode from its output\r\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\r\n                            mouse_scanner.feed(mouse_mode, &bytes);\r","truncated":true,"truncatedBy":"lines","totalLines":1456,"totalBytes":10259,"outputLines":190,"outputBytes":10259,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"enum PumpEnd {\n…\n    /// so it renders an honest \"no live session\" instead of a false \"detached\".\r\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n    NoLiveSession,\r\n}\r\n\r\n/// Generous first-event backstop for the attach pump (REQ-HAZARD-RC-ATTACH-FAILFAST\r\n/// path b): if NO broker event arrives within this window after attach, the\r\n/// session is treated as dead/wedged. Long on purpose — a fresh, mid-init alive\r\n/// session (empty OutputLog, still painting) MUST pass, so this only catches a\r\n/// true hang, never the working fresh-attach.\r\nconst FIRST_EVENT_GRACE: Duration = Duration::from_secs(30);\r\n\r\n/// The path-b stall decision (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach has\r\n/// stalled iff NO broker event has ever arrived AND the generous grace has\r\n/// elapsed. Pulled out so the decision is unit-testable without a 30s pump run.\r\n// [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\r\nfn first_event_stalled(seen_any: bool, elapsed: Duration) -> bool {\r\n    !seen_any && elapsed >= FIRST_EVENT_GRACE\r\n}\r\n\r\n/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\nenum ReadDisposition {\r\n    /// Transient poll-slice timeout — loop and poll again.\r\n    Retry,\r\n    /// Severed broker stream (broker bounced / hung up) — recoverable, surface\r\n    /// gracefully.\r\n    BrokerGone,\r\n    /// A genuine read failure — propagate.\r\n    Fatal,\r\n}\r\n\r\n/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\r\n/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\r\n/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\r\n/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\r\n/// gracefully rather than crashing the PTY with the raw io text (the same EOF\r\n/// family the v0.9.1 seed fix handles); anything else is fatal.\r\n// [impl->REQ-HAZARD-RC-EOF]\r\nfn classify_read_err(kind: std::io::ErrorKind) -> ReadDisposition {\r\n    use std::io::ErrorKind::*;\r\n    match kind {\r\n        WouldBlock | TimedOut => ReadDisposition::Retry,\r\n        UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {\r\n            ReadDisposition::BrokerGone\r\n        }\r\n        _ => ReadDisposition::Fatal,\r\n    }\r\n}\r\n\r\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\r\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\r\n#[allow(clippy::too_many_arguments)]\r\nfn pump(\r\n    brain: &mut Brain,\r\n    stream_id: u64,\r\n    ops: &OpMinter,\r\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\r\n    decoder: &mut AttachDecoder,\r\n    stdout: &mut impl std::io::Write,\r\n    controller: bool,\r\n    mouse_mode: &MouseMode,\r\n    mut status: Option<&mut StatusRow>,\r\n) -> Result<PumpEnd, String> {\r\n    // Track the harness's mouse-reporting mode from its output DECSET sequences so\r\n    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The\r\n    // scanner is pump-local (single producer) and survives a sequence split across\r\n    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.\r\n    let mut mouse_scanner = MouseModeScanner::default();\r\n    // The reserved-row re-assert scanner (REQ-RC-IDENTITY): watches the harness\r\n    // output for an alt-screen enter / DECSTBM reset that would destroy our\r\n    // scroll-region margin, so we can re-paint the status row. Pump-local, single\r\n    // producer, survives a trigger split across output chunks (like mouse_scanner).\r\n    let mut reassert_scanner = ReassertScanner::default();\r\n    // Reserve the top row + paint the identity banner up front (REQ-RC-IDENTITY).\r\n    // Controller-only (the caller passes `Some` only for a control attach).\r\n    if let Some(status) = status.as_deref_mut() {\r\n        stdout\r\n            .write_all(&status.assert_bytes())\r\n            .map_err(|e| format!(\"status assert: {e}\"))?;\r\n        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\r\n    }\r\n    // Finish our send side so the target's `serve_attach` sees EOF → Detached\r\n    // (the session outlives the viewport). Best-effort: an already-finished or\r\n    // exited stream send is harmless to attempt.\r\n    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\r\n        let _ = brain.net_stream_send(stream_id, &[], None, true);\r\n        PumpEnd::Detached\r\n    }\r\n    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\r\n    // drop any `seq` already rendered (the operator render cursor, matching the\r\n    // local `OutputLog` replay discipline).\r\n    let mut cursor = 0u64;\r\n    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size\r\n    // each slice and forward a Resize when it changes (cross-platform without\r\n    // hooking SIGWINCH / the crossterm event loop, which would conflict with the\r\n    // raw-stdin reader). `None` until the first read.\r\n    let mut last_size: Option<(u16, u16)> = None;\r\n    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown\r\n    // as a one-line indicator when it differs from our own terminal.\r\n    let mut pty_size: Option<(u16, u16)> = None;\r\n    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged\r\n    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE\r\n    // rather than an infinite blank. Any broker event clears it.\r\n    let started = Instant::now();\r\n    let mut seen_any = false;\r\n    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\r\n    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\r\n    // the cross-node D6 stale-row case — not a live viewport the operator detached.\r\n    // Any live session replays its buffered ring on attach, so a real attach sets\r\n    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\r\n    let mut rendered_any = false;\r\n    loop {\r\n        // ── controller window-change → resize ──────────────────────────────\r\n        if controller {\r\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\r\n                if last_size != Some((rows, cols)) {\r\n                    last_size = Some((rows, cols));\r\n                    match status.as_deref_mut() {\r\n                        // Status active: the harness PTY owns rows-1 (we keep row 1\r\n                        // for the banner), and we re-assert the margin + repaint on\r\n                        // the new size (REQ-RC-IDENTITY). Never resize the PTY to 0.\r\n                        Some(status) => {\r\n                            status.update_size(rows, cols);\r\n                            let pty_rows = rows.saturating_sub(1).max(1);\r\n                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\r\n                            if stdout.write_all(&status.assert_bytes()).is_ok() {\r\n                                let _ = stdout.flush();\r\n                            }\r\n                        }\r\n                        None => {\r\n                            let _ = send_attach_resize(brain, stream_id, rows, cols);\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        // ── stdin → session ────────────────────────────────────────────────\r\n        if let Some(rx) = rx {\r\n            loop {\r\n                match rx.try_recv() {\r\n                    Ok(StdinMsg::Bytes(bytes)) => {\r\n                        send_attach_input(brain, stream_id, &bytes, ops.next())\r\n                            .map_err(|e| format!(\"send input: {e}\"))?;\r\n                    }\r\n                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\r\n                    Err(mpsc::TryRecvError::Empty) => break,\r\n                    Err(mpsc::TryRecvError::Disconnected) => {\r\n                        return Ok(detach(brain, stream_id))\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        // ── session → terminal ─────────────────────────────────────────────\r\n        let ev = match brain.read_event_until(Some(Instant::now() + POLL_SLICE)) {\r\n            Ok(ev) => ev,\r\n            Err(e) => match classify_read_err(e.kind()) {\r\n                // A poll-slice timeout: no event this slice. Before looping, the\r\n                // first-event backstop — if NOTHING has ever arrived past the\r\n                // generous grace, the attach is dead/wedged (path b).\r\n                ReadDisposition::Retry => {\r\n                    if first_event_stalled(seen_any, started.elapsed()) {\r\n                        return Ok(PumpEnd::Stalled);\r\n                    }\r\n                    continue;\r\n                }\r\n                // A severed broker stream (broker bounce / hang-up) — surface it\r\n                // gracefully, never as a raw RC_FAIL crash (REQ-HAZARD-RC-EOF).\r\n                ReadDisposition::BrokerGone => return Ok(PumpEnd::BrokerGone),\r\n                ReadDisposition::Fatal => return Err(format!(\"read event: {e}\")),\r\n            },\r\n        };\r\n        seen_any = true; // any broker event proves the attach stream is live\r\n        match ev {\r\n            BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {\r\n                for rec in decoder.push(&bytes) {\r\n                    match rec {\r\n                        AttachRecord::Output { seq, data_b64 } => {\r\n                            if seq < cursor {\r\n                                continue; // already rendered — re-serve dedup\r\n                            }\r\n                            cursor = seq + 1;\r\n                            rendered_any = true;\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\r\n                            // Track the harness's mouse-reporting mode from its output\r\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\r\n                            mouse_scanner.feed(mouse_mode, &bytes);\r\n…\n                        }\n…\n                    }\n                }\n            }\n…\n        }\n    }\n}","startLine":1720,"lineNumbers":[1720,null,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,null,1966,null,1989,1990,1991,null,1997,1998,1999]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":3214,"totalBytes":10259,"outputLines":190,"outputBytes":10259,"shownRange":{"start":1759,"end":1948},"nextOffset":1949}}},"isError":false,"timestamp":1784195763439}}
{"type":"message","id":"cfee8456","parentId":"15dfb39d","timestamp":"2026-07-16T09:56:20.384Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying missing broker functions**"},{"type":"toolCall","id":"call_fUbrJTuJuxhP9JIbE63MlLVN|fc_008390f508d1d09b016a58aac456b0819abdb42ec5de36a055","name":"grep","arguments":{"i":"Locating broker ownership bodies","pattern":"fn (append|become_controller|controller_writer|handle_conn|dispatch_subscribe|dispatch_unsubscribe|run_drain|detach_if|all_sinks|converge_perch_stamps)|KIND_UNSUBSCRIBE|detach_if\\(","path":"crates/spt-daemon/src/broker.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_HlEPld6an9MylNTSkBJrZpL9|fc_008390f508d1d09b016a58aac456c0819aaa0e7cdb82d93bfd","name":"read","arguments":{"i":"Reading complete pump outcomes","path":"crates/spt/src/rc.rs","selector":"1938-2025"}},{"type":"toolCall","id":"call_yrixMqyibbHKr7D5wFpbo7fn|fc_008390f508d1d09b016a58aac456c8819aa4f59e1e2b1c24ee","name":"read","arguments":{"i":"Reading attach wire helpers","path":"crates/spt-daemon/src/attach.rs","selector":"552-627"}},{"type":"toolCall","id":"call_QMuBIyHoWOKcHp27xEnSsWSL|fc_008390f508d1d09b016a58aac456cc819a8c8c1e3fc04276f9","name":"read","arguments":{"i":"Reading PTY ownership layer","path":"crates/spt-term/src/pty.rs","selector":null}},{"type":"toolCall","id":"call_1u4yl09vzprlpi2oD81yUhGO|fc_008390f508d1d09b016a58aac456d4819aa6aebefe6a0d9ce4","name":"read","arguments":{"i":"Reading attach network protocol","path":"crates/spt-net/src/net/attach.rs","selector":null}},{"type":"toolCall","id":"call_yUA4VjTZgKVII4QZccGZm5xq|fc_008390f508d1d09b016a58aac456d8819a9386827c67c2158a","name":"read","arguments":{"i":"Reading key broker tests list","path":"crates/spt-daemon/tests","selector":null}},{"type":"toolCall","id":"call_chLDGELsrrqOr6JeD6vIRwAf|fc_008390f508d1d09b016a58aac456dc819ab4b7d45d03972424","name":"grep","arguments":{"i":"Indexing attach reconnect tests","pattern":"#\\[(test|cfg)|fn [a-zA-Z0-9_]+\\(|REQ-(RC|HAZARD|BRAIN)|reconnect|detach|controller|viewer|epoch|half|poison|stall","path":"crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/broker.rs;crates/spt-daemon/tests/control_stamp_lifetime.rs;crates/spt-daemon/tests/inject_control_wedge.rs;crates/spt-daemon/tests/netbroker.rs;crates/spt-daemon/tests/attach_wedge_e2e.rs;crates/spt/tests/attach_wedge_e2e.rs;crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/src/rc.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_PHakVn3VbdijB4dFCPDEREIs|fc_008390f508d1d09b016a58aac456e0819ab269996c92314077","name":"grep","arguments":{"i":"Finding update interaction tests","pattern":"old_gen_drained|stall_evict|CONN_WRITE_POISONED|black[-_ ]hole|suspend|refresh|update|detach|reattach|reconnect","path":"crates/spt-daemon/tests;crates/spt/tests;tests;scripts;docs","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":48012,"output":619,"cacheRead":72192,"cacheWrite":0,"totalTokens":120823,"reasoningTokens":30,"cost":{"input":0.24006000000000002,"output":0.01857,"cacheRead":0.036095999999999996,"cacheWrite":0,"total":0.29472600000000004}},"stopReason":"toolUse","timestamp":1784195763488,"responseId":"resp_008390f508d1d09b016a58aab3dbbc819a9c475dfea7c2eff7","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_008390f508d1d09b016a58aab8d1c8819aa71e0209abb1ae5c","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKrEXjED8G8NL0TgN620XPclwnWkzp3eVTvKNk7sgUhDWcdrGJe-HA7ZJ63IyywiNAu-Hr0Lxv1IML3va8ecEVy8XdiedRyvdwNBrASSEzz4Sb-mRngNupmAwo74gwS1rowB_L-v7m2AeZ83Jit-7TDQqHeLKS-VEhv37uRBT7gp729SeL__n_CdKkQOlHpDAbgJsvp7XKyMc7enoUWEE2eAIV6v95y1SqysHCM7f-h2gHq5xxMjjLyhWD3DJprAH_YIFt2p432hbbBS1wSp0SqMR1UlOja5dUO8dRHzmywQgbfb-vYiql22lvFQWyEbp3W3XR78nUYvG3H4cAHuWZ7wZoeYYtz1vMCJ3_9weK6oVoFbtcN4VtWECdjeFuQR-8-y86UkBnNbo9_w9WYFWiCERCGWIfAsHZgsofEt7Ou7VfnXFNv0RzKw5G2xaIz3iduMDVZf1Rr8PBmNyrzq-W422ICVa9ImoyJBiFs6OmLRD0jgm_x8pj1KkxcGcMx75ZJzYaLfM32M29dJ4O5CkDtcggfLaHD_OWRGhQXX_Yby6ygIdAEQGLIKRRJUUnIy0JTnfTrj3O-jvAPCllx0aPnSnGeej_zkWw2Qvxo3eEpLLamDihn_5JMdqNZcWsRaY6AYP14jAySKsE6oWUQ3hRt5DjEDsy8Py356iOyjO5gDpSdG1GCqXZpU6IsBQ-yNgkbiX2ekb5GQQZUa3F2g0TW_q_ZxGusHoR4ZmPkLHM8YV6cEmDTPmcQPbEQ3qECf947lcE6pT9IcjOw3o2h7pjJgl7DtbMrwtAUAuMs-h6A-2i64LXJzHn4Pfn1thL3p4EB8t9nJZA5zQrBbX7tnwumsXaxQR9OBXdim2Fw4noEsCH_3MzJCG0Q7TqNFRmxDBpXvi1Nvtsw2pgTf-8YRsh_OT6a9dSDA599KhlDSzSCCJYN-45iXWgtXnUm1ppTAIgcmjtg8ReQcyw35PkFAGgku7Sv1PD2WLBipO3X-rfaq9JUcnEGrVkHDKFi4WMpEZ49DS--zyb9fgec8lHUD4c9iSGNH_tX8-lLrW-3WXu47NBb9bsR6Hl5v4i97DzkrPz91ZvxYzH0Ijp5k9an8s2UP1XmhOMa0JbR5Qq9Q6yTzQyA=","summary":[{"type":"summary_text","text":"**Identifying missing broker functions**"}]},{"id":"fc_008390f508d1d09b016a58aac456b0819abdb42ec5de36a055","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating broker ownership bodies\",\"pattern\":\"fn (append|become_controller|controller_writer|handle_conn|dispatch_subscribe|dispatch_unsubscribe|run_drain|detach_if|all_sinks|converge_perch_stamps)|KIND_UNSUBSCRIBE|detach_if\\\\(\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_fUbrJTuJuxhP9JIbE63MlLVN","name":"grep"},{"id":"fc_008390f508d1d09b016a58aac456c0819aaa0e7cdb82d93bfd","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading complete pump outcomes\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1938-2025\"}","call_id":"call_HlEPld6an9MylNTSkBJrZpL9","name":"read"},{"id":"fc_008390f508d1d09b016a58aac456c8819aa4f59e1e2b1c24ee","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach wire helpers\",\"path\":\"crates/spt-daemon/src/attach.rs\",\"selector\":\"552-627\"}","call_id":"call_yrixMqyibbHKr7D5wFpbo7fn","name":"read"},{"id":"fc_008390f508d1d09b016a58aac456cc819a8c8c1e3fc04276f9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading PTY ownership layer\",\"path\":\"crates/spt-term/src/pty.rs\",\"selector\":null}","call_id":"call_QMuBIyHoWOKcHp27xEnSsWSL","name":"read"},{"id":"fc_008390f508d1d09b016a58aac456d4819aa6aebefe6a0d9ce4","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach network protocol\",\"path\":\"crates/spt-net/src/net/attach.rs\",\"selector\":null}","call_id":"call_1u4yl09vzprlpi2oD81yUhGO","name":"read"},{"id":"fc_008390f508d1d09b016a58aac456d8819a9386827c67c2158a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading key broker tests list\",\"path\":\"crates/spt-daemon/tests\",\"selector\":null}","call_id":"call_yUA4VjTZgKVII4QZccGZm5xq","name":"read"},{"id":"fc_008390f508d1d09b016a58aac456dc819ab4b7d45d03972424","type":"function_call","status":"completed","arguments":"{\"i\":\"Indexing attach reconnect tests\",\"pattern\":\"#\\\\[(test|cfg)|fn [a-zA-Z0-9_]+\\\\(|REQ-(RC|HAZARD|BRAIN)|reconnect|detach|controller|viewer|epoch|half|poison|stall\",\"path\":\"crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/broker.rs;crates/spt-daemon/tests/control_stamp_lifetime.rs;crates/spt-daemon/tests/inject_control_wedge.rs;crates/spt-daemon/tests/netbroker.rs;crates/spt-daemon/tests/attach_wedge_e2e.rs;crates/spt/tests/attach_wedge_e2e.rs;crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/src/rc.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_chLDGELsrrqOr6JeD6vIRwAf","name":"grep"},{"id":"fc_008390f508d1d09b016a58aac456e0819ab269996c92314077","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding update interaction tests\",\"pattern\":\"old_gen_drained|stall_evict|CONN_WRITE_POISONED|black[-_ ]hole|suspend|refresh|update|detach|reattach|reconnect\",\"path\":\"crates/spt-daemon/tests;crates/spt/tests;tests;scripts;docs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_PHakVn3VbdijB4dFCPDEREIs","name":"grep"}]},"duration":16895.72230000049,"ttft":5314.571299999952,"contextSnapshot":{"promptTokens":120204,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_fUbrJTuJuxhP9JIbE63MlLVN|fc_008390f508d1d09b016a58aac456b0819abdb42ec5de36a055","toolName":"grep","startedAt":"2026-07-16T09:56:20.385Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Locating broker ownership bodies"},"id":"81759299","parentId":"cfee8456","timestamp":"2026-07-16T09:56:20.385Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_HlEPld6an9MylNTSkBJrZpL9|fc_008390f508d1d09b016a58aac456c0819aaa0e7cdb82d93bfd","toolName":"read","startedAt":"2026-07-16T09:56:20.385Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading complete pump outcomes"},"id":"34c90554","parentId":"81759299","timestamp":"2026-07-16T09:56:20.385Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_yrixMqyibbHKr7D5wFpbo7fn|fc_008390f508d1d09b016a58aac456c8819aa4f59e1e2b1c24ee","toolName":"read","startedAt":"2026-07-16T09:56:20.385Z","args":{"path":"crates/spt-daemon/src/attach.rs"},"intent":"Reading attach wire helpers"},"id":"69c425f3","parentId":"34c90554","timestamp":"2026-07-16T09:56:20.385Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_QMuBIyHoWOKcHp27xEnSsWSL|fc_008390f508d1d09b016a58aac456cc819a8c8c1e3fc04276f9","toolName":"read","startedAt":"2026-07-16T09:56:20.385Z","args":{"path":"crates/spt-term/src/pty.rs"},"intent":"Reading PTY ownership layer"},"id":"4b10fa56","parentId":"69c425f3","timestamp":"2026-07-16T09:56:20.385Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_1u4yl09vzprlpi2oD81yUhGO|fc_008390f508d1d09b016a58aac456d4819aa6aebefe6a0d9ce4","toolName":"read","startedAt":"2026-07-16T09:56:20.385Z","args":{"path":"crates/spt-net/src/net/attach.rs"},"intent":"Reading attach network protocol"},"id":"5ea076a7","parentId":"4b10fa56","timestamp":"2026-07-16T09:56:20.385Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_yUA4VjTZgKVII4QZccGZm5xq|fc_008390f508d1d09b016a58aac456d8819a9386827c67c2158a","toolName":"read","startedAt":"2026-07-16T09:56:20.385Z","args":{"path":"crates/spt-daemon/tests"},"intent":"Reading key broker tests list"},"id":"455a2b5e","parentId":"5ea076a7","timestamp":"2026-07-16T09:56:20.385Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_chLDGELsrrqOr6JeD6vIRwAf|fc_008390f508d1d09b016a58aac456dc819ab4b7d45d03972424","toolName":"grep","startedAt":"2026-07-16T09:56:20.385Z","args":{"path":"crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/broker.rs;crates/spt-daemon/tests/control_stamp_lifetime.rs;crates/spt-daemon/tests/inject_control_wedge.rs;crates/spt-daemon/tests/netbroker.…"},"intent":"Indexing attach reconnect tests"},"id":"2f34b60f","parentId":"455a2b5e","timestamp":"2026-07-16T09:56:20.385Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_PHakVn3VbdijB4dFCPDEREIs|fc_008390f508d1d09b016a58aac456e0819ab269996c92314077","toolName":"grep","startedAt":"2026-07-16T09:56:20.385Z","args":{"path":"crates/spt-daemon/tests;crates/spt/tests;tests;scripts;docs"},"intent":"Finding update interaction tests"},"id":"339457a9","parentId":"2f34b60f","timestamp":"2026-07-16T09:56:20.385Z"}
{"type":"message","id":"dbc9eedd","parentId":"339457a9","timestamp":"2026-07-16T09:56:20.391Z","message":{"role":"toolResult","toolCallId":"call_yUA4VjTZgKVII4QZccGZm5xq|fc_008390f508d1d09b016a58aac456d8819a9386827c67c2158a","toolName":"read","content":[{"type":"text","text":".\n  - docs_server_e2e.rs                   5.4KB     22h ago\n  - propagate.rs                         30.1KB    23h ago\n  - pumpdeadline.rs                      6.5KB     5d ago\n  - pump.rs                              36.4KB    5d ago\n  - false_promote.rs                     15.3KB    5d ago\n  - net_worker_starve.rs                 10.7KB    5d ago\n  - brain_resume_conn_deadlock.rs        16.4KB    5d ago\n  - conn_blackhole_lifecycle.rs          33.3KB    5d ago\n  - brain_decouple.rs                    37.0KB    5d ago\n  - inject_control_wedge.rs              160.7KB   5d ago\n  - two_origin_spanning.rs               12.0KB    1w ago\n  - wake_single_flight.rs                6.8KB     1w ago\n  - endpoint_survival.rs                 7.8KB     1w ago\n  - xfer.rs                              13.6KB    1w ago\n  - wanmsg.rs                            15.0KB    1w ago\n  - twohost.rs                           69.4KB    1w ago\n  - replicate.rs                         9.7KB     1w ago\n  - sync.rs                              23.8KB    1w ago\n  - notifsync.rs                         10.9KB    1w ago\n  - netstream.rs                         12.9KB    1w ago\n  - netbroker.rs                         14.8KB    1w ago\n  - input_ack_deadlock.rs                28.2KB    1w ago\n  - idempotent.rs                        9.8KB     1w ago\n  - dispatch.rs                          45.0KB    1w ago\n  - daemon_e2e.rs                        13.8KB    1w ago\n  - broker.rs                            48.7KB    1w ago\n  - attach.rs                            70.1KB    1w ago\n  - access.rs                            9.3KB     1w ago\n  - shellchan.rs                         9.9KB     1w ago\n  - seedproofx.rs                        5.2KB     1w ago\n  - rosterprop.rs                        8.8KB     1w ago\n  - resume.rs                            14.9KB    1w ago\n  - reseed.rs                            6.9KB     1w ago\n  - psyche_residency_expectation_e2e.rs  13.3KB    1w ago\n  - psyche_event_turn_e2e.rs             9.7KB     1w ago\n  - psyche_context_file_e2e.rs           9.9KB     1w ago\n  - presence.rs                          7.1KB     1w ago\n  - pairjoin.rs                          6.8KB     1w ago\n  - mesh.rs                              21.0KB    1w ago\n  - legacy_resident_sweep_e2e.rs         9.3KB     1w ago\n  - handoff.rs                           7.2KB     1w ago\n  - driven_by_selfheal.rs                20.8KB    1w ago\n  - digest.rs                            8.1KB     1w ago\n  - daemon_lifecycle_real_brain.rs       5.4KB     1w ago\n  - control_stamp_lifetime.rs            19.5KB    1w ago\n  - budget.rs                            9.7KB     1w ago\n  - brain_swap.rs                        8.0KB     1w ago"}],"details":{"isDirectory":true,"resolvedPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests","meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests"}}},"isError":false,"timestamp":1784195780391}}
{"type":"message","id":"a018164a","parentId":"dbc9eedd","timestamp":"2026-07-16T09:56:20.393Z","message":{"role":"toolResult","toolCallId":"call_fUbrJTuJuxhP9JIbE63MlLVN|fc_008390f508d1d09b016a58aac456b0819abdb42ec5de36a055","toolName":"grep","content":[{"type":"text","text":" 68|    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n*69|    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n 70|};\n 71|use crate::nethost::{NetHost, NET_EFFECT_SESSION};\n 72|use crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};\n...\n 647|// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n*648|fn converge_perch_stamps(\n 649|    endpoint: &str,\n 650|    want_driven_by: Option<&str>,\n 651|    want_controlled: bool,\n...\n 794|\n*795|    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\n 796|        let seq = self.next_seq;\n 797|        self.next_seq += 1;\n 798|        self.ring.push_back((seq, bytes.to_vec()));\n...\n 890|    /// controller's identity.\n*891|    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {\n 892|        // Drop the prior controller sink first (its writer's live loop ends when\n 893|        // tx drops), and bump the generation so (a) an in-flight deadline-evict\n 894|        // for the old controller can't unseat this one and (b) a prior writer\n...\n 1258|    /// `driven_by` / `viewer_count` when a role actually changed.\n*1259|    fn detach_if(&mut self, sub: &SharedSend) {\n 1260|        let is_ctrl = self\n 1261|            .controller\n 1262|            .as_ref()\n...\n 1287|    /// an `exit` frame.\n*1288|    fn all_sinks(&self) -> Vec<SharedSend> {\n 1289|        let mut v: Vec<SharedSend> = Vec::new();\n 1290|        if let Some(c) = &self.controller {\n 1291|            v.push(Arc::clone(&c.send));\n...\n 1508|#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\n*1509|fn controller_writer(\n 1510|    sid: u64,\n 1511|    send: SharedSend,\n 1512|    initial: Vec<(u64, Vec<u8>)>,\n...\n 2979|    /// restart must not disturb the hosted children).\n*2980|    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n 2981|        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n 2982|        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n 2983|        struct HandlerTally<'a>(&'a AtomicUsize);\n...\n 3024|                },\n*3025|                KIND_UNSUBSCRIBE => {\n 3026|                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\n 3027|                        send_error(&send, &msg);\n 3028|                    }\n...\n 3200|            if let Some(h) = sessions.get(id) {\n*3201|                recover_log(&h.log).detach_if(&send);\n 3202|            }\n 3203|        }\n 3204|        drop(sessions);\n...\n 3448|    /// self-kick guard.\n*3449|    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n 3450|        let req: SubscribeReq = serde_json::from_value(env.payload)\n 3451|            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\n 3452|        // Clone the log Arc out from under the sessions lock, then resolve under\n...\n 3486|    /// a no-op if a newer controller already took over, or the session is gone.\n*3487|    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 3488|        let req: UnsubscribeReq = serde_json::from_value(env.payload)\n 3489|            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\n 3490|        let log = {\n...\n 3499|        };\n*3500|        recover_log(&log).detach_if(send);\n 3501|        Ok(())\n 3502|    }\n 3503|\n...\n 4726|    #[test]\n*4727|    fn append_overflow_flags_the_evicted_viewer_for_skip_to_live() {\n 4728|        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 4729|        // A viewer whose live queue is depth-1 and which we NEVER drain (rx held,\n 4730|        // unread) — it cannot keep up with the drain. We do NOT use `add_viewer`"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":14,"fileCount":1,"files":["crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":14}],"truncated":false,"displayContent":"   68│    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n  *69│    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n   70│};\n   71│use crate::nethost::{NetHost, NET_EFFECT_SESSION};\n   72│use crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};\n     │...\n  647│// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n *648│fn converge_perch_stamps(\n  649│    endpoint: &str,\n  650│    want_driven_by: Option<&str>,\n  651│    want_controlled: bool,\n     │...\n  794│\n *795│    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\n  796│        let seq = self.next_seq;\n  797│        self.next_seq += 1;\n  798│        self.ring.push_back((seq, bytes.to_vec()));\n     │...\n  890│    /// controller's identity.\n *891│    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {\n  892│        // Drop the prior controller sink first (its writer's live loop ends when\n  893│        // tx drops), and bump the generation so (a) an in-flight deadline-evict\n  894│        // for the old controller can't unseat this one and (b) a prior writer\n     │...\n 1258│    /// `driven_by` / `viewer_count` when a role actually changed.\n*1259│    fn detach_if(&mut self, sub: &SharedSend) {\n 1260│        let is_ctrl = self\n 1261│            .controller\n 1262│            .as_ref()\n     │...\n 1287│    /// an `exit` frame.\n*1288│    fn all_sinks(&self) -> Vec<SharedSend> {\n 1289│        let mut v: Vec<SharedSend> = Vec::new();\n 1290│        if let Some(c) = &self.controller {\n 1291│            v.push(Arc::clone(&c.send));\n     │...\n 1508│#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\n*1509│fn controller_writer(\n 1510│    sid: u64,\n 1511│    send: SharedSend,\n 1512│    initial: Vec<(u64, Vec<u8>)>,\n     │...\n 2979│    /// restart must not disturb the hosted children).\n*2980│    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n 2981│        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n 2982│        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n 2983│        struct HandlerTally<'a>(&'a AtomicUsize);\n     │...\n 3024│                },\n*3025│                KIND_UNSUBSCRIBE => {\n 3026│                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\n 3027│                        send_error(&send, &msg);\n 3028│                    }\n     │...\n 3200│            if let Some(h) = sessions.get(id) {\n*3201│                recover_log(&h.log).detach_if(&send);\n 3202│            }\n 3203│        }\n 3204│        drop(sessions);\n     │...\n 3448│    /// self-kick guard.\n*3449│    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n 3450│        let req: SubscribeReq = serde_json::from_value(env.payload)\n 3451│            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\n 3452│        // Clone the log Arc out from under the sessions lock, then resolve under\n     │...\n 3486│    /// a no-op if a newer controller already took over, or the session is gone.\n*3487│    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 3488│        let req: UnsubscribeReq = serde_json::from_value(env.payload)\n 3489│            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\n 3490│        let log = {\n     │...\n 3499│        };\n*3500│        recover_log(&log).detach_if(send);\n 3501│        Ok(())\n 3502│    }\n 3503│\n     │...\n 4726│    #[test]\n*4727│    fn append_overflow_flags_the_evicted_viewer_for_skip_to_live() {\n 4728│        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 4729│        // A viewer whose live queue is depth-1 and which we NEVER drain (rx held,\n 4730│        // unread) — it cannot keep up with the drain. We do NOT use `add_viewer`"},"isError":false,"timestamp":1784195780393}}
{"type":"message","id":"6a4e7938","parentId":"a018164a","timestamp":"2026-07-16T09:56:20.396Z","message":{"role":"toolResult","toolCallId":"call_QMuBIyHoWOKcHp27xEnSsWSL|fc_008390f508d1d09b016a58aac456cc819a8c8c1e3fc04276f9","toolName":"read","content":[{"type":"text","text":"//! Native-PTY session backend — ConPTY on Windows 10+, `forkpty(3)` on Unix,\n//! both via `portable-pty` (the exact crate + handoff shape Spikes #1 / #4\n//! proved).\n//!\n//! [`PtySession`] spawns a child under a real PTY, owns the master reader and\n//! writer, and serves the [`SessionSurface`] contract. It is **single-surface\n//! per handle** — one child's PTY, not the multiplexed fleet (that supervisor is\n//! the M3b broker).\n//!\n//! ## Concurrency discipline (Spike #5, binding)\n//!\n//! `resize()` races the reader thread on Windows; the master is therefore held\n//! behind a `Mutex` and every `resize` takes that lock. The writer is a shared\n//! `Arc<Mutex<…>>` so the A2 DSR drain thread can answer ConPTY's startup\n//! cursor query through the same serialized writer the caller injects input\n//! through — no torn writes.\n\nuse std::io::{Read, Write};\nuse std::sync::{Arc, Mutex};\n\nuse portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, ExitStatus, MasterPty};\n\nuse crate::surface::{SessionSurface, SurfaceError, SurfaceSize};\n\n/// Shared, serialized handle to the PTY master writer. Cloned to the A2 drain\n/// thread so the DSR auto-answer and caller input never interleave mid-write.\npub(crate) type SharedWriter = Arc<Mutex<Box<dyn Write + Send>>>;\n\n/// A child process running under a native PTY, exposed as a [`SessionSurface`].\n///\n/// Build one with [`PtySession::spawn`]; drive input via [`SessionSurface::write_input`]\n/// (or the A3 `send_keys`/`send_line` helpers), drain output via\n/// [`PtySession::take_reader`], and observe lifecycle via [`PtySession::try_wait`] /\n/// [`PtySession::wait`] / [`PtySession::kill`].\npub struct PtySession {\n    /// The master end, behind a lock so `resize` never races the reader (Spike #5).\n    master: Mutex<Box<dyn MasterPty + Send>>,\n    /// The single PTY writer, shared so the A2 drain thread can answer DSR.\n    writer: SharedWriter,\n    /// The spawned child; behind a lock for `wait`/`try_wait`.\n    child: Mutex<Box<dyn Child + Send + Sync>>,\n    /// A kill handle split from the child so we can terminate from another\n    /// thread even while `wait` blocks (`portable-pty` `ChildKiller`).\n    killer: Mutex<Box<dyn ChildKiller + Send + Sync>>,\n    /// The master reader, taken at most once (`take_reader`).\n    reader: Mutex<Option<Box<dyn Read + Send>>>,\n    /// The child OS pid, captured at spawn.\n    pid: Option<u32>,\n}\n\nimpl PtySession {\n    /// Spawn `cmd` under a fresh native PTY sized to `size`.\n    ///\n    /// The slave end is dropped immediately after the child is spawned: the\n    /// child holds the only remaining slave handle, which lets EOF propagate on\n    /// the Unix raw-pipe backend once the child exits. (On Windows the no-EOF\n    /// behaviour is governed by the writer, handled by the A2 drain discipline.)\n    //\n    // [impl->REQ-TERM-1]\n    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {\n        let pty_system = native_pty_system();\n        let pair = pty_system\n            .openpty(size.into())\n            .map_err(|e| SurfaceError::OpenPty(e.to_string()))?;\n\n        let child = pair\n            .slave\n            .spawn_command(cmd)\n            .map_err(|e| SurfaceError::Spawn(e.to_string()))?;\n        // Drop the slave so the child owns the only slave-side handle.\n        drop(pair.slave);\n\n        let reader = pair\n            .master\n            .try_clone_reader()\n            .map_err(|e| SurfaceError::Io(io_other(e)))?;\n        let writer = pair\n            .master\n            .take_writer()\n            .map_err(|e| SurfaceError::Io(io_other(e)))?;\n        let killer = child.clone_killer();\n        let pid = child.process_id();\n\n        Ok(Self {\n            master: Mutex::new(pair.master),\n            writer: Arc::new(Mutex::new(writer)),\n            child: Mutex::new(child),\n            killer: Mutex::new(killer),\n            reader: Mutex::new(Some(reader)),\n            pid,\n        })\n    }\n\n    /// Convenience: spawn `program` with `args` under a PTY sized to `size`,\n    /// inheriting the caller's working directory.\n    pub fn spawn_program<P, A, S>(\n        program: P,\n        args: A,\n        size: SurfaceSize,\n    ) -> Result<Self, SurfaceError>\n    where\n        P: AsRef<std::ffi::OsStr>,\n        A: IntoIterator<Item = S>,\n        S: AsRef<std::ffi::OsStr>,\n    {\n        Self::spawn_program_in(program, args, size, None::<&std::ffi::OsStr>)\n    }\n\n    /// Spawn `program` with `args` under a PTY sized to `size`, in `cwd` when\n    /// `Some` (REQ-HOST-RUN-2: a project-scoped working directory for the\n    /// spawned child) — `None` inherits the caller's cwd, identical to\n    /// [`Self::spawn_program`].\n    // [impl->REQ-HOST-RUN-2]\n    pub fn spawn_program_in<P, A, S, D>(\n        program: P,\n        args: A,\n        size: SurfaceSize,\n        cwd: Option<D>,\n    ) -> Result<Self, SurfaceError>\n    where\n        P: AsRef<std::ffi::OsStr>,\n        A: IntoIterator<Item = S>,\n        S: AsRef<std::ffi::OsStr>,\n        D: AsRef<std::ffi::OsStr>,\n    {\n        Self::spawn_program_in_env(program, args, size, cwd, std::iter::empty::<(&str, &str)>())\n    }\n\n    /// Like [`Self::spawn_program_in`] but also sets `env` vars on the child\n    /// (manifest `[env.<VAR>]` direction = inject — REQ-HAZARD-ENV-SUBST). Each\n    /// `(key, value)` is applied to the spawned process's environment ON TOP of\n    /// the inherited env, so an adapter's per-endpoint injects (e.g.\n    /// `SPT_ENDPOINT_ID = {id}`) actually reach the harness child.\n    // [impl->REQ-HAZARD-ENV-SUBST]\n    // [impl->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]\n    pub fn spawn_program_in_env<P, A, S, D, E, K, V>(\n        program: P,\n        args: A,\n        size: SurfaceSize,\n        cwd: Option<D>,\n        env: E,\n    ) -> Result<Self, SurfaceError>\n    where\n        P: AsRef<std::ffi::OsStr>,\n        A: IntoIterator<Item = S>,\n        S: AsRef<std::ffi::OsStr>,\n        D: AsRef<std::ffi::OsStr>,\n        E: IntoIterator<Item = (K, V)>,\n        K: AsRef<std::ffi::OsStr>,\n        V: AsRef<std::ffi::OsStr>,\n    {\n        // Resolve the program for the OS spawn before portable-pty's own `which`\n        // can pick an extensionless shebang shim that CreateProcessW rejects\n        // (os error 193). On Windows this PATHEXT-resolves a bare name and wraps a\n        // non-PE target (.cmd/.bat → cmd.exe, .ps1 → powershell); off Windows it\n        // is a passthrough. The wrap prefix args precede the caller's args.\n        // [impl->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]\n        let (program, prefix) = crate::winprog::resolve_for_pty(program.as_ref());\n        let mut cmd = CommandBuilder::new(&program);\n        for a in prefix {\n            cmd.arg(a);\n        }\n        for a in args {\n            cmd.arg(a);\n        }\n        if let Some(dir) = cwd {\n            cmd.cwd(dir);\n        }\n        for (k, v) in env {\n            cmd.env(k, v);\n        }\n        Self::spawn(cmd, size)\n    }\n\n    /// Take the master reader. Returns [`SurfaceError::ReaderTaken`] on the\n    /// second call — output is drained by exactly one consumer (the A2 drain\n    /// thread in the hosted path; a test consumer otherwise).\n    pub fn take_reader(&self) -> Result<Box<dyn Read + Send>, SurfaceError> {\n        self.reader\n            .lock()\n            .unwrap()\n            .take()\n            .ok_or(SurfaceError::ReaderTaken)\n    }\n\n    /// The shared writer handle — cloned to the A2 DSR drain thread so its\n    /// auto-answer goes through the same serialized writer the caller injects\n    /// input through (no torn writes).\n    pub(crate) fn shared_writer(&self) -> SharedWriter {\n        Arc::clone(&self.writer)\n    }\n\n    /// Poll for child exit without blocking.\n    pub fn try_wait(&self) -> Result<Option<ExitStatus>, SurfaceError> {\n        self.child\n            .lock()\n            .unwrap()\n            .try_wait()\n            .map_err(SurfaceError::Io)\n    }\n\n    /// Block until the child exits, yielding its status.\n    pub fn wait(&self) -> Result<ExitStatus, SurfaceError> {\n        self.child.lock().unwrap().wait().map_err(SurfaceError::Io)\n    }\n\n    /// Terminate the child. Safe to call from another thread while `wait`\n    /// blocks (uses the split `ChildKiller`). Confirm the reap via\n    /// [`PtySession::wait`] / [`PtySession::try_wait`].\n    pub fn kill(&self) -> Result<(), SurfaceError> {\n        let res = self.killer.lock().unwrap().kill();\n        #[cfg(windows)]\n        {\n            // portable-pty 0.8's Windows `ChildKiller` returns the spurious\n            // `last_os_error` even when `TerminateProcess` *succeeds* (the\n            // upstream success check is inverted; `WinChild::kill` swallows it,\n            // the cloned killer does not). Its Result is therefore untrustworthy\n            // — kill is best-effort; confirm death by reaping.\n            let _ = res;\n            Ok(())\n        }\n        #[cfg(unix)]\n        {\n            res.map_err(SurfaceError::Io)\n        }\n    }\n\n    /// The child's OS process id, if the backend exposes one.\n    pub fn process_id(&self) -> Option<u32> {\n        self.pid\n    }\n}\n\n// [impl->REQ-TERM-1]\nimpl SessionSurface for PtySession {\n    fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError> {\n        let mut w = self.writer.lock().unwrap();\n        w.write_all(bytes).map_err(SurfaceError::Io)?;\n        w.flush().map_err(SurfaceError::Io)\n    }\n\n    fn resize(&self, size: SurfaceSize) -> Result<(), SurfaceError> {\n        // Hold the master lock so resize never races a concurrent reader/resize\n        // (Spike #5: resize races the reader on Windows).\n        self.master\n            .lock()\n            .unwrap()\n            .resize(size.into())\n            .map_err(|e| SurfaceError::Io(io_other(e)))\n    }\n}\n\n/// Fold a `portable-pty` error (a private `anyhow::Error` alias) into a\n/// `std::io::Error` so the crate's public surface never leaks `anyhow`. Generic\n/// over `Display` to avoid naming the un-exported error type.\nfn io_other<E: std::fmt::Display>(e: E) -> std::io::Error {\n    std::io::Error::other(e.to_string())\n}\n"}],"details":{"displayContent":{"text":"//! Native-PTY session backend — ConPTY on Windows 10+, `forkpty(3)` on Unix,\r\n//! both via `portable-pty` (the exact crate + handoff shape Spikes #1 / #4\r\n//! proved).\r\n//!\r\n//! [`PtySession`] spawns a child under a real PTY, owns the master reader and\r\n//! writer, and serves the [`SessionSurface`] contract. It is **single-surface\r\n//! per handle** — one child's PTY, not the multiplexed fleet (that supervisor is\r\n//! the M3b broker).\r\n//!\r\n//! ## Concurrency discipline (Spike #5, binding)\r\n//!\r\n//! `resize()` races the reader thread on Windows; the master is therefore held\r\n//! behind a `Mutex` and every `resize` takes that lock. The writer is a shared\r\n//! `Arc<Mutex<…>>` so the A2 DSR drain thread can answer ConPTY's startup\r\n//! cursor query through the same serialized writer the caller injects input\r\n//! through — no torn writes.\r\n\r\nuse std::io::{Read, Write};\r\nuse std::sync::{Arc, Mutex};\r\n\r\nuse portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, ExitStatus, MasterPty};\r\n\r\nuse crate::surface::{SessionSurface, SurfaceError, SurfaceSize};\r\n\r\n/// Shared, serialized handle to the PTY master writer. Cloned to the A2 drain\r\n/// thread so the DSR auto-answer and caller input never interleave mid-write.\r\npub(crate) type SharedWriter = Arc<Mutex<Box<dyn Write + Send>>>;\r\n\r\n/// A child process running under a native PTY, exposed as a [`SessionSurface`].\r\n///\r\n/// Build one with [`PtySession::spawn`]; drive input via [`SessionSurface::write_input`]\r\n/// (or the A3 `send_keys`/`send_line` helpers), drain output via\r\n/// [`PtySession::take_reader`], and observe lifecycle via [`PtySession::try_wait`] /\r\n/// [`PtySession::wait`] / [`PtySession::kill`].\r\npub struct PtySession {\r\n    /// The master end, behind a lock so `resize` never races the reader (Spike #5).\r\n    master: Mutex<Box<dyn MasterPty + Send>>,\r\n    /// The single PTY writer, shared so the A2 drain thread can answer DSR.\r\n    writer: SharedWriter,\r\n    /// The spawned child; behind a lock for `wait`/`try_wait`.\r\n    child: Mutex<Box<dyn Child + Send + Sync>>,\r\n    /// A kill handle split from the child so we can terminate from another\r\n    /// thread even while `wait` blocks (`portable-pty` `ChildKiller`).\r\n    killer: Mutex<Box<dyn ChildKiller + Send + Sync>>,\r\n    /// The master reader, taken at most once (`take_reader`).\r\n    reader: Mutex<Option<Box<dyn Read + Send>>>,\r\n    /// The child OS pid, captured at spawn.\r\n    pid: Option<u32>,\r\n}\r\n\r\nimpl PtySession {\r\n    /// Spawn `cmd` under a fresh native PTY sized to `size`.\r\n    ///\r\n    /// The slave end is dropped immediately after the child is spawned: the\r\n    /// child holds the only remaining slave handle, which lets EOF propagate on\r\n    /// the Unix raw-pipe backend once the child exits. (On Windows the no-EOF\r\n    /// behaviour is governed by the writer, handled by the A2 drain discipline.)\r\n    //\r\n    // [impl->REQ-TERM-1]\r\n    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {\r\n        let pty_system = native_pty_system();\r\n        let pair = pty_system\r\n            .openpty(size.into())\r\n            .map_err(|e| SurfaceError::OpenPty(e.to_string()))?;\r\n\r\n        let child = pair\r\n            .slave\r\n            .spawn_command(cmd)\r\n            .map_err(|e| SurfaceError::Spawn(e.to_string()))?;\r\n        // Drop the slave so the child owns the only slave-side handle.\r\n        drop(pair.slave);\r\n\r\n        let reader = pair\r\n            .master\r\n            .try_clone_reader()\r\n            .map_err(|e| SurfaceError::Io(io_other(e)))?;\r\n        let writer = pair\r\n            .master\r\n            .take_writer()\r\n            .map_err(|e| SurfaceError::Io(io_other(e)))?;\r\n        let killer = child.clone_killer();\r\n        let pid = child.process_id();\r\n\r\n        Ok(Self {\r\n            master: Mutex::new(pair.master),\r\n            writer: Arc::new(Mutex::new(writer)),\r\n            child: Mutex::new(child),\r\n            killer: Mutex::new(killer),\r\n            reader: Mutex::new(Some(reader)),\r\n            pid,\r\n        })\r\n    }\r\n\r\n    /// Convenience: spawn `program` with `args` under a PTY sized to `size`,\r\n    /// inheriting the caller's working directory.\r\n    pub fn spawn_program<P, A, S>(\r\n        program: P,\r\n        args: A,\r\n        size: SurfaceSize,\r\n    ) -> Result<Self, SurfaceError>\r\n    where\r\n        P: AsRef<std::ffi::OsStr>,\r\n        A: IntoIterator<Item = S>,\r\n        S: AsRef<std::ffi::OsStr>,\r\n    {\r\n        Self::spawn_program_in(program, args, size, None::<&std::ffi::OsStr>)\r\n    }\r\n\r\n    /// Spawn `program` with `args` under a PTY sized to `size`, in `cwd` when\r\n    /// `Some` (REQ-HOST-RUN-2: a project-scoped working directory for the\r\n    /// spawned child) — `None` inherits the caller's cwd, identical to\r\n    /// [`Self::spawn_program`].\r\n    // [impl->REQ-HOST-RUN-2]\r\n    pub fn spawn_program_in<P, A, S, D>(\r\n        program: P,\r\n        args: A,\r\n        size: SurfaceSize,\r\n        cwd: Option<D>,\r\n    ) -> Result<Self, SurfaceError>\r\n    where\r\n        P: AsRef<std::ffi::OsStr>,\r\n        A: IntoIterator<Item = S>,\r\n        S: AsRef<std::ffi::OsStr>,\r\n        D: AsRef<std::ffi::OsStr>,\r\n    {\r\n        Self::spawn_program_in_env(program, args, size, cwd, std::iter::empty::<(&str, &str)>())\r\n    }\r\n\r\n    /// Like [`Self::spawn_program_in`] but also sets `env` vars on the child\r\n    /// (manifest `[env.<VAR>]` direction = inject — REQ-HAZARD-ENV-SUBST). Each\r\n    /// `(key, value)` is applied to the spawned process's environment ON TOP of\r\n    /// the inherited env, so an adapter's per-endpoint injects (e.g.\r\n    /// `SPT_ENDPOINT_ID = {id}`) actually reach the harness child.\r\n    // [impl->REQ-HAZARD-ENV-SUBST]\r\n    // [impl->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]\r\n    pub fn spawn_program_in_env<P, A, S, D, E, K, V>(\r\n        program: P,\r\n        args: A,\r\n        size: SurfaceSize,\r\n        cwd: Option<D>,\r\n        env: E,\r\n    ) -> Result<Self, SurfaceError>\r\n    where\r\n        P: AsRef<std::ffi::OsStr>,\r\n        A: IntoIterator<Item = S>,\r\n        S: AsRef<std::ffi::OsStr>,\r\n        D: AsRef<std::ffi::OsStr>,\r\n        E: IntoIterator<Item = (K, V)>,\r\n        K: AsRef<std::ffi::OsStr>,\r\n        V: AsRef<std::ffi::OsStr>,\r\n    {\r\n        // Resolve the program for the OS spawn before portable-pty's own `which`\r\n        // can pick an extensionless shebang shim that CreateProcessW rejects\r\n        // (os error 193). On Windows this PATHEXT-resolves a bare name and wraps a\r\n        // non-PE target (.cmd/.bat → cmd.exe, .ps1 → powershell); off Windows it\r\n        // is a passthrough. The wrap prefix args precede the caller's args.\r\n        // [impl->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]\r\n        let (program, prefix) = crate::winprog::resolve_for_pty(program.as_ref());\r\n        let mut cmd = CommandBuilder::new(&program);\r\n        for a in prefix {\r\n            cmd.arg(a);\r\n        }\r\n        for a in args {\r\n            cmd.arg(a);\r\n        }\r\n        if let Some(dir) = cwd {\r\n            cmd.cwd(dir);\r\n        }\r\n        for (k, v) in env {\r\n            cmd.env(k, v);\r\n        }\r\n        Self::spawn(cmd, size)\r\n    }\r\n\r\n    /// Take the master reader. Returns [`SurfaceError::ReaderTaken`] on the\r\n    /// second call — output is drained by exactly one consumer (the A2 drain\r\n    /// thread in the hosted path; a test consumer otherwise).\r\n    pub fn take_reader(&self) -> Result<Box<dyn Read + Send>, SurfaceError> {\r\n        self.reader\r\n            .lock()\r\n            .unwrap()\r\n            .take()\r\n            .ok_or(SurfaceError::ReaderTaken)\r\n    }\r\n\r\n    /// The shared writer handle — cloned to the A2 DSR drain thread so its\r\n    /// auto-answer goes through the same serialized writer the caller injects\r\n    /// input through (no torn writes).\r\n    pub(crate) fn shared_writer(&self) -> SharedWriter {\r\n        Arc::clone(&self.writer)\r\n    }\r\n\r\n    /// Poll for child exit without blocking.\r\n    pub fn try_wait(&self) -> Result<Option<ExitStatus>, SurfaceError> {\r\n        self.child\r\n            .lock()\r\n            .unwrap()\r\n            .try_wait()\r\n            .map_err(SurfaceError::Io)\r\n    }\r\n\r\n    /// Block until the child exits, yielding its status.\r\n    pub fn wait(&self) -> Result<ExitStatus, SurfaceError> {\r\n        self.child.lock().unwrap().wait().map_err(SurfaceError::Io)\r\n    }\r\n\r\n    /// Terminate the child. Safe to call from another thread while `wait`\r\n    /// blocks (uses the split `ChildKiller`). Confirm the reap via\r\n    /// [`PtySession::wait`] / [`PtySession::try_wait`].\r\n    pub fn kill(&self) -> Result<(), SurfaceError> {\r\n        let res = self.killer.lock().unwrap().kill();\r\n        #[cfg(windows)]\r\n        {\r\n            // portable-pty 0.8's Windows `ChildKiller` returns the spurious\r\n            // `last_os_error` even when `TerminateProcess` *succeeds* (the\r\n            // upstream success check is inverted; `WinChild::kill` swallows it,\r\n            // the cloned killer does not). Its Result is therefore untrustworthy\r\n            // — kill is best-effort; confirm death by reaping.\r\n            let _ = res;\r\n            Ok(())\r\n        }\r\n        #[cfg(unix)]\r\n        {\r\n            res.map_err(SurfaceError::Io)\r\n        }\r\n    }\r\n\r\n    /// The child's OS process id, if the backend exposes one.\r\n    pub fn process_id(&self) -> Option<u32> {\r\n        self.pid\r\n    }\r\n}\r\n\r\n// [impl->REQ-TERM-1]\r\nimpl SessionSurface for PtySession {\r\n    fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError> {\r\n        let mut w = self.writer.lock().unwrap();\r\n        w.write_all(bytes).map_err(SurfaceError::Io)?;\r\n        w.flush().map_err(SurfaceError::Io)\r\n    }\r\n\r\n    fn resize(&self, size: SurfaceSize) -> Result<(), SurfaceError> {\r\n        // Hold the master lock so resize never races a concurrent reader/resize\r\n        // (Spike #5: resize races the reader on Windows).\r\n        self.master\r\n            .lock()\r\n            .unwrap()\r\n            .resize(size.into())\r\n            .map_err(|e| SurfaceError::Io(io_other(e)))\r\n    }\r\n}\r\n\r\n/// Fold a `portable-pty` error (a private `anyhow::Error` alias) into a\r\n/// `std::io::Error` so the crate's public surface never leaks `anyhow`. Generic\r\n/// over `Display` to avoid naming the un-exported error type.\r\nfn io_other<E: std::fmt::Display>(e: E) -> std::io::Error {\r\n    std::io::Error::other(e.to_string())\r\n}\r\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,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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term\\src\\pty.rs"}}},"isError":false,"timestamp":1784195780396}}
{"type":"message","id":"475e9a8f","parentId":"6a4e7938","timestamp":"2026-07-16T09:56:20.410Z","message":{"role":"toolResult","toolCallId":"call_1u4yl09vzprlpi2oD81yUhGO|fc_008390f508d1d09b016a58aac456d4819aa6aebefe6a0d9ce4","toolName":"read","content":[{"type":"text","text":"//! Remote terminal attach records (M4-D5b, REQ-INST-8) — the byte-stream\n//! viewport onto an instance running on another node.\n//!\n//! Remote-control is a **distinct mode from local operation** (CONTEXT\n//! §Remote-control vs local operation): compute + files stay on the target\n//! node; the operator's node is a viewport — a Shell-like driven surface,\n//! daemon-to-daemon over the broker-owned QUIC streams (D4). The records here\n//! are the wire shape of that viewport: the operator node opens a bidi stream,\n//! sends [`AttachRecord::Request`], and the target node pumps its session's\n//! `OutputLog` chunks back as [`AttachRecord::Output`] while injecting\n//! [`AttachRecord::Input`] into the PTY through the journaled exactly-once\n//! input path.\n//!\n//! Framing is the shared NDJSON ([`crate::net::ndjson`]); chunk bytes ride\n//! base64 inside the JSON (the same posture as the IPC catalog's\n//! `data_b64` fields — no binary fork of the codec). `Output.seq` is the\n//! target session's `OutputLog` sequence verbatim, so the operator side dedups\n//! replays (a target-brain restart redrives journaled sends) by cursor, and a\n//! restarted operator brain resubscribes the QUIC stream and skips what it\n//! already rendered — gapless exactly-once on both ends, composed entirely\n//! from the D4/B5 machinery.\n//!\n//! Like [`WanMessage`](crate::net::wanmsg::WanMessage), attach records carry\n//! **no origin field** — the access gate's subject is the QUIC handshake\n//! identity from the broker's stream table (KNOWN-HAZARDS 7.5,\n//! REQ-HAZARD-WAN-ORIGIN-AUTH).\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::net::ndjson::{self, NdjsonDecoder};\n\n/// What an operator's attach is *for* (REQ-RCVIEW-1 / REQ-KICK-1). Three-valued,\n/// not a binary role — a plain `Control` request to an already-controlled endpoint\n/// is REFUSED (never an auto-viewer, never a silent displace); explicit `Take` is\n/// the only way to displace the incumbent controller.\n///\n/// Wire default is [`AttachIntent::Control`] (`#[serde(default)]` on\n/// [`AttachRecord::Request`]): an N-1 operator that omits the field drives a FREE\n/// endpoint exactly as before, but — lacking the `Take` variant — can never\n/// silently steal a controlled one (it gets the busy-refusal reply instead).\n// [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum AttachIntent {\n    /// Read-only viewer: output only, no input, no resize. Never displaces a\n    /// controller; coexists with any number of other viewers.\n    Viewer,\n    /// Become the interactive controller — but only of a FREE endpoint; a\n    /// `Control` request to a controlled endpoint is refused with guidance.\n    #[default]\n    Control,\n    /// Kick the incumbent controller (loud [`AttachRecord::Displaced`] notice to\n    /// them) and take control in one motion (`spt rc --take` / picker Kick).\n    Take,\n}\n\n/// One attach-protocol record. Tagged (`kind`) so both directions share one\n/// stream decoder; an unknown future kind fails the line's decode and is\n/// skipped (degrade, don't die — same posture as every wire feed). The\n/// W2.5 additions (`intent`, `Resize`, `Size`, `Displaced`) are additive: an\n/// N-1 peer omits `intent` (→ `Control`) and skips the unknown record kinds.\n// [impl->REQ-INST-8] [impl->REQ-RCVIEW-1]\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(tag = \"kind\", rename_all = \"snake_case\")]\npub enum AttachRecord {\n    /// Operator → target, first record on the stream: attach to `session_id`,\n    /// replaying output from `from_seq` (the operator's resume cursor — 0 for\n    /// a fresh attach, the last-rendered seq + 1 on re-attach). `intent` selects\n    /// viewer / control / take (defaulted to `Control` for N-1 operators).\n    ///\n    /// `endpoint_id` is the REMOTE cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    /// an operator on ANOTHER node cannot know the owning node's local\n    /// `session_id`, so it names the ENDPOINT and the owning node resolves\n    /// endpoint→its-own `session_id` SERVER-SIDE (it is authoritative for its own\n    /// session table — same subject-from-broker-table posture as the origin gate).\n    /// ABSENT on the LOCAL path, which sends a resolved `session_id`;\n    /// `skip_serializing_if` keeps the local wire byte-identical and N-1\n    /// compatible (an old target ignores the unknown field and reads `session_id`).\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n    Request {\n        session_id: u64,\n        from_seq: u64,\n        #[serde(default)]\n        intent: AttachIntent,\n        #[serde(default, skip_serializing_if = \"Option::is_none\")]\n        endpoint_id: Option<String>,\n    },\n    /// Target → operator: one session output chunk. `seq` is the target\n    /// session's `OutputLog` sequence verbatim (the operator's dedup cursor).\n    Output { seq: u64, data_b64: String },\n    /// Operator → target: input bytes for the session's PTY. `op_id` is the\n    /// operator-minted durable id the target feeds into its journaled\n    /// exactly-once input path (a replayed record cannot double-type). Honored\n    /// only from the CONTROLLER's stream (a viewer sends none).\n    Input { data_b64: String, op_id: u64 },\n    /// Operator(controller) → target: resize the PTY to the controller's viewport.\n    /// Controller-EXCLUSIVE — the broker rejects a `Resize` arriving on a viewer's\n    /// stream (REQ-RCVIEW-1, resize is controller-exclusive for ConPTY repaint cost).\n    Resize { rows: u16, cols: u16 },\n    /// Target → operator(viewer): the current PTY size, sent on a viewer's attach\n    /// and on every controller resize, so the viewer letterboxes client-side\n    /// (never resizing the PTY itself).\n    Size { rows: u16, cols: u16 },\n    /// Target → operator: the session's child exited; the viewport is done.\n    Exit { code: Option<i32> },\n    /// Target → operator(controller): you have been displaced as controller by\n    /// `by` (a `--take`/`kick`). The displaced controller renders this LOUD notice\n    /// and detaches — it is NOT demoted to a viewer (REQ-KICK-1).\n    Displaced { by: String },\n}\n\nimpl AttachRecord {\n    /// Encode as one NDJSON line (the JSON document + `\\n`).\n    pub fn encode_line(&self) -> Vec<u8> {\n        ndjson::encode_line(self)\n    }\n}\n\n/// The attach stream's NDJSON reassembler — the shared\n/// [`NdjsonDecoder`](crate::net::ndjson::NdjsonDecoder) typed to\n/// [`AttachRecord`]s. One decoder per stream side.\npub type AttachDecoder = NdjsonDecoder<AttachRecord>;\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    // [unit->REQ-INST-8] every record kind round-trips through the NDJSON line\n    // encoding, and the decoder skips an unknown future kind without wedging\n    // the stream (forward compat: degrade, don't die).\n    #[test]\n    fn records_round_trip_and_unknown_kind_is_skipped() {\n        let records = vec![\n            AttachRecord::Request {\n                session_id: 7,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                endpoint_id: None,\n            },\n            AttachRecord::Output {\n                seq: 3,\n                data_b64: \"aGk=\".into(),\n            },\n            AttachRecord::Input {\n                data_b64: \"bHM=\".into(),\n                op_id: 42,\n            },\n            AttachRecord::Resize { rows: 40, cols: 120 },\n            AttachRecord::Size { rows: 40, cols: 120 },\n            AttachRecord::Exit { code: Some(0) },\n            AttachRecord::Displaced { by: \"hfenduleam\".into() },\n        ];\n        let mut wire = Vec::new();\n        for r in &records {\n            wire.extend_from_slice(&r.encode_line());\n        }\n        // An unknown future kind in the middle of the feed is skipped.\n        let mut with_junk = records[0].encode_line();\n        with_junk.extend_from_slice(b\"{\\\"kind\\\":\\\"added_at_d9\\\",\\\"x\\\":1}\\n\");\n        with_junk.extend_from_slice(&records[3].encode_line());\n\n        let mut dec = AttachDecoder::new();\n        assert_eq!(dec.push(&wire), records, \"all kinds round-trip in order\");\n\n        let mut dec = AttachDecoder::new();\n        assert_eq!(\n            dec.push(&with_junk),\n            vec![records[0].clone(), records[3].clone()],\n            \"unknown kind skipped, feed not wedged\"\n        );\n    }\n\n    // [unit->REQ-INST-8] chunk-split safety rides the shared decoder: a record\n    // split mid-bytes across pushes reassembles (QUIC chunking is not record\n    // framing).\n    #[test]\n    fn record_split_across_chunks_reassembles() {\n        let r = AttachRecord::Output {\n            seq: 9,\n            data_b64: \"c3BsaXQtbWU=\".into(),\n        };\n        let line = r.encode_line();\n        let mut dec = AttachDecoder::new();\n        assert!(dec.push(&line[..line.len() / 2]).is_empty());\n        assert_eq!(dec.push(&line[line.len() / 2..]), vec![r]);\n    }\n\n    // [unit->REQ-RCVIEW-1] N-1 compat: an attach Request encoded WITHOUT the\n    // `intent` field (an old operator) decodes to `Control` — it drives a free\n    // endpoint as before, and, lacking the Take variant, can never silently\n    // steal a controlled one (it gets the busy-refusal instead).\n    #[test]\n    fn request_without_intent_defaults_to_control() {\n        let mut dec = AttachDecoder::new();\n        let legacy = b\"{\\\"kind\\\":\\\"request\\\",\\\"session_id\\\":7,\\\"from_seq\\\":0}\\n\";\n        assert_eq!(\n            dec.push(legacy),\n            vec![AttachRecord::Request {\n                session_id: 7,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                endpoint_id: None,\n            }],\n            \"omitted intent defaults to Control (N-1 operator)\"\n        );\n        assert_eq!(AttachIntent::default(), AttachIntent::Control);\n    }\n\n    // [unit->REQ-RC-CROSS-NODE-ATTACH] the remote leg's additive `endpoint_id`:\n    // a REMOTE operator (which cannot know the owning node's local session_id)\n    // names the endpoint and it round-trips; the LOCAL path omits it, and\n    // `skip_serializing_if` keeps that wire BYTE-IDENTICAL to N-1 (no\n    // `endpoint_id` key) so an old target still decodes it and reads session_id.\n    #[test]\n    fn request_endpoint_id_is_additive_and_local_wire_is_unchanged() {\n        // Remote: endpoint_id present, round-trips.\n        let remote = AttachRecord::Request {\n            session_id: 0,\n            from_seq: 0,\n            intent: AttachIntent::Control,\n            endpoint_id: Some(\"ling@gravity\".into()),\n        };\n        let mut dec = AttachDecoder::new();\n        assert_eq!(dec.push(&remote.encode_line()), vec![remote.clone()]);\n        assert!(\n            String::from_utf8_lossy(&remote.encode_line()).contains(\"ling@gravity\"),\n            \"remote wire carries the endpoint id\"\n        );\n\n        // Local: endpoint_id None is NOT serialized — byte-identical to N-1.\n        let local = AttachRecord::Request {\n            session_id: 7,\n            from_seq: 0,\n            intent: AttachIntent::Control,\n            endpoint_id: None,\n        };\n        assert!(\n            !String::from_utf8_lossy(&local.encode_line()).contains(\"endpoint_id\"),\n            \"local wire omits endpoint_id (N-1 byte-identity)\"\n        );\n        // And it still decodes (through the same shared decoder) to the local shape.\n        let mut dec = AttachDecoder::new();\n        assert_eq!(dec.push(&local.encode_line()), vec![local]);\n    }\n\n    // [unit->REQ-RCVIEW-1] the three intents round-trip distinctly on the wire.\n    #[test]\n    fn attach_intents_round_trip() {\n        for intent in [AttachIntent::Viewer, AttachIntent::Control, AttachIntent::Take] {\n            let r = AttachRecord::Request { session_id: 1, from_seq: 0, intent, endpoint_id: None };\n            let mut dec = AttachDecoder::new();\n            assert_eq!(dec.push(&r.encode_line()), vec![r]);\n        }\n    }\n\n    // [unit->REQ-KICK-1] the kick protocol round-trips: the `Take` intent (the\n    // explicit displacement request) and the `Displaced{by}` loud notice the\n    // displaced controller renders both survive the wire intact.\n    #[test]\n    fn kick_take_and_displaced_round_trip() {\n        let take = AttachRecord::Request {\n            session_id: 9,\n            from_seq: 0,\n            intent: AttachIntent::Take,\n            endpoint_id: None,\n        };\n        let displaced = AttachRecord::Displaced { by: \"hfenduleam\".into() };\n        for r in [take, displaced] {\n            let mut dec = AttachDecoder::new();\n            assert_eq!(dec.push(&r.encode_line()), vec![r]);\n        }\n    }\n}\n"}],"details":{"displayContent":{"text":"//! Remote terminal attach records (M4-D5b, REQ-INST-8) — the byte-stream\r\n//! viewport onto an instance running on another node.\r\n//!\r\n//! Remote-control is a **distinct mode from local operation** (CONTEXT\r\n//! §Remote-control vs local operation): compute + files stay on the target\r\n//! node; the operator's node is a viewport — a Shell-like driven surface,\r\n//! daemon-to-daemon over the broker-owned QUIC streams (D4). The records here\r\n//! are the wire shape of that viewport: the operator node opens a bidi stream,\r\n//! sends [`AttachRecord::Request`], and the target node pumps its session's\r\n//! `OutputLog` chunks back as [`AttachRecord::Output`] while injecting\r\n//! [`AttachRecord::Input`] into the PTY through the journaled exactly-once\r\n//! input path.\r\n//!\r\n//! Framing is the shared NDJSON ([`crate::net::ndjson`]); chunk bytes ride\r\n//! base64 inside the JSON (the same posture as the IPC catalog's\r\n//! `data_b64` fields — no binary fork of the codec). `Output.seq` is the\r\n//! target session's `OutputLog` sequence verbatim, so the operator side dedups\r\n//! replays (a target-brain restart redrives journaled sends) by cursor, and a\r\n//! restarted operator brain resubscribes the QUIC stream and skips what it\r\n//! already rendered — gapless exactly-once on both ends, composed entirely\r\n//! from the D4/B5 machinery.\r\n//!\r\n//! Like [`WanMessage`](crate::net::wanmsg::WanMessage), attach records carry\r\n//! **no origin field** — the access gate's subject is the QUIC handshake\r\n//! identity from the broker's stream table (KNOWN-HAZARDS 7.5,\r\n//! REQ-HAZARD-WAN-ORIGIN-AUTH).\r\n\r\nuse serde::{Deserialize, Serialize};\r\n\r\nuse crate::net::ndjson::{self, NdjsonDecoder};\r\n\r\n/// What an operator's attach is *for* (REQ-RCVIEW-1 / REQ-KICK-1). Three-valued,\r\n/// not a binary role — a plain `Control` request to an already-controlled endpoint\r\n/// is REFUSED (never an auto-viewer, never a silent displace); explicit `Take` is\r\n/// the only way to displace the incumbent controller.\r\n///\r\n/// Wire default is [`AttachIntent::Control`] (`#[serde(default)]` on\r\n/// [`AttachRecord::Request`]): an N-1 operator that omits the field drives a FREE\r\n/// endpoint exactly as before, but — lacking the `Take` variant — can never\r\n/// silently steal a controlled one (it gets the busy-refusal reply instead).\r\n// [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]\r\n#[serde(rename_all = \"snake_case\")]\r\npub enum AttachIntent {\r\n    /// Read-only viewer: output only, no input, no resize. Never displaces a\r\n    /// controller; coexists with any number of other viewers.\r\n    Viewer,\r\n    /// Become the interactive controller — but only of a FREE endpoint; a\r\n    /// `Control` request to a controlled endpoint is refused with guidance.\r\n    #[default]\r\n    Control,\r\n    /// Kick the incumbent controller (loud [`AttachRecord::Displaced`] notice to\r\n    /// them) and take control in one motion (`spt rc --take` / picker Kick).\r\n    Take,\r\n}\r\n\r\n/// One attach-protocol record. Tagged (`kind`) so both directions share one\r\n/// stream decoder; an unknown future kind fails the line's decode and is\r\n/// skipped (degrade, don't die — same posture as every wire feed). The\r\n/// W2.5 additions (`intent`, `Resize`, `Size`, `Displaced`) are additive: an\r\n/// N-1 peer omits `intent` (→ `Control`) and skips the unknown record kinds.\r\n// [impl->REQ-INST-8] [impl->REQ-RCVIEW-1]\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\n#[serde(tag = \"kind\", rename_all = \"snake_case\")]\r\npub enum AttachRecord {\r\n    /// Operator → target, first record on the stream: attach to `session_id`,\r\n    /// replaying output from `from_seq` (the operator's resume cursor — 0 for\r\n    /// a fresh attach, the last-rendered seq + 1 on re-attach). `intent` selects\r\n    /// viewer / control / take (defaulted to `Control` for N-1 operators).\r\n    ///\r\n    /// `endpoint_id` is the REMOTE cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\r\n    /// an operator on ANOTHER node cannot know the owning node's local\r\n    /// `session_id`, so it names the ENDPOINT and the owning node resolves\r\n    /// endpoint→its-own `session_id` SERVER-SIDE (it is authoritative for its own\r\n    /// session table — same subject-from-broker-table posture as the origin gate).\r\n    /// ABSENT on the LOCAL path, which sends a resolved `session_id`;\r\n    /// `skip_serializing_if` keeps the local wire byte-identical and N-1\r\n    /// compatible (an old target ignores the unknown field and reads `session_id`).\r\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n    Request {\r\n        session_id: u64,\r\n        from_seq: u64,\r\n        #[serde(default)]\r\n        intent: AttachIntent,\r\n        #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n        endpoint_id: Option<String>,\r\n    },\r\n    /// Target → operator: one session output chunk. `seq` is the target\r\n    /// session's `OutputLog` sequence verbatim (the operator's dedup cursor).\r\n    Output { seq: u64, data_b64: String },\r\n    /// Operator → target: input bytes for the session's PTY. `op_id` is the\r\n    /// operator-minted durable id the target feeds into its journaled\r\n    /// exactly-once input path (a replayed record cannot double-type). Honored\r\n    /// only from the CONTROLLER's stream (a viewer sends none).\r\n    Input { data_b64: String, op_id: u64 },\r\n    /// Operator(controller) → target: resize the PTY to the controller's viewport.\r\n    /// Controller-EXCLUSIVE — the broker rejects a `Resize` arriving on a viewer's\r\n    /// stream (REQ-RCVIEW-1, resize is controller-exclusive for ConPTY repaint cost).\r\n    Resize { rows: u16, cols: u16 },\r\n    /// Target → operator(viewer): the current PTY size, sent on a viewer's attach\r\n    /// and on every controller resize, so the viewer letterboxes client-side\r\n    /// (never resizing the PTY itself).\r\n    Size { rows: u16, cols: u16 },\r\n    /// Target → operator: the session's child exited; the viewport is done.\r\n    Exit { code: Option<i32> },\r\n    /// Target → operator(controller): you have been displaced as controller by\r\n    /// `by` (a `--take`/`kick`). The displaced controller renders this LOUD notice\r\n    /// and detaches — it is NOT demoted to a viewer (REQ-KICK-1).\r\n    Displaced { by: String },\r\n}\r\n\r\nimpl AttachRecord {\r\n    /// Encode as one NDJSON line (the JSON document + `\\n`).\r\n    pub fn encode_line(&self) -> Vec<u8> {\r\n        ndjson::encode_line(self)\r\n    }\r\n}\r\n\r\n/// The attach stream's NDJSON reassembler — the shared\r\n/// [`NdjsonDecoder`](crate::net::ndjson::NdjsonDecoder) typed to\r\n/// [`AttachRecord`]s. One decoder per stream side.\r\npub type AttachDecoder = NdjsonDecoder<AttachRecord>;\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    // [unit->REQ-INST-8] every record kind round-trips through the NDJSON line\r\n    // encoding, and the decoder skips an unknown future kind without wedging\r\n    // the stream (forward compat: degrade, don't die).\r\n    #[test]\r\n    fn records_round_trip_and_unknown_kind_is_skipped() {\r\n        let records = vec![\r\n            AttachRecord::Request {\r\n                session_id: 7,\r\n                from_seq: 0,\r\n                intent: AttachIntent::Control,\r\n                endpoint_id: None,\r\n            },\r\n            AttachRecord::Output {\r\n                seq: 3,\r\n                data_b64: \"aGk=\".into(),\r\n            },\r\n            AttachRecord::Input {\r\n                data_b64: \"bHM=\".into(),\r\n                op_id: 42,\r\n            },\r\n            AttachRecord::Resize { rows: 40, cols: 120 },\r\n            AttachRecord::Size { rows: 40, cols: 120 },\r\n            AttachRecord::Exit { code: Some(0) },\r\n            AttachRecord::Displaced { by: \"hfenduleam\".into() },\r\n        ];\r\n        let mut wire = Vec::new();\r\n        for r in &records {\r\n            wire.extend_from_slice(&r.encode_line());\r\n        }\r\n        // An unknown future kind in the middle of the feed is skipped.\r\n        let mut with_junk = records[0].encode_line();\r\n        with_junk.extend_from_slice(b\"{\\\"kind\\\":\\\"added_at_d9\\\",\\\"x\\\":1}\\n\");\r\n        with_junk.extend_from_slice(&records[3].encode_line());\r\n\r\n        let mut dec = AttachDecoder::new();\r\n        assert_eq!(dec.push(&wire), records, \"all kinds round-trip in order\");\r\n\r\n        let mut dec = AttachDecoder::new();\r\n        assert_eq!(\r\n            dec.push(&with_junk),\r\n            vec![records[0].clone(), records[3].clone()],\r\n            \"unknown kind skipped, feed not wedged\"\r\n        );\r\n    }\r\n\r\n    // [unit->REQ-INST-8] chunk-split safety rides the shared decoder: a record\r\n    // split mid-bytes across pushes reassembles (QUIC chunking is not record\r\n    // framing).\r\n    #[test]\r\n    fn record_split_across_chunks_reassembles() {\r\n        let r = AttachRecord::Output {\r\n            seq: 9,\r\n            data_b64: \"c3BsaXQtbWU=\".into(),\r\n        };\r\n        let line = r.encode_line();\r\n        let mut dec = AttachDecoder::new();\r\n        assert!(dec.push(&line[..line.len() / 2]).is_empty());\r\n        assert_eq!(dec.push(&line[line.len() / 2..]), vec![r]);\r\n    }\r\n\r\n    // [unit->REQ-RCVIEW-1] N-1 compat: an attach Request encoded WITHOUT the\r\n    // `intent` field (an old operator) decodes to `Control` — it drives a free\r\n    // endpoint as before, and, lacking the Take variant, can never silently\r\n    // steal a controlled one (it gets the busy-refusal instead).\r\n    #[test]\r\n    fn request_without_intent_defaults_to_control() {\r\n        let mut dec = AttachDecoder::new();\r\n        let legacy = b\"{\\\"kind\\\":\\\"request\\\",\\\"session_id\\\":7,\\\"from_seq\\\":0}\\n\";\r\n        assert_eq!(\r\n            dec.push(legacy),\r\n            vec![AttachRecord::Request {\r\n                session_id: 7,\r\n                from_seq: 0,\r\n                intent: AttachIntent::Control,\r\n                endpoint_id: None,\r\n            }],\r\n            \"omitted intent defaults to Control (N-1 operator)\"\r\n        );\r\n        assert_eq!(AttachIntent::default(), AttachIntent::Control);\r\n    }\r\n\r\n    // [unit->REQ-RC-CROSS-NODE-ATTACH] the remote leg's additive `endpoint_id`:\r\n    // a REMOTE operator (which cannot know the owning node's local session_id)\r\n    // names the endpoint and it round-trips; the LOCAL path omits it, and\r\n    // `skip_serializing_if` keeps that wire BYTE-IDENTICAL to N-1 (no\r\n    // `endpoint_id` key) so an old target still decodes it and reads session_id.\r\n    #[test]\r\n    fn request_endpoint_id_is_additive_and_local_wire_is_unchanged() {\r\n        // Remote: endpoint_id present, round-trips.\r\n        let remote = AttachRecord::Request {\r\n            session_id: 0,\r\n            from_seq: 0,\r\n            intent: AttachIntent::Control,\r\n            endpoint_id: Some(\"ling@gravity\".into()),\r\n        };\r\n        let mut dec = AttachDecoder::new();\r\n        assert_eq!(dec.push(&remote.encode_line()), vec![remote.clone()]);\r\n        assert!(\r\n            String::from_utf8_lossy(&remote.encode_line()).contains(\"ling@gravity\"),\r\n            \"remote wire carries the endpoint id\"\r\n        );\r\n\r\n        // Local: endpoint_id None is NOT serialized — byte-identical to N-1.\r\n        let local = AttachRecord::Request {\r\n            session_id: 7,\r\n            from_seq: 0,\r\n            intent: AttachIntent::Control,\r\n            endpoint_id: None,\r\n        };\r\n        assert!(\r\n            !String::from_utf8_lossy(&local.encode_line()).contains(\"endpoint_id\"),\r\n            \"local wire omits endpoint_id (N-1 byte-identity)\"\r\n        );\r\n        // And it still decodes (through the same shared decoder) to the local shape.\r\n        let mut dec = AttachDecoder::new();\r\n        assert_eq!(dec.push(&local.encode_line()), vec![local]);\r\n    }\r\n\r\n    // [unit->REQ-RCVIEW-1] the three intents round-trip distinctly on the wire.\r\n    #[test]\r\n    fn attach_intents_round_trip() {\r\n        for intent in [AttachIntent::Viewer, AttachIntent::Control, AttachIntent::Take] {\r\n            let r = AttachRecord::Request { session_id: 1, from_seq: 0, intent, endpoint_id: None };\r\n            let mut dec = AttachDecoder::new();\r\n            assert_eq!(dec.push(&r.encode_line()), vec![r]);\r\n        }\r\n    }\r\n\r\n    // [unit->REQ-KICK-1] the kick protocol round-trips: the `Take` intent (the\r\n    // explicit displacement request) and the `Displaced{by}` loud notice the\r\n    // displaced controller renders both survive the wire intact.\r\n    #[test]\r\n    fn kick_take_and_displaced_round_trip() {\r\n        let take = AttachRecord::Request {\r\n            session_id: 9,\r\n            from_seq: 0,\r\n            intent: AttachIntent::Take,\r\n            endpoint_id: None,\r\n        };\r\n        let displaced = AttachRecord::Displaced { by: \"hfenduleam\".into() };\r\n        for r in [take, displaced] {\r\n            let mut dec = AttachDecoder::new();\r\n            assert_eq!(dec.push(&r.encode_line()), vec![r]);\r\n        }\r\n    }\r\n}\r\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,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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-net\\src\\net\\attach.rs"}}},"isError":false,"timestamp":1784195780410}}
{"type":"message","id":"c303ac17","parentId":"475e9a8f","timestamp":"2026-07-16T09:56:20.443Z","message":{"role":"toolResult","toolCallId":"call_yrixMqyibbHKr7D5wFpbo7fn|fc_008390f508d1d09b016a58aac456c8819aa4f59e1e2b1c24ee","toolName":"read","content":[{"type":"text","text":"/// and re-attaches idempotently).\n// [impl->REQ-INST-8]\npub fn request_attach(\n    brain: &mut Brain,\n    conn_id: u64,\n    session_id: u64,\n    from_seq: u64,\n    open_op: MintedOp,\n    intent: AttachIntent,\n) -> io::Result<u64> {\n    request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)\n}\n\n/// Like [`request_attach`], but for the cross-node leg (#4,\n/// REQ-RC-CROSS-NODE-ATTACH): the remote operator cannot know the owning node's\n/// local session id, so it passes `endpoint_id = Some(endpoint)` (and a\n/// placeholder `session_id` of `0` the owning node ignores). The owning node\n/// resolves the endpoint to its OWN session id server-side\n/// ([`resolve_local_session`]) — authoritative for its own table. `None` here is\n/// exactly [`request_attach`]: the local path's wire bytes stay byte-identical.\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub fn request_attach_endpoint(\n    brain: &mut Brain,\n    conn_id: u64,\n    session_id: u64,\n    from_seq: u64,\n    open_op: MintedOp,\n    intent: AttachIntent,\n    endpoint_id: Option<&str>,\n) -> io::Result<u64> {\n    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n    let line = ndjson::encode_line(&AttachRecord::Request {\n        session_id,\n        from_seq,\n        intent,\n        endpoint_id: endpoint_id.map(str::to_string),\n    });\n    brain.net_stream_send(opened.stream_id, &line, None, false)?;\n    Ok(opened.stream_id)\n}\n\n/// Operator-side (CONTROLLER): send a resize record onto an attach stream —\n/// the controller's viewport size, forwarded into the target PTY (REQ-RCVIEW-1,\n/// controller-exclusive: the broker rejects a resize from a viewer's stream).\n/// Sent on attach (initial size) and on every window-change. Unjournaled (a\n/// stale resize is harmless — the next one corrects it).\n// [impl->REQ-RCVIEW-1]\npub fn send_attach_resize(\n    brain: &mut Brain,\n    stream_id: u64,\n    rows: u16,\n    cols: u16,\n) -> io::Result<()> {\n    let line = ndjson::encode_line(&AttachRecord::Resize { rows, cols });\n    brain.net_stream_send(stream_id, &line, None, false)?;\n    Ok(())\n}\n\n/// Operator-side: send one input record (the keystrokes) onto an attach\n/// stream. `wire_op` is the operator-minted durable id the TARGET journals the\n/// PTY write under — exactly-once at the keys, however often the record\n/// crosses the wire. The send itself is unjournaled (see module docs).\n// [impl->REQ-INST-8]\npub fn send_attach_input(\n    brain: &mut Brain,\n    stream_id: u64,\n    bytes: &[u8],\n    wire_op: u64,\n) -> io::Result<()> {\n    let line = ndjson::encode_line(&AttachRecord::Input {\n        data_b64: encode_bytes(bytes),\n        op_id: wire_op,\n    });\n    brain.net_stream_send(stream_id, &line, None, false)?;\n    Ok(())\n}\n"}],"details":{"displayContent":{"text":"/// and re-attaches idempotently).\r\n// [impl->REQ-INST-8]\r\npub fn request_attach(\r\n    brain: &mut Brain,\r\n    conn_id: u64,\r\n    session_id: u64,\r\n    from_seq: u64,\r\n    open_op: MintedOp,\r\n    intent: AttachIntent,\r\n) -> io::Result<u64> {\r\n    request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)\r\n}\r\n\r\n/// Like [`request_attach`], but for the cross-node leg (#4,\r\n/// REQ-RC-CROSS-NODE-ATTACH): the remote operator cannot know the owning node's\r\n/// local session id, so it passes `endpoint_id = Some(endpoint)` (and a\r\n/// placeholder `session_id` of `0` the owning node ignores). The owning node\r\n/// resolves the endpoint to its OWN session id server-side\r\n/// ([`resolve_local_session`]) — authoritative for its own table. `None` here is\r\n/// exactly [`request_attach`]: the local path's wire bytes stay byte-identical.\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub fn request_attach_endpoint(\r\n    brain: &mut Brain,\r\n    conn_id: u64,\r\n    session_id: u64,\r\n    from_seq: u64,\r\n    open_op: MintedOp,\r\n    intent: AttachIntent,\r\n    endpoint_id: Option<&str>,\r\n) -> io::Result<u64> {\r\n    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\r\n    let line = ndjson::encode_line(&AttachRecord::Request {\r\n        session_id,\r\n        from_seq,\r\n        intent,\r\n        endpoint_id: endpoint_id.map(str::to_string),\r\n    });\r\n    brain.net_stream_send(opened.stream_id, &line, None, false)?;\r\n    Ok(opened.stream_id)\r\n}\r\n\r\n/// Operator-side (CONTROLLER): send a resize record onto an attach stream —\r\n/// the controller's viewport size, forwarded into the target PTY (REQ-RCVIEW-1,\r\n/// controller-exclusive: the broker rejects a resize from a viewer's stream).\r\n/// Sent on attach (initial size) and on every window-change. Unjournaled (a\r\n/// stale resize is harmless — the next one corrects it).\r\n// [impl->REQ-RCVIEW-1]\r\npub fn send_attach_resize(\r\n    brain: &mut Brain,\r\n    stream_id: u64,\r\n    rows: u16,\r\n    cols: u16,\r\n) -> io::Result<()> {\r\n    let line = ndjson::encode_line(&AttachRecord::Resize { rows, cols });\r\n    brain.net_stream_send(stream_id, &line, None, false)?;\r\n    Ok(())\r\n}\r\n\r\n/// Operator-side: send one input record (the keystrokes) onto an attach\r\n/// stream. `wire_op` is the operator-minted durable id the TARGET journals the\r\n/// PTY write under — exactly-once at the keys, however often the record\r\n/// crosses the wire. The send itself is unjournaled (see module docs).\r\n// [impl->REQ-INST-8]\r\npub fn send_attach_input(\r\n    brain: &mut Brain,\r\n    stream_id: u64,\r\n    bytes: &[u8],\r\n    wire_op: u64,\r\n) -> io::Result<()> {\r\n    let line = ndjson::encode_line(&AttachRecord::Input {\r\n        data_b64: encode_bytes(bytes),\r\n        op_id: wire_op,\r\n    });\r\n    brain.net_stream_send(stream_id, &line, None, false)?;\r\n    Ok(())\r\n}\r\n","startLine":551,"lineNumbers":[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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\attach.rs"}}},"isError":false,"timestamp":1784195780443}}
{"type":"message","id":"f9b0201b","parentId":"c303ac17","timestamp":"2026-07-16T09:56:20.463Z","message":{"role":"toolResult","toolCallId":"call_HlEPld6an9MylNTSkBJrZpL9|fc_008390f508d1d09b016a58aac456c0819aaa0e7cdb82d93bfd","toolName":"read","content":[{"type":"text","text":"fn pump(\n…\n) -> Result<PumpEnd, String> {\n…\n    loop {\n…\n        match ev {\n            BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Output { seq, data_b64 } => {\n                            if seq < cursor {\n                                continue; // already rendered — re-serve dedup\n                            }\n                            cursor = seq + 1;\n                            rendered_any = true;\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\n                            // Track the harness's mouse-reporting mode from its output\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\n                            mouse_scanner.feed(mouse_mode, &bytes);\n                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\n                            // Re-assert the reserved row if the harness output just\n                            // destroyed our scroll region — alt-screen enter or a\n                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\n                            // margin is gone (alt-screen has its own full-screen\n                            // margins; a DECSTBM reset clears ours), so re-set the\n                            // MARGIN + repaint (assert_bytes), not just the text —\n                            // else the harness scrolls back over row 1. Scanned on the\n                            // SAME bytes we rendered, after writing them.\n                            if let Some(status) = status.as_deref_mut() {\n                                if reassert_scanner.feed(&bytes) {\n                                    stdout\n                                        .write_all(&status.assert_bytes())\n                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\n                                }\n                            }\n                            stdout.flush().map_err(|e| format!(\"flush: {e}\"))?;\n                        }\n                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\n                        AttachRecord::Displaced { by } => {\n                            return Ok(PumpEnd::Displaced(by));\n                        }\n                        // Viewer letterbox feed (REQ-RCVIEW-1): on a size change,\n                        // show a one-line indicator when our terminal differs from\n                        // the new PTY size (rc is a byte pump — true clip/pad needs\n                        // a grid model, deferred; the honest size cue renders here).\n                        AttachRecord::Size { rows, cols } if pty_size != Some((rows, cols)) => {\n                            pty_size = Some((rows, cols));\n                            match crossterm::terminal::size() {\n                                Ok((mycols, myrows)) if (myrows, mycols) != (rows, cols) => {\n                                    let _ = write!(\n                                        stdout,\n                                        \"\\r\\n[viewing {cols}x{rows} — your terminal is {mycols}x{myrows}]\\r\\n\"\n                                    );\n                                    let _ = stdout.flush();\n                                }\n                                _ => {}\n                            }\n                        }\n                        _ => {} // Request/Input echoed back: protocol noise, ignore.\n                    }\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                return Ok(serve_eof_end(rendered_any));\n            }\n            BrokerEvent::Error { message } => return Err(message),\n            _ => {} // acks + unrelated events: not ours.\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn parse(armed: &mut bool, input: &[u8]) -> (Vec<u8>, bool) {\n        let p = parse_stdin_chunk(armed, input);\n        (p.forward, p.detach)\n    }\n\n    // [unit->REQ-RC-WIN-VT-OUTPUT] the rc attach path composes the OUTPUT console\n    // mode that renders ANSI escapes instead of printing them literally (#12): it\n    // adds VT + processed-output, preserves the prior bits, and is idempotent.\n    #[cfg(windows)]\n    #[test]\n    fn with_vt_output_enables_virtual_terminal_processing() {\n        use windows_sys::Win32::System::Console::{\n            ENABLE_PROCESSED_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WRAP_AT_EOL_OUTPUT,\n        };\n        let prior = ENABLE_WRAP_AT_EOL_OUTPUT;\n        let got = with_vt_output(prior);\n        assert!(\n            got & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0,\n            \"VT-output processing enabled\"\n        );\n        assert!(got & ENABLE_PROCESSED_OUTPUT != 0, \"processed output enabled\");\n        assert!(\n            got & ENABLE_WRAP_AT_EOL_OUTPUT != 0,\n            \"prior console bits preserved\"\n        );\n…\n    }\n…\n}\n\n[Showing lines 1937-2028 of 3214. Use :2029 to continue]"}],"details":{"truncation":{"content":"                    match rec {\r\n                        AttachRecord::Output { seq, data_b64 } => {\r\n                            if seq < cursor {\r\n                                continue; // already rendered — re-serve dedup\r\n                            }\r\n                            cursor = seq + 1;\r\n                            rendered_any = true;\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\r\n                            // Track the harness's mouse-reporting mode from its output\r\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\r\n                            mouse_scanner.feed(mouse_mode, &bytes);\r\n                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\r\n                            // Re-assert the reserved row if the harness output just\r\n                            // destroyed our scroll region — alt-screen enter or a\r\n                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\r\n                            // margin is gone (alt-screen has its own full-screen\r\n                            // margins; a DECSTBM reset clears ours), so re-set the\r\n                            // MARGIN + repaint (assert_bytes), not just the text —\r\n                            // else the harness scrolls back over row 1. Scanned on the\r\n                            // SAME bytes we rendered, after writing them.\r\n                            if let Some(status) = status.as_deref_mut() {\r\n                                if reassert_scanner.feed(&bytes) {\r\n                                    stdout\r\n                                        .write_all(&status.assert_bytes())\r\n                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\r\n                                }\r\n                            }\r\n                            stdout.flush().map_err(|e| format!(\"flush: {e}\"))?;\r\n                        }\r\n                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\r\n                        AttachRecord::Displaced { by } => {\r\n                            return Ok(PumpEnd::Displaced(by));\r\n                        }\r\n                        // Viewer letterbox feed (REQ-RCVIEW-1): on a size change,\r\n                        // show a one-line indicator when our terminal differs from\r\n                        // the new PTY size (rc is a byte pump — true clip/pad needs\r\n                        // a grid model, deferred; the honest size cue renders here).\r\n                        AttachRecord::Size { rows, cols } if pty_size != Some((rows, cols)) => {\r\n                            pty_size = Some((rows, cols));\r\n                            match crossterm::terminal::size() {\r\n                                Ok((mycols, myrows)) if (myrows, mycols) != (rows, cols) => {\r\n                                    let _ = write!(\r\n                                        stdout,\r\n                                        \"\\r\\n[viewing {cols}x{rows} — your terminal is {mycols}x{myrows}]\\r\\n\"\r\n                                    );\r\n                                    let _ = stdout.flush();\r\n                                }\r\n                                _ => {}\r\n                            }\r\n                        }\r\n                        _ => {} // Request/Input echoed back: protocol noise, ignore.\r\n                    }\r\n                }\r\n            }\r\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\r\n                return Ok(serve_eof_end(rendered_any));\r\n            }\r\n            BrokerEvent::Error { message } => return Err(message),\r\n            _ => {} // acks + unrelated events: not ours.\r\n        }\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    fn parse(armed: &mut bool, input: &[u8]) -> (Vec<u8>, bool) {\r\n        let p = parse_stdin_chunk(armed, input);\r\n        (p.forward, p.detach)\r\n    }\r\n\r\n    // [unit->REQ-RC-WIN-VT-OUTPUT] the rc attach path composes the OUTPUT console\r\n    // mode that renders ANSI escapes instead of printing them literally (#12): it\r\n    // adds VT + processed-output, preserves the prior bits, and is idempotent.\r\n    #[cfg(windows)]\r\n    #[test]\r\n    fn with_vt_output_enables_virtual_terminal_processing() {\r\n        use windows_sys::Win32::System::Console::{\r\n            ENABLE_PROCESSED_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WRAP_AT_EOL_OUTPUT,\r\n        };\r\n        let prior = ENABLE_WRAP_AT_EOL_OUTPUT;\r\n        let got = with_vt_output(prior);\r\n        assert!(\r\n            got & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0,\r\n            \"VT-output processing enabled\"\r\n        );\r\n        assert!(got & ENABLE_PROCESSED_OUTPUT != 0, \"processed output enabled\");\r\n        assert!(\r\n            got & ENABLE_WRAP_AT_EOL_OUTPUT != 0,\r\n            \"prior console bits preserved\"\r","truncated":true,"truncatedBy":"lines","totalLines":1278,"totalBytes":4984,"outputLines":92,"outputBytes":4984,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn pump(\n…\n) -> Result<PumpEnd, String> {\n…\n    loop {\n…\n        match ev {\n            BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\r\n                        AttachRecord::Output { seq, data_b64 } => {\r\n                            if seq < cursor {\r\n                                continue; // already rendered — re-serve dedup\r\n                            }\r\n                            cursor = seq + 1;\r\n                            rendered_any = true;\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\r\n                            // Track the harness's mouse-reporting mode from its output\r\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\r\n                            mouse_scanner.feed(mouse_mode, &bytes);\r\n                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\r\n                            // Re-assert the reserved row if the harness output just\r\n                            // destroyed our scroll region — alt-screen enter or a\r\n                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\r\n                            // margin is gone (alt-screen has its own full-screen\r\n                            // margins; a DECSTBM reset clears ours), so re-set the\r\n                            // MARGIN + repaint (assert_bytes), not just the text —\r\n                            // else the harness scrolls back over row 1. Scanned on the\r\n                            // SAME bytes we rendered, after writing them.\r\n                            if let Some(status) = status.as_deref_mut() {\r\n                                if reassert_scanner.feed(&bytes) {\r\n                                    stdout\r\n                                        .write_all(&status.assert_bytes())\r\n                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\r\n                                }\r\n                            }\r\n                            stdout.flush().map_err(|e| format!(\"flush: {e}\"))?;\r\n                        }\r\n                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\r\n                        AttachRecord::Displaced { by } => {\r\n                            return Ok(PumpEnd::Displaced(by));\r\n                        }\r\n                        // Viewer letterbox feed (REQ-RCVIEW-1): on a size change,\r\n                        // show a one-line indicator when our terminal differs from\r\n                        // the new PTY size (rc is a byte pump — true clip/pad needs\r\n                        // a grid model, deferred; the honest size cue renders here).\r\n                        AttachRecord::Size { rows, cols } if pty_size != Some((rows, cols)) => {\r\n                            pty_size = Some((rows, cols));\r\n                            match crossterm::terminal::size() {\r\n                                Ok((mycols, myrows)) if (myrows, mycols) != (rows, cols) => {\r\n                                    let _ = write!(\r\n                                        stdout,\r\n                                        \"\\r\\n[viewing {cols}x{rows} — your terminal is {mycols}x{myrows}]\\r\\n\"\r\n                                    );\r\n                                    let _ = stdout.flush();\r\n                                }\r\n                                _ => {}\r\n                            }\r\n                        }\r\n                        _ => {} // Request/Input echoed back: protocol noise, ignore.\r\n                    }\r\n                }\r\n            }\r\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\r\n                return Ok(serve_eof_end(rendered_any));\r\n            }\r\n            BrokerEvent::Error { message } => return Err(message),\r\n            _ => {} // acks + unrelated events: not ours.\r\n        }\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    fn parse(armed: &mut bool, input: &[u8]) -> (Vec<u8>, bool) {\r\n        let p = parse_stdin_chunk(armed, input);\r\n        (p.forward, p.detach)\r\n    }\r\n\r\n    // [unit->REQ-RC-WIN-VT-OUTPUT] the rc attach path composes the OUTPUT console\r\n    // mode that renders ANSI escapes instead of printing them literally (#12): it\r\n    // adds VT + processed-output, preserves the prior bits, and is idempotent.\r\n    #[cfg(windows)]\r\n    #[test]\r\n    fn with_vt_output_enables_virtual_terminal_processing() {\r\n        use windows_sys::Win32::System::Console::{\r\n            ENABLE_PROCESSED_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WRAP_AT_EOL_OUTPUT,\r\n        };\r\n        let prior = ENABLE_WRAP_AT_EOL_OUTPUT;\r\n        let got = with_vt_output(prior);\r\n        assert!(\r\n            got & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0,\r\n            \"VT-output processing enabled\"\r\n        );\r\n        assert!(got & ENABLE_PROCESSED_OUTPUT != 0, \"processed output enabled\");\r\n        assert!(\r\n            got & ENABLE_WRAP_AT_EOL_OUTPUT != 0,\r\n            \"prior console bits preserved\"\r\n        );\n…\n    }\n…\n}","startLine":1812,"lineNumbers":[1812,null,1822,null,1871,null,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,null,2031,null,3213]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":3214,"totalBytes":4984,"outputLines":92,"outputBytes":4984,"shownRange":{"start":1937,"end":2028},"nextOffset":2029}}},"isError":false,"timestamp":1784195780463}}
{"type":"message","id":"c7827dc9","parentId":"f9b0201b","timestamp":"2026-07-16T09:56:20.485Z","message":{"role":"toolResult","toolCallId":"call_PHakVn3VbdijB4dFCPDEREIs|fc_008390f508d1d09b016a58aac456e0819ab269996c92314077","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/tests/\n## attach.rs\n 140|/// origin), drive `marker` through the PTY and render it back over the wire,\n*141|/// then detach and confirm the serve loop ended `Detached` (session outlives).\n 142|/// Returns the rendered viewport bytes + the (still-live) target brain so the\n 143|/// caller can assert session survival and clean up.\n 144|#[allow(clippy::too_many_arguments)]\n*145|fn attach_drive_detach(\n 146|    operator: &mut Brain,\n 147|    target_broker: &str,\n 148|    mut target: Brain,\n...\n 191|\n*192|    // Detach: the viewport ends with the session still running.\n 193|    operator\n 194|        .net_stream_send(stream_b, &[], None, true)\n*195|        .expect(\"detach\");\n 196|    let (outcome, target) = server.join().expect(\"serve thread\");\n*197|    assert_eq!(outcome, AttachServeOutcome::Detached);\n 198|    (viewport, target)\n 199|}\n 200|\n...\n 295|// endpoint\" over the byte-identical cross-node attach pump; the session outlives\n*296|// the viewport (detach never kills it).\n 297|#[test]\n 298|fn spt_hosted_bringup_then_cross_node_attach_drives_the_pty() {\n 299|    init_home();\n...\n 360|\n*361|    // Detach: the brought-up session outlives the viewport.\n 362|    operator\n 363|        .net_stream_send(stream_b, &[], None, true)\n*364|        .expect(\"detach\");\n 365|    let (outcome, mut target) = server.join().expect(\"serve thread\");\n*366|    assert_eq!(outcome, AttachServeOutcome::Detached);\n 367|    target.kill_session().expect(\"cleanup kill\");\n 368|}\n 369|\n...\n 373|// its own marker echoed remotely. The serve gate sees the handshake-proven\n*374|// origin; B detaching ends the viewport with the session still running on A.\n 375|#[test]\n 376|fn remote_attach_drives_a_real_pty_cross_daemon() {\n 377|    init_home();\n...\n 389|\n*390|    // Operator B: dial A (CROSS-NODE QUIC conn), then drive + detach over the\n 391|    // SHARED attach pump. The serve gate sees the QUIC handshake identity.\n 392|    let mut operator = connect_retry(&name_b);\n 393|    let b_node = operator\n...\n 398|    let conn = operator.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\n*399|    let (_viewport, mut target) = attach_drive_detach(\n 400|        &mut operator,\n 401|        &name_a,\n 402|        target,\n...\n 423|// `remote_attach_drives_a_real_pty_cross_daemon` call the identical\n*424|// `attach_drive_detach` helper; they differ only in how `conn_id` is acquired\n 425|// (`net_dial_loopback` vs `net_dial`) and in the expected origin. The loopback\n 426|// origin is THIS node's own id, gating same-node (access short-circuit Allow in\n 427|// production; default-open in this hermetic home — both Allow). The session\n...\n 449|    let own_origin = dialed.remote_id_hex.clone();\n*450|    let (_viewport, mut target) = attach_drive_detach(\n 451|        &mut operator,\n 452|        &name,\n 453|        target,\n...\n 573|        .net_stream_send(stream_b, &[], None, true)\n*574|        .expect(\"detach\");\n 575|    let _ = server.join().expect(\"serve thread\");\n 576|}\n 577|\n...\n 675|        .net_stream_send(stream_b, &[], None, true)\n*676|        .expect(\"detach\");\n 677|    let _ = server.join().expect(\"serve thread\");\n 678|\n 679|    // The raw wire carries the alt viewport but NOT the main scrollback…\n...\n 869|\n*870|    // B renders through the dead-window marker, then detaches.\n 871|    let mut op_decoder = AttachDecoder::new();\n 872|    let (mut cursor, mut viewport) = (0u64, Vec::new());\n 873|    render_until(\n...\n 882|        .net_stream_send(stream_b, &[], None, true)\n*883|        .expect(\"detach\");\n 884|    let (outcome, mut life2) = server.join().expect(\"serve thread\");\n*885|    assert_eq!(outcome, AttachServeOutcome::Detached);\n 886|\n 887|    // Exactly-once, stated against the SCREEN MODEL (#6 / ADR-0031). Attach no\n 888|    // longer replays the raw ring — the broker synthesizes a clean current-screen\n...\n 1008|\n*1009|    // Detach: the viewport ends; the endpoint reads locally driven again.\n 1010|    operator\n 1011|        .net_stream_send(stream_b, &[], None, true)\n## brain_decouple.rs\n*1|//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\n 2|//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\n 3|//!\n*4|//! ## The bug this gate locks down (the update-wedge root)\n 5|//!\n 6|//! A controller's writer thread does a BLOCKING socket write to its brain\n*7|//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\n*8|//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\n 9|//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\n 10|//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\n 11|//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\n*12|//! evicted by neither: control latched, reattach was refused, `rc --take` hung,\n 13|//! `daemon status` still read healthy.\n 14|//!\n*15|//! ## Faithful in-process analog of a suspended brain\n 16|//!\n*17|//! A test process cannot `NtSuspendProcess` itself, so the rig reproduces the exact\n 18|//! MECHANISM: controller A stops reading its socket. The flood child fills A's recv\n 19|//! buffer, the controller channel fills, and A's writer thread parks INSIDE\n*20|//! `write_frame` — precisely the state a suspended brain leaves its broker conn in\n 21|//! (a peer that stopped draining). This is the same analog the 7.12 wedge gate uses.\n 22|//! `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15 s production deadline so the\n 23|//! structural outcome is provable in ~1 s (the deadline knob grants wall-clock only;\n...\n 40|//! journal replay dedup, the `delivered_through` no-rewind CAS `advanced_cursor`, and\n*41|//! the clean-repaint cold-attach batch). A live `NtSuspendProcess` / `SIGSTOP` run on a\n 42|//! real brain child remains the MANUAL field rig from the incident night — this CI test\n 43|//! is the deterministic in-process ANALOG, not that run.\n 44|//!\n...\n 62|    SpawnReq, Spawned, StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_SPAWN,\n*63|    KIND_SPAWNED, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n 64|};\n 65|use spt_daemon::transport::{send_hello, LocalSocketTransport};\n 66|use spt_daemon::Broker;\n...\n 178|                    // Keep the conn briefly so a Controller take is not immediately\n*179|                    // detached (drop would clear the slot) — then let it fall.\n 180|                    thread::sleep(Duration::from_millis(200));\n 181|                    let _ = tx.send(Some(reply.outcome));\n 182|                    return;\n...\n 195|/// Query the broker's stall-evict tally.\n*196|fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 197|    let name = name.to_string();\n 198|    let (tx, rx) = std::sync::mpsc::channel();\n 199|    thread::spawn(move || {\n 200|        let mut c = connect(&name);\n*201|        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n 202|        loop {\n 203|            match read_frame(&mut c) {\n*204|                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n 205|                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n 206|                    let _ = tx.send(r.count);\n 207|                    return;\n...\n 221|#[test]\n*222|fn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\n 223|    init_home();\n 224|    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 225|    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\n...\n 252|    // ── Remote controller R: a REMOTE operator (by=Some) takes control of the\n*253|    //    (locally-spawned, undriven) session, then STOPS reading — the suspended-\n 254|    //    brain analog. R MUST be a remote controller: a second remote's Control\n 255|    //    subscribe against a DIFFERENT-remote-driven session busy-refuses PRE-W2\n 256|    //    (the wedge), so only the stall-evict can free the slot — this is what\n...\n 259|    //    read from) on its own thread so the broker's writer for R parks in\n*260|    //    write_frame exactly as against a suspended brain. ──\n 261|    let rname = name.clone();\n 262|    thread::spawn(move || {\n 263|        let mut r = connect(&rname);\n...\n 335|\n*336|    let evicts = stall_evict_count(&name, Duration::from_secs(5));\n 337|\n 338|    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\n 339|    if let Some(pid) = broker.session_pid(sid) {\n...\n 344|        \"=== W2 GATE: viewer_ticks_during_wedge={ticks_during_wedge} take_outcome={outcome:?} \\\n*345|         take_elapsed={took:?} stall_evicts={evicts} ===\"\n 346|    );\n 347|\n 348|    // The viewer kept ticking while the controller conn was wedged.\n...\n 377|// ─────────────────────────────────────────────────────────────────────────────\n*378|// UPDATE-WEDGE round-4 — PHYSICAL writer/connection retirement gate.\n 379|//\n 380|// The companion test above proves LOGICAL isolation: a wedged controller is\n 381|// stall-evicted (the ROLE is freed), the viewer keeps ticking, a fresh take\n## brain_resume_conn_deadlock.rs\n*1|//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\n*2|//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\n 3|//!\n*4|//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n 5|//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n 6|//! onto the brain's OWN request/reply conn. A conn's send half is one\n 7|//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n...\n 30|//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\n*31|//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\n 32|\n 33|use std::sync::atomic::{AtomicU32, Ordering};\n 34|use std::sync::{Arc, OnceLock};\n...\n 235|\n*236|/// RESPAWN-interleave severity — the SHIPPING path under the update-restart burst: a\n 237|/// fresh brain generation resumes CURSOR-ONLY while N sessions already flood at full\n 238|/// rate (the sessions predate this generation). The new generation's heartbeat — hence\n 239|/// its readiness/promotion — is never starved.\n...\n 259|/// proof was invalidated — it failed REAL on kitsubito Linux CI (\"worst completed 10\").\n*260|/// doyle ruling `docs/UPDATE-WEDGE-R4-STALE-GUARD-RULING.md` (2026-07-10). The two\n 261|/// cursor-only heartbeat tests above remain the POSITIVE liveness proof; THIS is the\n 262|/// deterministic, timing-independent STRUCTURAL proof that the shipping resume attaches\n 263|/// ZERO subscribers.\n## brain_swap.rs\n*1|//! C0 E2E — the M3c keystone: a **brain-only self-update** swaps the brain logic\n*2|//! through the update engine with **zero hosted-endpoint interruption**\n 3|//! (REQ-UPD-3, absolute for the brain-only class — ADR-0004 §A).\n 4|//!\n 5|//! This rides the same broker/brain substrate the B2 handoff test proves, but\n*6|//! drives the swap through the *update engine* (`plan_update` → `apply_brain_only`)\n 7|//! rather than calling `Brain::handoff` directly — the engine *triggers* a\n 8|//! handoff, it does not re-implement one (`M3c-PLAN.md` §C0, clean-room posture).\n 9|//!\n...\n 11|//! brain injects a marker it never reads, then the engine applies a brain-only\n*12|//! update: the new brain re-attaches via the supplied relaunch closure (which\n 13|//! stands for \"exec the verified new binary → `Brain::handoff`\") and must find\n 14|//! that marker in the *replayed* buffer — proving the broker buffered output\n 15|//! produced while no brain was attached. Throughout, the child's pid is unchanged\n...\n 17|//!\n*18|//! **Filename note:** this file is `brain_swap.rs`, NOT `update.rs`, on purpose.\n 19|//! Windows' installer-detection heuristic (UAC) forces elevation for any\n*20|//! executable whose name contains `update`/`setup`/`install`/`patch`; a test\n*21|//! binary `update-<hash>.exe` then fails to launch with `os error 740` (requires\n 22|//! elevation). Do not rename this to match the module under test.\n 23|//!\n 24|//! **REQ-UPD-3 `int` re-pointed (restoration D7-1).** This test proves the\n...\n 27|//! NOT process-level survival. The REQ-UPD-3 `int` evidence (no endpoint drops\n*28|//! across a brain-only update) now lives in `crates/spt/tests/brain_survive.rs`,\n 29|//! which swaps a real on-disk binary and respawns the real `spt daemon brain`\n 30|//! child process (ADR-0018 V5). This file keeps exercising the engine path; it is\n 31|//! no longer the REQ-UPD-3 int tag.\n...\n 39|use spt_daemon::msg::SpawnReq;\n*40|use spt_daemon::update::{apply_brain_only, plan_update, BrokerAbi, ReleaseSpec, UpdateClass};\n 41|use spt_daemon::Broker;\n 42|\n 43|static SEQ: AtomicU32 = AtomicU32::new(0);\n...\n 94|#[test]\n*95|fn brain_only_update_swaps_logic_with_zero_endpoint_interruption() {\n 96|    let name = unique_name();\n 97|    let broker = Broker::bind(&name).expect(\"bind broker\");\n 98|\n...\n 122|    };\n*123|    let plan = plan_update(&running, release);\n 124|    assert_eq!(\n 125|        plan.class,\n*126|        UpdateClass::BrainOnly,\n 127|        \"a pure-logic release is brain-only\"\n 128|    );\n 129|\n...\n 133|    brain\n*134|        .send_input(b\"UPDATED_MARKER_DONE\\r\")\n 135|        .expect(\"inject marker for successor\");\n 136|\n*137|    // Apply the brain-only update through the engine. This is the swap: snapshot\n 138|    // → drop the outgoing brain (the crash boundary the broker tolerates) →\n 139|    // relaunch the new brain over the substrate. The engine touches no endpoint.\n 140|    now += 1; // wall clock advances so the new generation gets a fresh gen_start\n 141|    let mut brain = apply_brain_only(&plan, brain, |state| handoff_relaunch(&name, state, now))\n*142|        .expect(\"brain-only update must apply\");\n 143|\n 144|    // ── REQ-UPD-3: the hosted endpoint never noticed the swap. ──\n 145|    assert_eq!(\n...\n 147|        1,\n*148|        \"exactly one session across the update\"\n 149|    );\n 150|    assert_eq!(\n 151|        broker.session_pid(sid),\n...\n 164|\n*165|    // Gapless resume: the marker the pre-update brain injected (and never read)\n 166|    // arrives via the broker's replayed buffer — output produced across the swap\n 167|    // window was not lost.\n 168|    let mut buf = Vec::new();\n 169|    brain\n*170|        .read_output_until(b\"UPDATED_MARKER_DONE\", &mut buf)\n*171|        .expect(\"the swapped-in brain must replay output gaplessly across the update\");\n 172|\n*173|    // The post-update brain still fully drives the same endpoint: kill it and see\n 174|    // the exit event + reaping — the session was live and owned, not a husk.\n 175|    brain.kill_session().expect(\"kill session\");\n 176|    loop {\n## budget.rs\n 12|//! A **dormant** seat is a live, state-preserved harness session held warm\n*13|//! by the broker (PTY master + child process + output ring). A **suspended**\n 14|//! seat is that session closed — only the on-disk perch record remains. So:\n 15|//!\n 16|//! 1. spawn N idle sessions through the real broker (the production PTY\n...\n 21|//!    per-seat overhead: PTY plumbing + output rings);\n*22|//! 4. kill the sessions (the suspended state) and sample the residual.\n 23|//!\n 24|//! The per-seat child cost + broker delta IS the warm premium over\n*25|//! suspended; the suspended residual is ~the daemon baseline.\n 26|//!\n 27|//! ## Knobs (env)\n 28|//!\n...\n 153|\n*154|    // Suspend: close every seat (the session ends; only the perch record\n 155|    // would remain in production). The residual is the daemon baseline.\n 156|    for (brain, _, _) in seats.iter_mut() {\n 157|        let _ = brain.kill_session();\n...\n 161|    println!(\n*162|        \"suspended residual (this process RSS after killing all seats): {} (was {} warm)\",\n 163|        fmt_bytes(self_after.rss_bytes),\n 164|        fmt_bytes(self_warm.rss_bytes)\n 165|    );\n## conn_blackhole_lifecycle.rs\n*1|//! MSG-IDENTITY W6 / F-039 leg (e) — the DETERMINISTIC BLACK-HOLED-CONTROLLER\n 2|//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker\n 3|//! hosting REAL PTY children over the real local IPC.\n 4|//!\n...\n 9|//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the\n*10|//! EXACT Windows named-pipe black-hole (a controller that stops draining while a\n 11|//! finite burst floods its socket, so the broker's writer parks inside\n 12|//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` — no supported\n 13|//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/\n...\n 17|//!   1. UNRELATED SESSIONS CONTINUE — a second, independent session's viewer keeps\n*18|//!      receiving output throughout the black-hole incident (the fan-out never\n 19|//!      blocks the whole broker on the one wedged conn);\n 20|//!   2. THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE BOUND — a write\n 21|//!      on the retired old-client connection FAILS, and the conn's `handle_conn`\n...\n 25|//!      unrelated-viewer floor (<= 1; the persistent invariant-1 viewer holds one\n*26|//!      handler) WITHOUT the black-holed client ever dropping its end (the parked\n 27|//!      writer was aborted + the reader EOF-cleaned, so the handler thread ran to\n 28|//!      completion), and the viewer flows output AFTER the sample latches — proving\n 29|//!      the survivor is the viewer's handler, not a leaked one;\n...\n 45|//! This harness GENERALIZES `brain_decouple.rs`'s r4 gate\n*46|//! (`non_draining_controller_stall_evict_releases_writer_and_connection`, the\n 47|//! Windows-mandatory physical-retirement leg for\n 48|//! REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, which stays the primary\n 49|//! gate for that hazard). It reuses the same SEED/trigger/burst choreography,\n 50|//! the hand-back-the-unread-Stream pattern, the `conn_handler_count` and\n*51|//! `stall_evict_count` seams, the old-client-write-fails probe, and the\n 52|//! teardown-before-assert discipline. It ADDS the two pieces the r4 gate does\n 53|//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n 54|//! UNRELATED session whose viewer keeps ticking across the incident, and a fresh\n...\n 59|//! Invariants 2, 3, and the task-ownership proxy of 5 are the PHYSICAL-retirement\n*60|//! asserts, and the deterministic black-hole only reproduces on the Windows\n 61|//! named-pipe path (`interprocess 2.4.2` → `WriteFileEx`+`SleepEx(INFINITE)`,\n 62|//! `set_timeout` → `Unsupported`) — the exact field failure. On Unix the\n 63|//! poison/retire path is a `shutdown(SHUT_RDWR)` abort of a buffer-fill block: a\n...\n 91|    StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_INPUT, KIND_OUTPUT,\n*92|    KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN, KIND_SPAWNED, KIND_STALL_EVICTS,\n*93|    KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n 94|};\n 95|use spt_daemon::transport::{send_hello, LocalSocketTransport};\n 96|use spt_daemon::Broker;\n...\n 136|/// session so its viewer has a continuous frame stream to keep ticking on\n*137|/// throughout the black-hole incident (invariant 1).\n 138|fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139|    SpawnReq {\n 140|        program: \"cmd\".to_string(),\n...\n 159|/// the burst floods a non-draining controller's socket so its writer parks INSIDE\n*160|/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n 161|/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162|fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n 163|    SpawnReq {\n...\n 197|/// surfaces as a timeout, not a hang). Verbatim sibling of the r4 gate helper.\n*198|fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 199|    let name = name.to_string();\n 200|    let (tx, rx) = std::sync::mpsc::channel();\n 201|    thread::spawn(move || {\n 202|        let mut c = connect(&name);\n*203|        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n 204|        loop {\n 205|            match read_frame(&mut c) {\n*206|                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n 207|                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n 208|                    let _ = tx.send(r.count);\n 209|                    return;\n...\n 222|/// One `KIND_SESSIONS` round-trip, returning the reply. The dispatch runs the\n*223|/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n 224|/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225|/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226|fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n...\n 291|\n*292|    // ── BLACK-HOLED session B: the real SEED+burst PTY child from conn A, then\n*293|    //    DROP A so the ONLY persistent brain conn is the black-holed controller R. ──\n 294|    let (a, sid_b) = spawn_session(&name, seed_burst_spawn_req(\"blackhole-victim-ep\"));\n 295|    drop(a);\n 296|\n...\n 302|    //    draining, so the broker's R-writer parks in `write_frame` exactly as\n*303|    //    against a suspended brain. R's Stream is NEVER dropped until the very end. ──\n 304|    let r_name = name.clone();\n 305|    let (seed_tx, seed_rx) = std::sync::mpsc::channel::<(bool, u64)>();\n 306|    let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();\n...\n 371|        // Drain a bounded handful of burst frames so a REAL seq>0 controller frame\n*372|        // is proven delivered, then STOP reading forever (the black-hole). The\n 373|        // remaining ~1300 frames flood the now-unread socket → R-writer wedges.\n 374|        let post_deadline = Instant::now() + Duration::from_secs(5);\n 375|        let mut post = 0u32;\n...\n 386|        }\n*387|        // Hand the Stream back to main UNREAD (main holds it = the black-holed\n 388|        // client). The broker's R-writer stays wedged on the remaining burst.\n 389|        let _ = hand_tx.send((r, max_seq));\n 390|    });\n## control_stamp_lifetime.rs\n 9|//!\n*10|//! `/exit` ends the RC session INDIRECTLY: the endpoint suspends when the child\n 11|//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n*12|//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n 13|//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n 14|//! path dropped the OutputLog with the controller slot still occupied, so the\n 15|//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\n...\n 183|    // ── Kill the PTY CHILD → the exit-waiter reaps the session (NOT a conn drop, so\n*184|    //    the controller-detach path never runs; only the reap path can unlatch). ──\n 185|    let pid = broker\n 186|        .session_pid(sid)\n 187|        .expect(\"live session must have a pid before kill\");\n## daemon_lifecycle_real_brain.rs\n 16|//! no longer waits for a resident `<id>-psyche` perch to bind (host_one spawns none)\n*17|//! and reaps no detached child. The DAEMON-1 evidence is the hosted pulse loop\n 18|//! ingesting the commune — unchanged and independent of the retired resident spawn.\n 19|//!\n 20|//! Single `#[test]`: it owns the process-global `SPT_HOME`.\n...\n 113|    // No resident psyche to reap (F-030 W1 — host_one spawns none). The run_brain +\n*114|    // serve threads are detached and die with the test process.\n 115|}\n## dispatch.rs\n 22|use spt_daemon::nethost::{NetConfig, NetHost};\n*23|use spt_daemon::propagate::{request_update, UpdatePullOutcome};\n 24|use spt_daemon::relcache::ReleaseCache;\n 25|use spt_daemon::release::{sha256_hex, ReleaseMetadata, SignedRelease, VerifyPolicy};\n 26|use spt_daemon::sync::request_sync;\n*27|use spt_daemon::update::{BrokerAbi, BROKER_RESOURCE_ABI};\n 28|use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\n 29|use spt_net::net::notif::NotifRecord;\n 30|use spt_net::net::wanmsg::WanMessage;\n...\n 123|    // Mesh-D6: membership is the roster — seed the canonical roster (the inbound\n*124|    // gates and the update serve gate both read it).\n 125|    let mut roster = spt_store::roster::RosterStore::load();\n 126|    roster.merge_entry(spt_store::roster::RosterEntry {\n 127|        pubkey_hex: peer_hex.to_string(),\n...\n 207|\n*208|// [int->REQ-UPD-1] an update pull is served end to end by the dispatcher:\n 209|// B stages a release, A queries/fetches/verifies/stages — no hand-wired\n*210|// serve_update. The serve gate ran from B's canonical roster.\n 211|#[test]\n*212|fn dispatcher_serves_an_update_pull_undriven() {\n 213|    init_home();\n 214|    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n 215|    let dir = TempDir::new().unwrap();\n...\n 262|    let cache_a = ReleaseCache::open(&dir.path().join(\"releases-a\"));\n*263|    let outcome = request_update(\n 264|        &mut a,\n 265|        conn.conn_id,\n 266|        MintedOp::new(Minter::Pump, op()),\n...\n 274|    assert!(\n*275|        matches!(outcome, UpdatePullOutcome::Updated(_)),\n*276|        \"the dispatcher served the update undriven: {outcome:?}\"\n 277|    );\n 278|    assert_eq!(\n 279|        cache_a.load_artifact().expect(\"staged bytes\"),\n...\n 615|    );\n*616|    // Detach the viewport cleanly before the refuse probe.\n 617|    let _ = a.net_stream_send(viewport, &[], None, true);\n 618|\n 619|    // A STALE endpoint (no live session on B) refuses: clean finish, no output.\n...\n 970|// own AttachmentStore + SubnetStore. An attached member answers serving=true;\n*971|// a subnet B never joined answers false; B `detach`ing the subnet flips the\n 972|// answer to false WHILE B STAYS REACHABLE on the same connection (the\n 973|// criterion-5 fix — a raw dial proved only \"the box is up\" and kept reading\n*974|// a detached peer online); re-attach restores serving.\n 975|#[test]\n 976|fn dispatcher_serves_a_subnet_serve_probe() {\n 977|    use spt_daemon::request_subnet_probe;\n...\n 995|    let mut att = AttachmentStore::load();\n*996|    att.set_detached(\"ProbeNet\", false); // clean any prior detach in shared home\n 997|    att.save().expect(\"save attachment\");\n 998|\n 999|    let mut a = connect_retry(&a_name);\n...\n 1016|\n*1017|    // Detach it → the SAME reachable box now answers not-serving.\n 1018|    let mut att = AttachmentStore::load();\n*1019|    att.set_detached(\"ProbeNet\", true);\n 1020|    att.save().expect(\"save attachment\");\n 1021|    assert!(\n*1022|        !request_subnet_probe(&mut a, conn.conn_id, \"ProbeNet\").expect(\"probe detached\"),\n*1023|        \"a detached member stops serving while still reachable (criterion-5 fix)\"\n 1024|    );\n 1025|\n 1026|    // Re-attach → serving again (also restores the shared SPT_HOME state).\n 1027|    let mut att = AttachmentStore::load();\n*1028|    att.set_detached(\"ProbeNet\", false);\n 1029|    att.save().expect(\"save attachment\");\n 1030|    assert!(\n*1031|        request_subnet_probe(&mut a, conn.conn_id, \"ProbeNet\").expect(\"probe reattached\"),\n 1032|        \"re-attach restores serving\"\n 1033|    );\n 1034|\n...\n 1038|// [unit->REQ-SUBNET-3] the demux routes a node-label feed: an endpoint-less\n*1039|// peer sends a NodeLabelUpdate first (no instance row), so the feed's FIRST\n 1040|// line is `{subnet,node,label,epoch}` — it must classify as the Registry\n 1041|// family (same serve arm + gate) rather than dropping as Unknown. An instance\n 1042|// row still classifies Registry; a bare unrelated object stays Unknown.\n## docs_server_e2e.rs\n 5|//! rejected; a docs-less home answers the loud not-found WITH the\n*6|//! `spt update fetch --apply` pointer while the server stays bound; writes\n 7|//! are refused (GET/HEAD only).\n 8|//!\n 9|//! Hermetic: port 0 (ephemeral), raw std TcpStream client — no HTTP client\n...\n 114|    assert!(\n*115|        text.contains(\"spt update fetch --apply\"),\n 116|        \"the pointer at the landing command: {text}\"\n 117|    );\n 118|    // Still bound and answering after the docs-less response.\n## driven_by_selfheal.rs\n 6|//! An spt-hosted endpoint can latch `ONLINE+CONTROLLED` — `info.json`\n*7|//! `driven_by = Some(node)` — and never clear it when a controller's detach is\n 8|//! LOST (no clean disconnect, and no output to trigger the W1 drain-evict\n 9|//! `mark_controller_gone`). The picker then renders a phantom \"controlled by X\"\n 10|//! forever. `reconcile_hosted_liveness` (`livehost.rs`) is the pull-primary\n...\n 35|//!   an idle session whose ABANDONED REMOTE controller wedged without a clean\n*36|//!   detach — keeps `controller_by==Some(origin)`, which a reconcile read can't\n 37|//!   detect at all. That case is its own deferred requirement,\n 38|//!   `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT` (broker-side controller eviction —\n 39|//!   NOT a reconcile `controller_by` clear). See `inject_control_wedge.rs` `w5_a2`\n...\n 52|//! It also captures the broker's OWN self-heal on an OBSERVED controller\n*53|//! disconnect (per-conn `detach_if`→`clear_controller`→`stamp_driven_by`).\n 54|//!\n 55|//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n 56|//!\n...\n 156|/// spt-hosted (`controllable=Some(true)`) `live_agent` self-perch, `status=online`,\n*157|/// pre-stamped `driven_by=Some(node)` (the lost-detach latch). Mirrors the\n 158|/// livehost-bootgate-e2e stamping exactly, plus the `driven_by` marker.\n 159|fn stamp_latched_perch(id: &str, node: &str) -> std::path::PathBuf {\n 160|    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n...\n 166|    spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_ONLINE).unwrap();\n*167|    // The lost-detach latch the self-heal must clear.\n 168|    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\n 169|    self_perch\n 170|}\n...\n 281|    // FINDING (the broker's OWN self-heal on a controller disconnect): dropping the\n*282|    // spawner socket makes the broker's per-conn handler run `detach_if` →\n 283|    // `clear_controller` → `stamp_driven_by`, which (since controller_by() is now\n 284|    // None) RE-STAMPS the perch's driven_by to None. So an OBSERVED controller\n 285|    // disconnect already unlatches via the broker single-writer — even without a\n*286|    // clean detach. The Gap-A the W5 RECONCILE must heal is the path where that\n 287|    // re-stamp NEVER fires: a brain restart re-adopts the live session but the\n 288|    // in-memory controller (and thus the clear_controller re-stamp) never existed,\n 289|    // so the STALE on-disk driven_by marker persists against a controller_by==None\n...\n 300|         driven_by_after_spawner_drop={driven_by_after_disconnect:?} \\\n*301|         (None = the broker's detach_if→clear_controller→stamp_driven_by already \\\n 302|         unlatched the OBSERVED disconnect — so the RECONCILE Gap-A is only the \\\n 303|         NEVER-RE-STAMPED brain-restart leftover, reproduced next) ===\"\n 304|    );\n## endpoint_survival.rs\n*1|//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) — daemon-restart endpoint\n 2|//! survival, against a REAL broker over the real local IPC.\n 3|//!\n 4|//! ## The bug this gate locks down (seed #6, the field-accept blocker)\n 5|//!\n*6|//! A `daemon stop` + `start` (the `update apply` notice's OWN instruction) kills every\n 7|//! daemon-hosted harness; pre-W3 they stayed OFFLINE after start (no resurrection)\n 8|//! though their records survived (info.json status + adapter + cwd + a ledger row). The\n 9|//! fix: `livehost::resume_restart_orphaned_endpoints` runs ONCE at the fresh broker's\n...\n 84|\n*85|// [int->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n 86|#[test]\n 87|fn daemon_restart_reruns_a_previously_online_orphaned_endpoint() {\n 88|    let home = tempfile::TempDir::new().expect(\"temp home\");\n## false_promote.rs\n*1|//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-PROMOTE-DRAINED) — the FALSE-PROMOTE int rig, on a\n 2|//! REAL broker hosting a REAL PTY child over the real local IPC. Exercises the\n 3|//! PROMOTION PATH ITSELF (`brainproc::supervise_brain` → `run_trial`), the gate\n 4|//! condition doyle's W2 verdict (@e5ae7a9) made binding for W3.\n...\n 10|//! can signal `brain.ready` INSIDE that window while the OLD generation's local\n*11|//! controller conn is still wedged (blocked writes pending), so `update apply`\n 12|//! \"promotes\" onto a still-frozen control plane (the incident-night 22:47 apply). The\n 13|//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\n*14|//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\n 15|//! read from BROKER TRUTH in-process (no brain round-trip).\n 16|//!\n 17|//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\n...\n 19|//! Pre-round-4 this rig manufactured its wedge with a REAL writer parked inside\n*20|//! `write_frame` on a black-holed spawner conn. Round-4\n 21|//! (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK) gives every physical conn\n 22|//! write an independent watchdog that CANCELS + retires a wedged write at the SAME\n 23|//! `brain_write_deadline()` bound — so a real parked writer now self-heals at the\n...\n 25|//! fix working, not a regression; the 22:47 permanent-frozen-plane class is dead by\n*26|//! construction). Ruling `docs/UPDATE-WEDGE-R4-FALSE-PROMOTE-RULING.md`: this rig\n 27|//! tests the GATE DECISION in isolation, so the wedge is now INJECTED statically —\n 28|//! `Broker::test_seed_wedged_local_controller` installs a LOCAL controller\n 29|//! (`by = None`, the brain's own resume/host identity) whose `write_blocked_since`\n...\n 32|//! bound/watchdog itself is `brain_decouple.rs`'s concern. The DRAIN below still\n*33|//! rides the REAL logical path (the take's `stall_evict_controller`), unchanged.\n 34|//! `SPT_BRAIN_WRITE_DEADLINE_MS` still shrinks the 15 s deadline (the knob grants\n 35|//! wall-clock only; it never relaxes an assertion).\n 36|//!\n...\n 40|//! 1. the trial does NOT promote while wedged (RED-first: remove the\n*41|//!    `&& env.old_gen_drained()` gate in `run_trial` and it promotes on ready alone,\n 42|//!    reproducing the 22:47 false-promote);\n 43|//! 2. once the wedged old conn drains (here: a second operator's take stall-evicts it\n 44|//!    broker-side, the W2 self-heal), the candidate promotes — ready-seen was LATCHED\n...\n 157|/// stall-evicts the wedged local incumbent broker-side (the W2 self-heal). Holds the\n*158|/// conn briefly so the take is not immediately detached, then lets it fall.\n 159|fn remote_take(name: &str, sid: u64) {\n 160|    let name = name.to_string();\n 161|    thread::spawn(move || {\n...\n 229|    // The gate under test: REAL broker truth, no brain round-trip.\n*230|    fn old_gen_drained(&self) -> bool {\n 231|        !self.broker.any_local_controller_wedged()\n 232|    }\n 233|}\n 234|\n*235|// [int->REQ-UPDATE-PROMOTE-DRAINED]\n 236|#[test]\n 237|fn ready_candidate_does_not_promote_until_the_wedged_old_gen_conn_drains() {\n 238|    init_home();\n...\n 268|    // REPLACES A's spawn-attached controller sink; A itself can drop (its conn\n*269|    // detach no longer owns the controller slot).\n 270|    assert!(\n 271|        broker.test_seed_wedged_local_controller(sid, 1_000),\n 272|        \"precondition: the wedge seed must install on the hosted session\"\n## handoff.rs\n 83|\n*84|    // The broker accepts connections forever on a detached thread; it outlives\n 85|    // every brain (that is the whole point).\n 86|    let serve_broker = Arc::clone(&broker);\n 87|    thread::spawn(move || {\n## inject_control_wedge.rs\n 22|//! the writer advances `delivered_through`). The drain hands each chunk off with a\n*23|//! bounded `send_timeout` OFF the log lock; a fully-stalled controller is detached\n 24|//! after `CONTROLLER_WRITE_DEADLINE`. So even while the controller is fully backed\n 25|//! up, the session does NOT wedge — `KIND_SESSIONS` still answers. THIS test\n 26|//! asserts that fixed behavior.\n...\n 169|/// `sessions` Mutex — if a wedged per-conn handler is holding that lock the read\n*170|/// blocks and rides teardown to the 240s backstop. Probe it on a detached thread\n 171|/// (the broker is an `Arc`, so the clone keeps it alive) and give up on timeout;\n 172|/// a missed child-kill is harmless, the process-per-test exit reaps it.\n 173|fn session_pid_bounded(broker: &Arc<Broker>, sid: u64, dur: Duration) -> Option<u32> {\n...\n 775|        // Report the result IMMEDIATELY (the main thread's watchdog can proceed),\n*776|        // THEN best-effort detach + drain. We deliberately do NOT block the result\n 777|        // on the serve thread: a controller's `serve_attach` forward can block on a\n 778|        // non-draining operator socket while the flood child keeps producing — the\n 779|        // main thread kills the child to unblock it. Reporting first then draining\n...\n 783|        // Keep draining the operator so serve's forwards drain and serve_attach can\n*784|        // observe our detach + the child's eventual death and return cleanly.\n 785|        let drain_deadline = std::time::Instant::now() + Duration::from_secs(12);\n 786|        while std::time::Instant::now() < drain_deadline {\n 787|            match operator\n...\n 848|    // Each `cold_start_pump` Brain (the driver's `w`, the attacher's `operator`)\n*849|    // spawns a DETACHED `pump-ipc-reader` thread parked in a BLOCKING named-pipe\n 850|    // `read_frame` on its RecvHalf. Joining the helper threads drops those Brains,\n 851|    // which drops each Brain's SendHalf → the broker's per-conn handler read errors\n 852|    // → the broker closes its end → the client RecvHalf read finally errors → the\n...\n 2158|//     session is listed live, and `driven_by` (if pre-stamped) stays Some.\n*2159|//   - A2 (production wedged-pump / lost detach — the REQ's real target): a real\n 2160|//     remote controller ATTACHES (become_controller → driven_by=Some, slot\n 2161|//     occupied by Some(origin)), then its remote is gone but the broker-side\n*2162|//     controller sink stays OPEN (no clean EOF/detach) and the session is IDLE\n 2163|//     (no output → the W1 drain-evict / CONTROLLER_WRITE_DEADLINE never fires) →\n 2164|//     the slot stays Some(origin) → `controller_by` reads SOME, `driven_by` stays\n 2165|//     Some — the broker still THINKS it's controlled.\n...\n 2264|    //    operator's remote is then ABANDONED by dropping its brain WITHOUT a clean\n*2265|    //    EOF/detach, and the session is kept IDLE (a QUIET child, no output → the W1\n 2266|    //    output-drain-evict never fires). We read controller_by + driven_by and REPORT\n 2267|    //    whether the broker self-clears the abandoned slot (characterization-only). ──\n 2268|    //\n...\n 2342|        // Drive serve_attach until told to stop (it blocks on brain.read_event()).\n*2343|        // We DON'T cleanly detach — we just abandon it when the test ends. Running\n 2344|        // it here keeps the controller slot occupied by Some(origin).\n 2345|        let serve_broker = serve_name.clone();\n*2346|        let _ = serve_stop; // abandoned, not signalled (the \"lost detach\" shape)\n 2347|        let _ = serve_attach(&mut target, &serve_broker, stream_a, \"w5-a2-ep\", &origin, 0, None);\n 2348|    });\n 2349|\n...\n 2372|\n*2373|    // ── ABANDON the operator's remote WITHOUT a clean detach: drop the operator\n*2374|    //    brain WITHOUT sending the EOF detach record (net_stream_send(.., true)).\n 2375|    //    The session is idle (quiet child), so nothing evicts the controller slot. ──\n 2376|    drop(operator);\n 2377|\n...\n 2413|        // DID become the controller (slot = Some(origin)). The remote is then\n*2414|        // abandoned (drop, no clean EOF/detach), closing its loopback QUIC conn (FIN).\n 2415|        // Whether the broker clears the slot WITHIN the 2s read window is conn-close\n 2416|        // detection — and it is TIMING-VARIABLE on BOTH platforms: QUIC FIN-detection\n 2417|        // latency vs the read window is NON-DETERMINISTIC (unix loopback USUALLY but\n## input_ack_deadlock.rs\n 15|//! forever: a concurrent attach is never serviced, and the original controller's\n*16|//! own detach can never be processed (it stays latched).\n 17|//!\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...\n 263|/// viewer attach still OPENS + is serviced + receives the flood child's output, and\n*264|/// the flood controller's detach registers. Pre-fix (ack=true) the return direction\n 265|/// fills, the handler deadlocks, and the concurrent attach is never serviced.\n 266|///\n 267|/// Real broker + real PTY + real serve_attach, NO mocks. Every wait is behind a\n...\n 374|\n*375|        // Detach: close the send side (best-effort — the broker's draining handler\n 376|        // processes it and releases the controller seat). Then ABANDON the conn +\n 377|        // serve thread WITHOUT a blocking drain loop: a Whole conn's read ignores\n 378|        // any deadline, so a drain loop here would hang the helper. The main thread's\n...\n 517|    //    child (closes the PTY → unwinds the abandoned serve_attach threads + their\n*518|    //    target conns), then DETACH the helper handles (do NOT join — a serve_attach\n 519|    //    on a Whole `target` conn can park, and joining it would re-introduce the\n 520|    //    very hang this test guards against). ──\n 521|    if let Some(pid) = broker.session_pid(sid) {\n## legacy_resident_sweep_e2e.rs\n 124|    let child_exe = spt_store::proc::exe_basename(mock_pid).expect(\"mock exe basename\");\n*125|    // DETACH the child handles (they keep running — dropping a `Child` never kills). This\n 126|    // is load-bearing on Windows: a held process HANDLE keeps the process object queryable\n 127|    // (OpenProcess succeeds) even AFTER termination, so `is_process_alive` would lie true;\n 128|    // with no handle held, liveness is honest by pid. Cleanup reaps by pid below.\n## mesh.rs\n 120|/// The inert/registry-only cadence: only registry feeds flow (notif/sync/\n*121|/// update are pushed far enough out to never fire), so the shared canonical\n 122|/// notif/context stores stay untouched across nodes.\n 123|fn cadence() -> PumpCadence {\n 124|    PumpCadence {\n...\n 127|        sync: Duration::from_secs(3_000),\n*128|        update: Duration::from_secs(3_000),\n 129|        registry_evict_after: Duration::from_secs(3_000),\n 130|    }\n 131|}\n...\n 229|                    cadence: cadence(),\n*230|                    full_auto_update: false,\n 231|                };\n 232|                let _ = run_peer_pump(\n 233|                    &name,\n## net_worker_starve.rs\n*1|//! UPDATE-WEDGE round-3 DIAGNOSTIC rig (throwaway — NO REQ until the verdict\n*2|//! lands and doyle rules the shape; see docs/UPDATE-WEDGE-2-ROUND3-RULING.md).\n 3|//!\n 4|//! The decider the round-3 ruling §5 demands: does a burst of dead-peer\n 5|//! `submit_dial`s actually HOLD/serialize the broker net runtime's 2 worker\n...\n 10|//! STAGE 1 (this file, first): the narrow mechanism probe. Stand a dialer whose\n*11|//! connects black-hole a dead peer, fire a `submit_dial` burst straight onto the\n 12|//! broker's own [`NetHost`] (the exact pump path), and read the two round-3\n 13|//! DIAGNOSTIC probes added to `nethost.rs`:\n 14|//!   - `active_dial_tasks()` — in-flight dead-peer connects on the 2 workers;\n...\n 106|/// THE DECIDER (round-3 ruling §5): a burst of `submit_dial`s to a peer that\n*107|/// accepts the QUIC handshake but black-holes the seed-proof. Reports whether\n 108|/// the burst holds the 2 workers (canary goes stale) or the awaits yield (canary\n 109|/// stays fresh). This test does not FAIL on either outcome — it PRINTS the verdict\n 110|/// the ruling needs; an explicit assert nails the direction once observed.\n...\n 121|\n*122|    // B: the black hole — accepts the handshake, never runs the proof responder.\n 123|    let broker_b = served_broker(&name_b, hermetic(Identity::generate()), &dir.path().join(\"b\"));\n 124|    let host_b = broker_b.net().expect(\"host b\");\n 125|    let b_addr = host_b.addr();\n## netbroker.rs\n 53|/// is irrelevant; its PRESENCE is what makes the dialer open the stream and\n*54|/// block, giving the deterministic black hole the broker-side bound must cut.\n 55|fn proving(identity: Identity) -> NetConfig {\n 56|    let mut cfg = hermetic(identity);\n 57|    cfg.membership = Some(std::sync::Arc::new(|| {\n...\n 234|// pump-IPC-deadline B-half): a dial to a peer that ACCEPTS the connection but\n*235|// black-holes the seed-proof (a membership-mismatched/dead peer — the exact\n 236|// shape of the 2.2h hfenduleam wedge) must fail PROMPTLY with an ORDINARY broker\n 237|// error reply within the bound, NEVER an unbounded await. Exactly-once is\n 238|// preserved on timeout: the journaled dial records NO phantom conn — the journal\n...\n 260|\n*261|    // B: the black hole — accepts the QUIC handshake (`membership: None` ⇒ it\n 262|    // registers the conn) but never runs the proof responder, so A's\n 263|    // `prove_membership` awaits a control-stream reply that never comes.\n 264|    let _broker_b = net_broker(&name_b, Identity::generate(), &dir.path().join(\"b\"));\n...\n 273|        .net_dial(b_addr, Some(MintedOp::new(Minter::Cli, OP)))\n*274|        .expect_err(\"a black-holed dial must error, never block forever\");\n 275|    let elapsed = started.elapsed();\n 276|\n 277|    // The broker REPLIED an ordinary error within the bound — not a wedge, and\n\nShowing files 1-20 of 152+. Use skip=20 for the next page, or narrow paths/pattern.\n\nSkipped missing paths: scripts"}],"details":{"scopePath":"crates/spt-daemon/tests, crates/spt/tests, tests, docs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":171,"fileCount":20,"files":["crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/brain_decouple.rs","crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","crates/spt-daemon/tests/brain_swap.rs","crates/spt-daemon/tests/budget.rs","crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","crates/spt-daemon/tests/control_stamp_lifetime.rs","crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs","crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/tests/docs_server_e2e.rs","crates/spt-daemon/tests/driven_by_selfheal.rs","crates/spt-daemon/tests/endpoint_survival.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt-daemon/tests/handoff.rs","crates/spt-daemon/tests/inject_control_wedge.rs","crates/spt-daemon/tests/input_ack_deadlock.rs","crates/spt-daemon/tests/legacy_resident_sweep_e2e.rs","crates/spt-daemon/tests/mesh.rs","crates/spt-daemon/tests/net_worker_starve.rs","crates/spt-daemon/tests/netbroker.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/attach.rs","count":20},{"path":"crates/spt-daemon/tests/brain_decouple.rs","count":20},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","count":6},{"path":"crates/spt-daemon/tests/brain_swap.rs","count":20},{"path":"crates/spt-daemon/tests/budget.rs","count":5},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","count":20},{"path":"crates/spt-daemon/tests/control_stamp_lifetime.rs","count":3},{"path":"crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs","count":2},{"path":"crates/spt-daemon/tests/dispatch.rs","count":20},{"path":"crates/spt-daemon/tests/docs_server_e2e.rs","count":2},{"path":"crates/spt-daemon/tests/driven_by_selfheal.rs","count":8},{"path":"crates/spt-daemon/tests/endpoint_survival.rs","count":3},{"path":"crates/spt-daemon/tests/false_promote.rs","count":11},{"path":"crates/spt-daemon/tests/handoff.rs","count":1},{"path":"crates/spt-daemon/tests/inject_control_wedge.rs","count":13},{"path":"crates/spt-daemon/tests/input_ack_deadlock.rs","count":4},{"path":"crates/spt-daemon/tests/legacy_resident_sweep_e2e.rs","count":1},{"path":"crates/spt-daemon/tests/mesh.rs","count":3},{"path":"crates/spt-daemon/tests/net_worker_starve.rs","count":5},{"path":"crates/spt-daemon/tests/netbroker.rs","count":4}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/tests/\n## attach.rs\n  140│/// origin), drive `marker` through the PTY and render it back over the wire,\n *141│/// then detach and confirm the serve loop ended `Detached` (session outlives).\n  142│/// Returns the rendered viewport bytes + the (still-live) target brain so the\n  143│/// caller can assert session survival and clean up.\n  144│#[allow(clippy::too_many_arguments)]\n *145│fn attach_drive_detach(\n  146│    operator: &mut Brain,\n  147│    target_broker: &str,\n  148│    mut target: Brain,\n     │...\n  191│\n *192│    // Detach: the viewport ends with the session still running.\n  193│    operator\n  194│        .net_stream_send(stream_b, &[], None, true)\n *195│        .expect(\"detach\");\n  196│    let (outcome, target) = server.join().expect(\"serve thread\");\n *197│    assert_eq!(outcome, AttachServeOutcome::Detached);\n  198│    (viewport, target)\n  199│}\n  200│\n     │...\n  295│// endpoint\" over the byte-identical cross-node attach pump; the session outlives\n *296│// the viewport (detach never kills it).\n  297│#[test]\n  298│fn spt_hosted_bringup_then_cross_node_attach_drives_the_pty() {\n  299│    init_home();\n     │...\n  360│\n *361│    // Detach: the brought-up session outlives the viewport.\n  362│    operator\n  363│        .net_stream_send(stream_b, &[], None, true)\n *364│        .expect(\"detach\");\n  365│    let (outcome, mut target) = server.join().expect(\"serve thread\");\n *366│    assert_eq!(outcome, AttachServeOutcome::Detached);\n  367│    target.kill_session().expect(\"cleanup kill\");\n  368│}\n  369│\n     │...\n  373│// its own marker echoed remotely. The serve gate sees the handshake-proven\n *374│// origin; B detaching ends the viewport with the session still running on A.\n  375│#[test]\n  376│fn remote_attach_drives_a_real_pty_cross_daemon() {\n  377│    init_home();\n     │...\n  389│\n *390│    // Operator B: dial A (CROSS-NODE QUIC conn), then drive + detach over the\n  391│    // SHARED attach pump. The serve gate sees the QUIC handshake identity.\n  392│    let mut operator = connect_retry(&name_b);\n  393│    let b_node = operator\n     │...\n  398│    let conn = operator.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\n *399│    let (_viewport, mut target) = attach_drive_detach(\n  400│        &mut operator,\n  401│        &name_a,\n  402│        target,\n     │...\n  423│// `remote_attach_drives_a_real_pty_cross_daemon` call the identical\n *424│// `attach_drive_detach` helper; they differ only in how `conn_id` is acquired\n  425│// (`net_dial_loopback` vs `net_dial`) and in the expected origin. The loopback\n  426│// origin is THIS node's own id, gating same-node (access short-circuit Allow in\n  427│// production; default-open in this hermetic home — both Allow). The session\n     │...\n  449│    let own_origin = dialed.remote_id_hex.clone();\n *450│    let (_viewport, mut target) = attach_drive_detach(\n  451│        &mut operator,\n  452│        &name,\n  453│        target,\n     │...\n  573│        .net_stream_send(stream_b, &[], None, true)\n *574│        .expect(\"detach\");\n  575│    let _ = server.join().expect(\"serve thread\");\n  576│}\n  577│\n     │...\n  675│        .net_stream_send(stream_b, &[], None, true)\n *676│        .expect(\"detach\");\n  677│    let _ = server.join().expect(\"serve thread\");\n  678│\n  679│    // The raw wire carries the alt viewport but NOT the main scrollback…\n     │...\n  869│\n *870│    // B renders through the dead-window marker, then detaches.\n  871│    let mut op_decoder = AttachDecoder::new();\n  872│    let (mut cursor, mut viewport) = (0u64, Vec::new());\n  873│    render_until(\n     │...\n  882│        .net_stream_send(stream_b, &[], None, true)\n *883│        .expect(\"detach\");\n  884│    let (outcome, mut life2) = server.join().expect(\"serve thread\");\n *885│    assert_eq!(outcome, AttachServeOutcome::Detached);\n  886│\n  887│    // Exactly-once, stated against the SCREEN MODEL (#6 / ADR-0031). Attach no\n  888│    // longer replays the raw ring — the broker synthesizes a clean current-screen\n     │...\n 1008│\n*1009│    // Detach: the viewport ends; the endpoint reads locally driven again.\n 1010│    operator\n 1011│        .net_stream_send(stream_b, &[], None, true)\n## brain_decouple.rs\n  *1│//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\n   2│//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\n   3│//!\n  *4│//! ## The bug this gate locks down (the update-wedge root)\n   5│//!\n   6│//! A controller's writer thread does a BLOCKING socket write to its brain\n  *7│//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\n  *8│//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\n   9│//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\n  10│//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\n  11│//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\n *12│//! evicted by neither: control latched, reattach was refused, `rc --take` hung,\n  13│//! `daemon status` still read healthy.\n  14│//!\n *15│//! ## Faithful in-process analog of a suspended brain\n  16│//!\n *17│//! A test process cannot `NtSuspendProcess` itself, so the rig reproduces the exact\n  18│//! MECHANISM: controller A stops reading its socket. The flood child fills A's recv\n  19│//! buffer, the controller channel fills, and A's writer thread parks INSIDE\n *20│//! `write_frame` — precisely the state a suspended brain leaves its broker conn in\n  21│//! (a peer that stopped draining). This is the same analog the 7.12 wedge gate uses.\n  22│//! `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15 s production deadline so the\n  23│//! structural outcome is provable in ~1 s (the deadline knob grants wall-clock only;\n    │...\n  40│//! journal replay dedup, the `delivered_through` no-rewind CAS `advanced_cursor`, and\n *41│//! the clean-repaint cold-attach batch). A live `NtSuspendProcess` / `SIGSTOP` run on a\n  42│//! real brain child remains the MANUAL field rig from the incident night — this CI test\n  43│//! is the deterministic in-process ANALOG, not that run.\n  44│//!\n    │...\n  62│    SpawnReq, Spawned, StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_SPAWN,\n *63│    KIND_SPAWNED, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n  64│};\n  65│use spt_daemon::transport::{send_hello, LocalSocketTransport};\n  66│use spt_daemon::Broker;\n    │...\n 178│                    // Keep the conn briefly so a Controller take is not immediately\n*179│                    // detached (drop would clear the slot) — then let it fall.\n 180│                    thread::sleep(Duration::from_millis(200));\n 181│                    let _ = tx.send(Some(reply.outcome));\n 182│                    return;\n    │...\n 195│/// Query the broker's stall-evict tally.\n*196│fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 197│    let name = name.to_string();\n 198│    let (tx, rx) = std::sync::mpsc::channel();\n 199│    thread::spawn(move || {\n 200│        let mut c = connect(&name);\n*201│        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n 202│        loop {\n 203│            match read_frame(&mut c) {\n*204│                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n 205│                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n 206│                    let _ = tx.send(r.count);\n 207│                    return;\n    │...\n 221│#[test]\n*222│fn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\n 223│    init_home();\n 224│    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 225│    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\n    │...\n 252│    // ── Remote controller R: a REMOTE operator (by=Some) takes control of the\n*253│    //    (locally-spawned, undriven) session, then STOPS reading — the suspended-\n 254│    //    brain analog. R MUST be a remote controller: a second remote's Control\n 255│    //    subscribe against a DIFFERENT-remote-driven session busy-refuses PRE-W2\n 256│    //    (the wedge), so only the stall-evict can free the slot — this is what\n    │...\n 259│    //    read from) on its own thread so the broker's writer for R parks in\n*260│    //    write_frame exactly as against a suspended brain. ──\n 261│    let rname = name.clone();\n 262│    thread::spawn(move || {\n 263│        let mut r = connect(&rname);\n    │...\n 335│\n*336│    let evicts = stall_evict_count(&name, Duration::from_secs(5));\n 337│\n 338│    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\n 339│    if let Some(pid) = broker.session_pid(sid) {\n    │...\n 344│        \"=== W2 GATE: viewer_ticks_during_wedge={ticks_during_wedge} take_outcome={outcome:?} \\\n*345│         take_elapsed={took:?} stall_evicts={evicts} ===\"\n 346│    );\n 347│\n 348│    // The viewer kept ticking while the controller conn was wedged.\n    │...\n 377│// ─────────────────────────────────────────────────────────────────────────────\n*378│// UPDATE-WEDGE round-4 — PHYSICAL writer/connection retirement gate.\n 379│//\n 380│// The companion test above proves LOGICAL isolation: a wedged controller is\n 381│// stall-evicted (the ROLE is freed), the viewer keeps ticking, a fresh take\n## brain_resume_conn_deadlock.rs\n  *1│//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\n  *2│//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\n   3│//!\n  *4│//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n   5│//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n   6│//! onto the brain's OWN request/reply conn. A conn's send half is one\n   7│//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n    │...\n  30│//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\n *31│//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\n  32│\n  33│use std::sync::atomic::{AtomicU32, Ordering};\n  34│use std::sync::{Arc, OnceLock};\n    │...\n 235│\n*236│/// RESPAWN-interleave severity — the SHIPPING path under the update-restart burst: a\n 237│/// fresh brain generation resumes CURSOR-ONLY while N sessions already flood at full\n 238│/// rate (the sessions predate this generation). The new generation's heartbeat — hence\n 239│/// its readiness/promotion — is never starved.\n    │...\n 259│/// proof was invalidated — it failed REAL on kitsubito Linux CI (\"worst completed 10\").\n*260│/// doyle ruling `docs/UPDATE-WEDGE-R4-STALE-GUARD-RULING.md` (2026-07-10). The two\n 261│/// cursor-only heartbeat tests above remain the POSITIVE liveness proof; THIS is the\n 262│/// deterministic, timing-independent STRUCTURAL proof that the shipping resume attaches\n 263│/// ZERO subscribers.\n## brain_swap.rs\n  *1│//! C0 E2E — the M3c keystone: a **brain-only self-update** swaps the brain logic\n  *2│//! through the update engine with **zero hosted-endpoint interruption**\n   3│//! (REQ-UPD-3, absolute for the brain-only class — ADR-0004 §A).\n   4│//!\n   5│//! This rides the same broker/brain substrate the B2 handoff test proves, but\n  *6│//! drives the swap through the *update engine* (`plan_update` → `apply_brain_only`)\n   7│//! rather than calling `Brain::handoff` directly — the engine *triggers* a\n   8│//! handoff, it does not re-implement one (`M3c-PLAN.md` §C0, clean-room posture).\n   9│//!\n    │...\n  11│//! brain injects a marker it never reads, then the engine applies a brain-only\n *12│//! update: the new brain re-attaches via the supplied relaunch closure (which\n  13│//! stands for \"exec the verified new binary → `Brain::handoff`\") and must find\n  14│//! that marker in the *replayed* buffer — proving the broker buffered output\n  15│//! produced while no brain was attached. Throughout, the child's pid is unchanged\n    │...\n  17│//!\n *18│//! **Filename note:** this file is `brain_swap.rs`, NOT `update.rs`, on purpose.\n  19│//! Windows' installer-detection heuristic (UAC) forces elevation for any\n *20│//! executable whose name contains `update`/`setup`/`install`/`patch`; a test\n *21│//! binary `update-<hash>.exe` then fails to launch with `os error 740` (requires\n  22│//! elevation). Do not rename this to match the module under test.\n  23│//!\n  24│//! **REQ-UPD-3 `int` re-pointed (restoration D7-1).** This test proves the\n    │...\n  27│//! NOT process-level survival. The REQ-UPD-3 `int` evidence (no endpoint drops\n *28│//! across a brain-only update) now lives in `crates/spt/tests/brain_survive.rs`,\n  29│//! which swaps a real on-disk binary and respawns the real `spt daemon brain`\n  30│//! child process (ADR-0018 V5). This file keeps exercising the engine path; it is\n  31│//! no longer the REQ-UPD-3 int tag.\n    │...\n  39│use spt_daemon::msg::SpawnReq;\n *40│use spt_daemon::update::{apply_brain_only, plan_update, BrokerAbi, ReleaseSpec, UpdateClass};\n  41│use spt_daemon::Broker;\n  42│\n  43│static SEQ: AtomicU32 = AtomicU32::new(0);\n    │...\n  94│#[test]\n *95│fn brain_only_update_swaps_logic_with_zero_endpoint_interruption() {\n  96│    let name = unique_name();\n  97│    let broker = Broker::bind(&name).expect(\"bind broker\");\n  98│\n    │...\n 122│    };\n*123│    let plan = plan_update(&running, release);\n 124│    assert_eq!(\n 125│        plan.class,\n*126│        UpdateClass::BrainOnly,\n 127│        \"a pure-logic release is brain-only\"\n 128│    );\n 129│\n    │...\n 133│    brain\n*134│        .send_input(b\"UPDATED_MARKER_DONE\\r\")\n 135│        .expect(\"inject marker for successor\");\n 136│\n*137│    // Apply the brain-only update through the engine. This is the swap: snapshot\n 138│    // → drop the outgoing brain (the crash boundary the broker tolerates) →\n 139│    // relaunch the new brain over the substrate. The engine touches no endpoint.\n 140│    now += 1; // wall clock advances so the new generation gets a fresh gen_start\n 141│    let mut brain = apply_brain_only(&plan, brain, |state| handoff_relaunch(&name, state, now))\n*142│        .expect(\"brain-only update must apply\");\n 143│\n 144│    // ── REQ-UPD-3: the hosted endpoint never noticed the swap. ──\n 145│    assert_eq!(\n    │...\n 147│        1,\n*148│        \"exactly one session across the update\"\n 149│    );\n 150│    assert_eq!(\n 151│        broker.session_pid(sid),\n    │...\n 164│\n*165│    // Gapless resume: the marker the pre-update brain injected (and never read)\n 166│    // arrives via the broker's replayed buffer — output produced across the swap\n 167│    // window was not lost.\n 168│    let mut buf = Vec::new();\n 169│    brain\n*170│        .read_output_until(b\"UPDATED_MARKER_DONE\", &mut buf)\n*171│        .expect(\"the swapped-in brain must replay output gaplessly across the update\");\n 172│\n*173│    // The post-update brain still fully drives the same endpoint: kill it and see\n 174│    // the exit event + reaping — the session was live and owned, not a husk.\n 175│    brain.kill_session().expect(\"kill session\");\n 176│    loop {\n## budget.rs\n  12│//! A **dormant** seat is a live, state-preserved harness session held warm\n *13│//! by the broker (PTY master + child process + output ring). A **suspended**\n  14│//! seat is that session closed — only the on-disk perch record remains. So:\n  15│//!\n  16│//! 1. spawn N idle sessions through the real broker (the production PTY\n    │...\n  21│//!    per-seat overhead: PTY plumbing + output rings);\n *22│//! 4. kill the sessions (the suspended state) and sample the residual.\n  23│//!\n  24│//! The per-seat child cost + broker delta IS the warm premium over\n *25│//! suspended; the suspended residual is ~the daemon baseline.\n  26│//!\n  27│//! ## Knobs (env)\n  28│//!\n    │...\n 153│\n*154│    // Suspend: close every seat (the session ends; only the perch record\n 155│    // would remain in production). The residual is the daemon baseline.\n 156│    for (brain, _, _) in seats.iter_mut() {\n 157│        let _ = brain.kill_session();\n    │...\n 161│    println!(\n*162│        \"suspended residual (this process RSS after killing all seats): {} (was {} warm)\",\n 163│        fmt_bytes(self_after.rss_bytes),\n 164│        fmt_bytes(self_warm.rss_bytes)\n 165│    );\n## conn_blackhole_lifecycle.rs\n  *1│//! MSG-IDENTITY W6 / F-039 leg (e) — the DETERMINISTIC BLACK-HOLED-CONTROLLER\n   2│//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker\n   3│//! hosting REAL PTY children over the real local IPC.\n   4│//!\n    │...\n   9│//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the\n *10│//! EXACT Windows named-pipe black-hole (a controller that stops draining while a\n  11│//! finite burst floods its socket, so the broker's writer parks inside\n  12│//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` — no supported\n  13│//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/\n    │...\n  17│//!   1. UNRELATED SESSIONS CONTINUE — a second, independent session's viewer keeps\n *18│//!      receiving output throughout the black-hole incident (the fan-out never\n  19│//!      blocks the whole broker on the one wedged conn);\n  20│//!   2. THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE BOUND — a write\n  21│//!      on the retired old-client connection FAILS, and the conn's `handle_conn`\n    │...\n  25│//!      unrelated-viewer floor (<= 1; the persistent invariant-1 viewer holds one\n *26│//!      handler) WITHOUT the black-holed client ever dropping its end (the parked\n  27│//!      writer was aborted + the reader EOF-cleaned, so the handler thread ran to\n  28│//!      completion), and the viewer flows output AFTER the sample latches — proving\n  29│//!      the survivor is the viewer's handler, not a leaked one;\n    │...\n  45│//! This harness GENERALIZES `brain_decouple.rs`'s r4 gate\n *46│//! (`non_draining_controller_stall_evict_releases_writer_and_connection`, the\n  47│//! Windows-mandatory physical-retirement leg for\n  48│//! REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, which stays the primary\n  49│//! gate for that hazard). It reuses the same SEED/trigger/burst choreography,\n  50│//! the hand-back-the-unread-Stream pattern, the `conn_handler_count` and\n *51│//! `stall_evict_count` seams, the old-client-write-fails probe, and the\n  52│//! teardown-before-assert discipline. It ADDS the two pieces the r4 gate does\n  53│//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n  54│//! UNRELATED session whose viewer keeps ticking across the incident, and a fresh\n    │...\n  59│//! Invariants 2, 3, and the task-ownership proxy of 5 are the PHYSICAL-retirement\n *60│//! asserts, and the deterministic black-hole only reproduces on the Windows\n  61│//! named-pipe path (`interprocess 2.4.2` → `WriteFileEx`+`SleepEx(INFINITE)`,\n  62│//! `set_timeout` → `Unsupported`) — the exact field failure. On Unix the\n  63│//! poison/retire path is a `shutdown(SHUT_RDWR)` abort of a buffer-fill block: a\n    │...\n  91│    StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_INPUT, KIND_OUTPUT,\n *92│    KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN, KIND_SPAWNED, KIND_STALL_EVICTS,\n *93│    KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n  94│};\n  95│use spt_daemon::transport::{send_hello, LocalSocketTransport};\n  96│use spt_daemon::Broker;\n    │...\n 136│/// session so its viewer has a continuous frame stream to keep ticking on\n*137│/// throughout the black-hole incident (invariant 1).\n 138│fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139│    SpawnReq {\n 140│        program: \"cmd\".to_string(),\n    │...\n 159│/// the burst floods a non-draining controller's socket so its writer parks INSIDE\n*160│/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n 161│/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162│fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n 163│    SpawnReq {\n    │...\n 197│/// surfaces as a timeout, not a hang). Verbatim sibling of the r4 gate helper.\n*198│fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 199│    let name = name.to_string();\n 200│    let (tx, rx) = std::sync::mpsc::channel();\n 201│    thread::spawn(move || {\n 202│        let mut c = connect(&name);\n*203│        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n 204│        loop {\n 205│            match read_frame(&mut c) {\n*206│                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n 207│                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n 208│                    let _ = tx.send(r.count);\n 209│                    return;\n    │...\n 222│/// One `KIND_SESSIONS` round-trip, returning the reply. The dispatch runs the\n*223│/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n 224│/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225│/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226│fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n    │...\n 291│\n*292│    // ── BLACK-HOLED session B: the real SEED+burst PTY child from conn A, then\n*293│    //    DROP A so the ONLY persistent brain conn is the black-holed controller R. ──\n 294│    let (a, sid_b) = spawn_session(&name, seed_burst_spawn_req(\"blackhole-victim-ep\"));\n 295│    drop(a);\n 296│\n    │...\n 302│    //    draining, so the broker's R-writer parks in `write_frame` exactly as\n*303│    //    against a suspended brain. R's Stream is NEVER dropped until the very end. ──\n 304│    let r_name = name.clone();\n 305│    let (seed_tx, seed_rx) = std::sync::mpsc::channel::<(bool, u64)>();\n 306│    let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();\n    │...\n 371│        // Drain a bounded handful of burst frames so a REAL seq>0 controller frame\n*372│        // is proven delivered, then STOP reading forever (the black-hole). The\n 373│        // remaining ~1300 frames flood the now-unread socket → R-writer wedges.\n 374│        let post_deadline = Instant::now() + Duration::from_secs(5);\n 375│        let mut post = 0u32;\n    │...\n 386│        }\n*387│        // Hand the Stream back to main UNREAD (main holds it = the black-holed\n 388│        // client). The broker's R-writer stays wedged on the remaining burst.\n 389│        let _ = hand_tx.send((r, max_seq));\n 390│    });\n## control_stamp_lifetime.rs\n   9│//!\n *10│//! `/exit` ends the RC session INDIRECTLY: the endpoint suspends when the child\n  11│//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n *12│//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n  13│//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n  14│//! path dropped the OutputLog with the controller slot still occupied, so the\n  15│//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\n    │...\n 183│    // ── Kill the PTY CHILD → the exit-waiter reaps the session (NOT a conn drop, so\n*184│    //    the controller-detach path never runs; only the reap path can unlatch). ──\n 185│    let pid = broker\n 186│        .session_pid(sid)\n 187│        .expect(\"live session must have a pid before kill\");\n## daemon_lifecycle_real_brain.rs\n  16│//! no longer waits for a resident `<id>-psyche` perch to bind (host_one spawns none)\n *17│//! and reaps no detached child. The DAEMON-1 evidence is the hosted pulse loop\n  18│//! ingesting the commune — unchanged and independent of the retired resident spawn.\n  19│//!\n  20│//! Single `#[test]`: it owns the process-global `SPT_HOME`.\n    │...\n 113│    // No resident psyche to reap (F-030 W1 — host_one spawns none). The run_brain +\n*114│    // serve threads are detached and die with the test process.\n 115│}\n## dispatch.rs\n   22│use spt_daemon::nethost::{NetConfig, NetHost};\n  *23│use spt_daemon::propagate::{request_update, UpdatePullOutcome};\n   24│use spt_daemon::relcache::ReleaseCache;\n   25│use spt_daemon::release::{sha256_hex, ReleaseMetadata, SignedRelease, VerifyPolicy};\n   26│use spt_daemon::sync::request_sync;\n  *27│use spt_daemon::update::{BrokerAbi, BROKER_RESOURCE_ABI};\n   28│use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\n   29│use spt_net::net::notif::NotifRecord;\n   30│use spt_net::net::wanmsg::WanMessage;\n     │...\n  123│    // Mesh-D6: membership is the roster — seed the canonical roster (the inbound\n *124│    // gates and the update serve gate both read it).\n  125│    let mut roster = spt_store::roster::RosterStore::load();\n  126│    roster.merge_entry(spt_store::roster::RosterEntry {\n  127│        pubkey_hex: peer_hex.to_string(),\n     │...\n  207│\n *208│// [int->REQ-UPD-1] an update pull is served end to end by the dispatcher:\n  209│// B stages a release, A queries/fetches/verifies/stages — no hand-wired\n *210│// serve_update. The serve gate ran from B's canonical roster.\n  211│#[test]\n *212│fn dispatcher_serves_an_update_pull_undriven() {\n  213│    init_home();\n  214│    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n  215│    let dir = TempDir::new().unwrap();\n     │...\n  262│    let cache_a = ReleaseCache::open(&dir.path().join(\"releases-a\"));\n *263│    let outcome = request_update(\n  264│        &mut a,\n  265│        conn.conn_id,\n  266│        MintedOp::new(Minter::Pump, op()),\n     │...\n  274│    assert!(\n *275│        matches!(outcome, UpdatePullOutcome::Updated(_)),\n *276│        \"the dispatcher served the update undriven: {outcome:?}\"\n  277│    );\n  278│    assert_eq!(\n  279│        cache_a.load_artifact().expect(\"staged bytes\"),\n     │...\n  615│    );\n *616│    // Detach the viewport cleanly before the refuse probe.\n  617│    let _ = a.net_stream_send(viewport, &[], None, true);\n  618│\n  619│    // A STALE endpoint (no live session on B) refuses: clean finish, no output.\n     │...\n  970│// own AttachmentStore + SubnetStore. An attached member answers serving=true;\n *971│// a subnet B never joined answers false; B `detach`ing the subnet flips the\n  972│// answer to false WHILE B STAYS REACHABLE on the same connection (the\n  973│// criterion-5 fix — a raw dial proved only \"the box is up\" and kept reading\n *974│// a detached peer online); re-attach restores serving.\n  975│#[test]\n  976│fn dispatcher_serves_a_subnet_serve_probe() {\n  977│    use spt_daemon::request_subnet_probe;\n     │...\n  995│    let mut att = AttachmentStore::load();\n *996│    att.set_detached(\"ProbeNet\", false); // clean any prior detach in shared home\n  997│    att.save().expect(\"save attachment\");\n  998│\n  999│    let mut a = connect_retry(&a_name);\n     │...\n 1016│\n*1017│    // Detach it → the SAME reachable box now answers not-serving.\n 1018│    let mut att = AttachmentStore::load();\n*1019│    att.set_detached(\"ProbeNet\", true);\n 1020│    att.save().expect(\"save attachment\");\n 1021│    assert!(\n*1022│        !request_subnet_probe(&mut a, conn.conn_id, \"ProbeNet\").expect(\"probe detached\"),\n*1023│        \"a detached member stops serving while still reachable (criterion-5 fix)\"\n 1024│    );\n 1025│\n 1026│    // Re-attach → serving again (also restores the shared SPT_HOME state).\n 1027│    let mut att = AttachmentStore::load();\n*1028│    att.set_detached(\"ProbeNet\", false);\n 1029│    att.save().expect(\"save attachment\");\n 1030│    assert!(\n*1031│        request_subnet_probe(&mut a, conn.conn_id, \"ProbeNet\").expect(\"probe reattached\"),\n 1032│        \"re-attach restores serving\"\n 1033│    );\n 1034│\n     │...\n 1038│// [unit->REQ-SUBNET-3] the demux routes a node-label feed: an endpoint-less\n*1039│// peer sends a NodeLabelUpdate first (no instance row), so the feed's FIRST\n 1040│// line is `{subnet,node,label,epoch}` — it must classify as the Registry\n 1041│// family (same serve arm + gate) rather than dropping as Unknown. An instance\n 1042│// row still classifies Registry; a bare unrelated object stays Unknown.\n## docs_server_e2e.rs\n   5│//! rejected; a docs-less home answers the loud not-found WITH the\n  *6│//! `spt update fetch --apply` pointer while the server stays bound; writes\n   7│//! are refused (GET/HEAD only).\n   8│//!\n   9│//! Hermetic: port 0 (ephemeral), raw std TcpStream client — no HTTP client\n    │...\n 114│    assert!(\n*115│        text.contains(\"spt update fetch --apply\"),\n 116│        \"the pointer at the landing command: {text}\"\n 117│    );\n 118│    // Still bound and answering after the docs-less response.\n## driven_by_selfheal.rs\n   6│//! An spt-hosted endpoint can latch `ONLINE+CONTROLLED` — `info.json`\n  *7│//! `driven_by = Some(node)` — and never clear it when a controller's detach is\n   8│//! LOST (no clean disconnect, and no output to trigger the W1 drain-evict\n   9│//! `mark_controller_gone`). The picker then renders a phantom \"controlled by X\"\n  10│//! forever. `reconcile_hosted_liveness` (`livehost.rs`) is the pull-primary\n    │...\n  35│//!   an idle session whose ABANDONED REMOTE controller wedged without a clean\n *36│//!   detach — keeps `controller_by==Some(origin)`, which a reconcile read can't\n  37│//!   detect at all. That case is its own deferred requirement,\n  38│//!   `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT` (broker-side controller eviction —\n  39│//!   NOT a reconcile `controller_by` clear). See `inject_control_wedge.rs` `w5_a2`\n    │...\n  52│//! It also captures the broker's OWN self-heal on an OBSERVED controller\n *53│//! disconnect (per-conn `detach_if`→`clear_controller`→`stamp_driven_by`).\n  54│//!\n  55│//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n  56│//!\n    │...\n 156│/// spt-hosted (`controllable=Some(true)`) `live_agent` self-perch, `status=online`,\n*157│/// pre-stamped `driven_by=Some(node)` (the lost-detach latch). Mirrors the\n 158│/// livehost-bootgate-e2e stamping exactly, plus the `driven_by` marker.\n 159│fn stamp_latched_perch(id: &str, node: &str) -> std::path::PathBuf {\n 160│    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n    │...\n 166│    spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_ONLINE).unwrap();\n*167│    // The lost-detach latch the self-heal must clear.\n 168│    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\n 169│    self_perch\n 170│}\n    │...\n 281│    // FINDING (the broker's OWN self-heal on a controller disconnect): dropping the\n*282│    // spawner socket makes the broker's per-conn handler run `detach_if` →\n 283│    // `clear_controller` → `stamp_driven_by`, which (since controller_by() is now\n 284│    // None) RE-STAMPS the perch's driven_by to None. So an OBSERVED controller\n 285│    // disconnect already unlatches via the broker single-writer — even without a\n*286│    // clean detach. The Gap-A the W5 RECONCILE must heal is the path where that\n 287│    // re-stamp NEVER fires: a brain restart re-adopts the live session but the\n 288│    // in-memory controller (and thus the clear_controller re-stamp) never existed,\n 289│    // so the STALE on-disk driven_by marker persists against a controller_by==None\n    │...\n 300│         driven_by_after_spawner_drop={driven_by_after_disconnect:?} \\\n*301│         (None = the broker's detach_if→clear_controller→stamp_driven_by already \\\n 302│         unlatched the OBSERVED disconnect — so the RECONCILE Gap-A is only the \\\n 303│         NEVER-RE-STAMPED brain-restart leftover, reproduced next) ===\"\n 304│    );\n## endpoint_survival.rs\n *1│//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) — daemon-restart endpoint\n  2│//! survival, against a REAL broker over the real local IPC.\n  3│//!\n  4│//! ## The bug this gate locks down (seed #6, the field-accept blocker)\n  5│//!\n *6│//! A `daemon stop` + `start` (the `update apply` notice's OWN instruction) kills every\n  7│//! daemon-hosted harness; pre-W3 they stayed OFFLINE after start (no resurrection)\n  8│//! though their records survived (info.json status + adapter + cwd + a ledger row). The\n  9│//! fix: `livehost::resume_restart_orphaned_endpoints` runs ONCE at the fresh broker's\n   │...\n 84│\n*85│// [int->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n 86│#[test]\n 87│fn daemon_restart_reruns_a_previously_online_orphaned_endpoint() {\n 88│    let home = tempfile::TempDir::new().expect(\"temp home\");\n## false_promote.rs\n  *1│//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-PROMOTE-DRAINED) — the FALSE-PROMOTE int rig, on a\n   2│//! REAL broker hosting a REAL PTY child over the real local IPC. Exercises the\n   3│//! PROMOTION PATH ITSELF (`brainproc::supervise_brain` → `run_trial`), the gate\n   4│//! condition doyle's W2 verdict (@e5ae7a9) made binding for W3.\n    │...\n  10│//! can signal `brain.ready` INSIDE that window while the OLD generation's local\n *11│//! controller conn is still wedged (blocked writes pending), so `update apply`\n  12│//! \"promotes\" onto a still-frozen control plane (the incident-night 22:47 apply). The\n  13│//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\n *14│//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\n  15│//! read from BROKER TRUTH in-process (no brain round-trip).\n  16│//!\n  17│//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\n    │...\n  19│//! Pre-round-4 this rig manufactured its wedge with a REAL writer parked inside\n *20│//! `write_frame` on a black-holed spawner conn. Round-4\n  21│//! (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK) gives every physical conn\n  22│//! write an independent watchdog that CANCELS + retires a wedged write at the SAME\n  23│//! `brain_write_deadline()` bound — so a real parked writer now self-heals at the\n    │...\n  25│//! fix working, not a regression; the 22:47 permanent-frozen-plane class is dead by\n *26│//! construction). Ruling `docs/UPDATE-WEDGE-R4-FALSE-PROMOTE-RULING.md`: this rig\n  27│//! tests the GATE DECISION in isolation, so the wedge is now INJECTED statically —\n  28│//! `Broker::test_seed_wedged_local_controller` installs a LOCAL controller\n  29│//! (`by = None`, the brain's own resume/host identity) whose `write_blocked_since`\n    │...\n  32│//! bound/watchdog itself is `brain_decouple.rs`'s concern. The DRAIN below still\n *33│//! rides the REAL logical path (the take's `stall_evict_controller`), unchanged.\n  34│//! `SPT_BRAIN_WRITE_DEADLINE_MS` still shrinks the 15 s deadline (the knob grants\n  35│//! wall-clock only; it never relaxes an assertion).\n  36│//!\n    │...\n  40│//! 1. the trial does NOT promote while wedged (RED-first: remove the\n *41│//!    `&& env.old_gen_drained()` gate in `run_trial` and it promotes on ready alone,\n  42│//!    reproducing the 22:47 false-promote);\n  43│//! 2. once the wedged old conn drains (here: a second operator's take stall-evicts it\n  44│//!    broker-side, the W2 self-heal), the candidate promotes — ready-seen was LATCHED\n    │...\n 157│/// stall-evicts the wedged local incumbent broker-side (the W2 self-heal). Holds the\n*158│/// conn briefly so the take is not immediately detached, then lets it fall.\n 159│fn remote_take(name: &str, sid: u64) {\n 160│    let name = name.to_string();\n 161│    thread::spawn(move || {\n    │...\n 229│    // The gate under test: REAL broker truth, no brain round-trip.\n*230│    fn old_gen_drained(&self) -> bool {\n 231│        !self.broker.any_local_controller_wedged()\n 232│    }\n 233│}\n 234│\n*235│// [int->REQ-UPDATE-PROMOTE-DRAINED]\n 236│#[test]\n 237│fn ready_candidate_does_not_promote_until_the_wedged_old_gen_conn_drains() {\n 238│    init_home();\n    │...\n 268│    // REPLACES A's spawn-attached controller sink; A itself can drop (its conn\n*269│    // detach no longer owns the controller slot).\n 270│    assert!(\n 271│        broker.test_seed_wedged_local_controller(sid, 1_000),\n 272│        \"precondition: the wedge seed must install on the hosted session\"\n## handoff.rs\n 83│\n*84│    // The broker accepts connections forever on a detached thread; it outlives\n 85│    // every brain (that is the whole point).\n 86│    let serve_broker = Arc::clone(&broker);\n 87│    thread::spawn(move || {\n## inject_control_wedge.rs\n   22│//! the writer advances `delivered_through`). The drain hands each chunk off with a\n  *23│//! bounded `send_timeout` OFF the log lock; a fully-stalled controller is detached\n   24│//! after `CONTROLLER_WRITE_DEADLINE`. So even while the controller is fully backed\n   25│//! up, the session does NOT wedge — `KIND_SESSIONS` still answers. THIS test\n   26│//! asserts that fixed behavior.\n     │...\n  169│/// `sessions` Mutex — if a wedged per-conn handler is holding that lock the read\n *170│/// blocks and rides teardown to the 240s backstop. Probe it on a detached thread\n  171│/// (the broker is an `Arc`, so the clone keeps it alive) and give up on timeout;\n  172│/// a missed child-kill is harmless, the process-per-test exit reaps it.\n  173│fn session_pid_bounded(broker: &Arc<Broker>, sid: u64, dur: Duration) -> Option<u32> {\n     │...\n  775│        // Report the result IMMEDIATELY (the main thread's watchdog can proceed),\n *776│        // THEN best-effort detach + drain. We deliberately do NOT block the result\n  777│        // on the serve thread: a controller's `serve_attach` forward can block on a\n  778│        // non-draining operator socket while the flood child keeps producing — the\n  779│        // main thread kills the child to unblock it. Reporting first then draining\n     │...\n  783│        // Keep draining the operator so serve's forwards drain and serve_attach can\n *784│        // observe our detach + the child's eventual death and return cleanly.\n  785│        let drain_deadline = std::time::Instant::now() + Duration::from_secs(12);\n  786│        while std::time::Instant::now() < drain_deadline {\n  787│            match operator\n     │...\n  848│    // Each `cold_start_pump` Brain (the driver's `w`, the attacher's `operator`)\n *849│    // spawns a DETACHED `pump-ipc-reader` thread parked in a BLOCKING named-pipe\n  850│    // `read_frame` on its RecvHalf. Joining the helper threads drops those Brains,\n  851│    // which drops each Brain's SendHalf → the broker's per-conn handler read errors\n  852│    // → the broker closes its end → the client RecvHalf read finally errors → the\n     │...\n 2158│//     session is listed live, and `driven_by` (if pre-stamped) stays Some.\n*2159│//   - A2 (production wedged-pump / lost detach — the REQ's real target): a real\n 2160│//     remote controller ATTACHES (become_controller → driven_by=Some, slot\n 2161│//     occupied by Some(origin)), then its remote is gone but the broker-side\n*2162│//     controller sink stays OPEN (no clean EOF/detach) and the session is IDLE\n 2163│//     (no output → the W1 drain-evict / CONTROLLER_WRITE_DEADLINE never fires) →\n 2164│//     the slot stays Some(origin) → `controller_by` reads SOME, `driven_by` stays\n 2165│//     Some — the broker still THINKS it's controlled.\n     │...\n 2264│    //    operator's remote is then ABANDONED by dropping its brain WITHOUT a clean\n*2265│    //    EOF/detach, and the session is kept IDLE (a QUIET child, no output → the W1\n 2266│    //    output-drain-evict never fires). We read controller_by + driven_by and REPORT\n 2267│    //    whether the broker self-clears the abandoned slot (characterization-only). ──\n 2268│    //\n     │...\n 2342│        // Drive serve_attach until told to stop (it blocks on brain.read_event()).\n*2343│        // We DON'T cleanly detach — we just abandon it when the test ends. Running\n 2344│        // it here keeps the controller slot occupied by Some(origin).\n 2345│        let serve_broker = serve_name.clone();\n*2346│        let _ = serve_stop; // abandoned, not signalled (the \"lost detach\" shape)\n 2347│        let _ = serve_attach(&mut target, &serve_broker, stream_a, \"w5-a2-ep\", &origin, 0, None);\n 2348│    });\n 2349│\n     │...\n 2372│\n*2373│    // ── ABANDON the operator's remote WITHOUT a clean detach: drop the operator\n*2374│    //    brain WITHOUT sending the EOF detach record (net_stream_send(.., true)).\n 2375│    //    The session is idle (quiet child), so nothing evicts the controller slot. ──\n 2376│    drop(operator);\n 2377│\n     │...\n 2413│        // DID become the controller (slot = Some(origin)). The remote is then\n*2414│        // abandoned (drop, no clean EOF/detach), closing its loopback QUIC conn (FIN).\n 2415│        // Whether the broker clears the slot WITHIN the 2s read window is conn-close\n 2416│        // detection — and it is TIMING-VARIABLE on BOTH platforms: QUIC FIN-detection\n 2417│        // latency vs the read window is NON-DETERMINISTIC (unix loopback USUALLY but\n## input_ack_deadlock.rs\n  15│//! forever: a concurrent attach is never serviced, and the original controller's\n *16│//! own detach can never be processed (it stays latched).\n  17│//!\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    │...\n 263│/// viewer attach still OPENS + is serviced + receives the flood child's output, and\n*264│/// the flood controller's detach registers. Pre-fix (ack=true) the return direction\n 265│/// fills, the handler deadlocks, and the concurrent attach is never serviced.\n 266│///\n 267│/// Real broker + real PTY + real serve_attach, NO mocks. Every wait is behind a\n    │...\n 374│\n*375│        // Detach: close the send side (best-effort — the broker's draining handler\n 376│        // processes it and releases the controller seat). Then ABANDON the conn +\n 377│        // serve thread WITHOUT a blocking drain loop: a Whole conn's read ignores\n 378│        // any deadline, so a drain loop here would hang the helper. The main thread's\n    │...\n 517│    //    child (closes the PTY → unwinds the abandoned serve_attach threads + their\n*518│    //    target conns), then DETACH the helper handles (do NOT join — a serve_attach\n 519│    //    on a Whole `target` conn can park, and joining it would re-introduce the\n 520│    //    very hang this test guards against). ──\n 521│    if let Some(pid) = broker.session_pid(sid) {\n## legacy_resident_sweep_e2e.rs\n 124│    let child_exe = spt_store::proc::exe_basename(mock_pid).expect(\"mock exe basename\");\n*125│    // DETACH the child handles (they keep running — dropping a `Child` never kills). This\n 126│    // is load-bearing on Windows: a held process HANDLE keeps the process object queryable\n 127│    // (OpenProcess succeeds) even AFTER termination, so `is_process_alive` would lie true;\n 128│    // with no handle held, liveness is honest by pid. Cleanup reaps by pid below.\n## mesh.rs\n 120│/// The inert/registry-only cadence: only registry feeds flow (notif/sync/\n*121│/// update are pushed far enough out to never fire), so the shared canonical\n 122│/// notif/context stores stay untouched across nodes.\n 123│fn cadence() -> PumpCadence {\n 124│    PumpCadence {\n    │...\n 127│        sync: Duration::from_secs(3_000),\n*128│        update: Duration::from_secs(3_000),\n 129│        registry_evict_after: Duration::from_secs(3_000),\n 130│    }\n 131│}\n    │...\n 229│                    cadence: cadence(),\n*230│                    full_auto_update: false,\n 231│                };\n 232│                let _ = run_peer_pump(\n 233│                    &name,\n## net_worker_starve.rs\n  *1│//! UPDATE-WEDGE round-3 DIAGNOSTIC rig (throwaway — NO REQ until the verdict\n  *2│//! lands and doyle rules the shape; see docs/UPDATE-WEDGE-2-ROUND3-RULING.md).\n   3│//!\n   4│//! The decider the round-3 ruling §5 demands: does a burst of dead-peer\n   5│//! `submit_dial`s actually HOLD/serialize the broker net runtime's 2 worker\n    │...\n  10│//! STAGE 1 (this file, first): the narrow mechanism probe. Stand a dialer whose\n *11│//! connects black-hole a dead peer, fire a `submit_dial` burst straight onto the\n  12│//! broker's own [`NetHost`] (the exact pump path), and read the two round-3\n  13│//! DIAGNOSTIC probes added to `nethost.rs`:\n  14│//!   - `active_dial_tasks()` — in-flight dead-peer connects on the 2 workers;\n    │...\n 106│/// THE DECIDER (round-3 ruling §5): a burst of `submit_dial`s to a peer that\n*107│/// accepts the QUIC handshake but black-holes the seed-proof. Reports whether\n 108│/// the burst holds the 2 workers (canary goes stale) or the awaits yield (canary\n 109│/// stays fresh). This test does not FAIL on either outcome — it PRINTS the verdict\n 110│/// the ruling needs; an explicit assert nails the direction once observed.\n    │...\n 121│\n*122│    // B: the black hole — accepts the handshake, never runs the proof responder.\n 123│    let broker_b = served_broker(&name_b, hermetic(Identity::generate()), &dir.path().join(\"b\"));\n 124│    let host_b = broker_b.net().expect(\"host b\");\n 125│    let b_addr = host_b.addr();\n## netbroker.rs\n  53│/// is irrelevant; its PRESENCE is what makes the dialer open the stream and\n *54│/// block, giving the deterministic black hole the broker-side bound must cut.\n  55│fn proving(identity: Identity) -> NetConfig {\n  56│    let mut cfg = hermetic(identity);\n  57│    cfg.membership = Some(std::sync::Arc::new(|| {\n    │...\n 234│// pump-IPC-deadline B-half): a dial to a peer that ACCEPTS the connection but\n*235│// black-holes the seed-proof (a membership-mismatched/dead peer — the exact\n 236│// shape of the 2.2h hfenduleam wedge) must fail PROMPTLY with an ORDINARY broker\n 237│// error reply within the bound, NEVER an unbounded await. Exactly-once is\n 238│// preserved on timeout: the journaled dial records NO phantom conn — the journal\n    │...\n 260│\n*261│    // B: the black hole — accepts the QUIC handshake (`membership: None` ⇒ it\n 262│    // registers the conn) but never runs the proof responder, so A's\n 263│    // `prove_membership` awaits a control-stream reply that never comes.\n 264│    let _broker_b = net_broker(&name_b, Identity::generate(), &dir.path().join(\"b\"));\n    │...\n 273│        .net_dial(b_addr, Some(MintedOp::new(Minter::Cli, OP)))\n*274│        .expect_err(\"a black-holed dial must error, never block forever\");\n 275│    let elapsed = started.elapsed();\n 276│\n 277│    // The broker REPLIED an ordinary error within the bound — not a wedge, and","missingPaths":["scripts"]},"isError":false,"timestamp":1784195780485}}
{"type":"message","id":"51689ab9","parentId":"c7827dc9","timestamp":"2026-07-16T09:56:20.518Z","message":{"role":"toolResult","toolCallId":"call_chLDGELsrrqOr6JeD6vIRwAf|fc_008390f508d1d09b016a58aac456dc819ab4b7d45d03972424","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/tests/\n### attach.rs\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 27|use spt_daemon::attach::{\n*28|    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n*29|    AttachServeOutcome, ControllerIrrecoverablyBehind,\n 30|};\n 31|// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n*32|#[cfg(unix)]\n 33|use spt_daemon::attach::request_attach_endpoint;\n 34|use spt_daemon::brain::{Brain, BrainState, BrokerEvent};\n 35|use spt_daemon::effect::{Minter, MintedOp};\n...\n 49|\n*50|fn init_home() {\n 51|    HOME.get_or_init(|| {\n 52|        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n 53|        std::env::set_var(\"SPT_HOME\", dir.path());\n...\n 58|static SEQ: AtomicU32 = AtomicU32::new(0);\n*59|fn unique_name() -> String {\n 60|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 61|    format!(\"spt-daemon-d5b-{}-{}.sock\", std::process::id(), n)\n 62|}\n 63|\n*64|fn hermetic() -> NetConfig {\n 65|    NetConfig {\n 66|        identity: Identity::generate(),\n 67|        relay: RelayPolicy::Disabled,\n...\n 73|\n*74|fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n 75|    let host = NetHost::start(hermetic()).expect(\"net host start\");\n 76|    let broker =\n 77|        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n...\n 84|\n*85|fn connect_retry(name: &str) -> Brain {\n 86|    for _ in 0..300 {\n 87|        if let Ok(b) = Brain::cold_start(name, 1) {\n 88|            return b;\n...\n 94|\n*95|fn echo_spawn_req() -> SpawnReq {\n*96|    #[cfg(unix)]\n 97|    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n*98|    #[cfg(windows)]\n 99|    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n 100|    SpawnReq {\n 101|        program,\n...\n 109|        adapter: String::new(),\n*110|        install_dir: None,\n 111|    }\n 112|}\n 113|\n...\n 118|/// row and the cross-wired peer row: the serve target is always the peer row.\n*119|fn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n 120|    for _ in 0..400 {\n 121|        let reply = brain.net_streams().expect(\"net-streams\");\n 122|        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {\n...\n 140|/// origin), drive `marker` through the PTY and render it back over the wire,\n*141|/// then detach and confirm the serve loop ended `Detached` (session outlives).\n 142|/// Returns the rendered viewport bytes + the (still-live) target brain so the\n 143|/// caller can assert session survival and clean up.\n 144|#[allow(clippy::too_many_arguments)]\n*145|fn attach_drive_detach(\n 146|    operator: &mut Brain,\n 147|    target_broker: &str,\n 148|    mut target: Brain,\n...\n 191|\n*192|    // Detach: the viewport ends with the session still running.\n 193|    operator\n 194|        .net_stream_send(stream_b, &[], None, true)\n*195|        .expect(\"detach\");\n 196|    let (outcome, target) = server.join().expect(\"serve thread\");\n*197|    assert_eq!(outcome, AttachServeOutcome::Detached);\n 198|    (viewport, target)\n 199|}\n 200|\n...\n 203|/// the rendered-through cursor (last accepted seq + 1).\n*204|fn render_until(\n 205|    brain: &mut Brain,\n 206|    stream_id: u64,\n 207|    decoder: &mut AttachDecoder,\n### broker.rs\n 37|static SEQ: AtomicU32 = AtomicU32::new(0);\n*38|fn unique_name() -> String {\n 39|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 40|    format!(\"spt-daemon-b1-{}-{}.sock\", std::process::id(), n)\n 41|}\n...\n 43|/// The echo child: reads stdin, writes each line to stdout.\n*44|fn echo_spawn_req() -> SpawnReq {\n*45|    #[cfg(unix)]\n 46|    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n*47|    #[cfg(windows)]\n 48|    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n 49|    SpawnReq {\n 50|        program,\n...\n 58|        adapter: String::new(),\n*59|        install_dir: None,\n 60|    }\n 61|}\n 62|\n*63|fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 64|    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 65|}\n 66|\n*67|fn main_test() {\n 68|    let name = unique_name();\n 69|    let broker = Broker::bind(&name).expect(\"bind broker\");\n 70|\n...\n 91|    // always pushes `spawned`/`output`/`exit` frames, so the blocking reads\n*92|    // below return; a genuine stall is caught by the test runner's own timeout.)\n 93|\n 94|    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n 95|\n...\n 175|\n*176|fn accumulate_output(frame: &Envelope, out: &mut Vec<u8>) {\n 177|    let ev: OutputEvent = serde_json::from_value(frame.payload.clone()).unwrap();\n 178|    out.extend_from_slice(&decode_bytes(&ev.data_b64).unwrap());\n 179|}\n 180|\n*181|#[test]\n*182|fn broker_hosts_pty_child_streams_output_and_accepts_input() {\n 183|    main_test();\n 184|}\n 185|\n...\n 197|// [int->REQ-MSG-IDLE-TRANSLATION-BINARY]\n*198|// [int->REQ-HAZARD-IDLE-SILENT-NONDELIVERY]\n*199|fn endpoint_input_test() {\n 200|    let name = unique_name();\n 201|    let broker = Broker::bind(&name).expect(\"bind broker\");\n 202|    let server_broker = Arc::clone(&broker);\n...\n 311|\n*312|#[test]\n*313|fn endpoint_keyed_inject_without_binary_spools_not_pty() {\n 314|    endpoint_input_test();\n 315|}\n 316|\n*317|/// REQ-HAZARD-ENV-SUBST: a SpawnReq's `env` map reaches the spawned PTY child's\n 318|/// environment — the broker leg of the F-013 fix (a flagless harness gets\n 319|/// SPT_ENDPOINT_ID populated, instead of an empty value → seed-by-PPID → zero\n 320|/// perch). Spawn a child that ECHOES the injected env var; its output must carry\n 321|/// the value.\n*322|// [int->REQ-HAZARD-ENV-SUBST]\n*323|fn env_inject_test() {\n 324|    let name = unique_name();\n 325|    let broker = Broker::bind(&name).expect(\"bind broker\");\n 326|    let server_broker = Arc::clone(&broker);\n...\n 344|    // A child that prints the injected env var, cross-OS.\n*345|    #[cfg(unix)]\n 346|    let (program, args) = (\n 347|        \"sh\".to_string(),\n 348|        vec![\"-c\".to_string(), \"echo ENVCHECK=$SPT_ENDPOINT_ID\".to_string()],\n 349|    );\n*350|    #[cfg(windows)]\n 351|    let (program, args) = (\n 352|        \"cmd\".to_string(),\n 353|        vec![\"/c\".to_string(), \"echo ENVCHECK=%SPT_ENDPOINT_ID%\".to_string()],\n### control_stamp_lifetime.rs\n*1|//! F-026 W1 #2 (REQ-HAZARD-CONTROL-STAMP-LIFETIME) — a control/viewer stamp never\n 2|//! outlives its session, on a REAL broker over the real local IPC (no PTY/transport\n 3|//! mocks). Red-first: the exit-waiter used to `sessions.remove(&id)` with NO stamp\n*4|//! clear, so `/exit` (which kills the CHILD, not the controller conn) left\n*5|//! `controlled` / `driven_by` / `viewer_count` latched in info.json forever — and\n*6|//! gossiped a stale `controller_node` cross-node.\n 7|//!\n 8|//! ## The bug this gate locks down\n 9|//!\n...\n 11|//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n*12|//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n*13|//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n*14|//! path dropped the OutputLog with the controller slot still occupied, so the\n 15|//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\n 16|//! hall-a's info.json still read `controlled:true` and hfenduleam still gossiped\n*17|//! `controller_node=self`.\n 18|//!\n 19|//! ## What the fix does\n 20|//!\n...\n 22|//! three stamps (`set_driven_by(None)`, `set_controlled(false)`,\n*23|//! `set_viewer_count(0)`) via the known endpoint id. The broker stays the single\n*24|//! writer; the session is dead so no live controller re-stamps concurrently (the\n 25|//! same race-free argument as the W5 no-session self-heal).\n 26|//!\n 27|//! ## Why kill the CHILD (not drop the conn)\n 28|//!\n*29|//! Dropping the spawner conn would run the EXISTING `clear_controller` path and\n 30|//! prove nothing about the reap leg. Killing the PTY child fires the exit-waiter\n*31|//! while the controller conn stays open — isolating the reap clear as the ONLY\n 32|//! path that can unlatch the stamps.\n 33|//!\n 34|//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n...\n 55|static SEQ: AtomicU32 = AtomicU32::new(0);\n*56|fn unique_name() -> String {\n 57|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 58|    format!(\"spt-daemon-ctlstamp-{}-{}.sock\", std::process::id(), n)\n 59|}\n...\n 61|static STAMP_HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n*62|fn init_stamp_home() {\n 63|    STAMP_HOME.get_or_init(|| {\n 64|        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n 65|        std::env::set_var(\"SPT_HOME\", dir.path());\n...\n 69|\n*70|fn kill_pid(pid: u32) {\n*71|    #[cfg(windows)]\n 72|    let _ = std::process::Command::new(\"taskkill\")\n 73|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 74|        .output();\n*75|    #[cfg(unix)]\n 76|    let _ = std::process::Command::new(\"kill\")\n 77|        .args([\"-9\", &pid.to_string()])\n 78|        .output();\n...\n 80|\n*81|fn start_broker(name: &str) -> Arc<Broker> {\n 82|    let broker = Broker::bind(name).expect(\"bind broker\");\n 83|    let serve = Arc::clone(&broker);\n 84|    thread::spawn(move || {\n...\n 89|\n*90|fn connect_brain(name: &str) -> Brain {\n 91|    for _ in 0..300 {\n 92|        if let Ok(b) = Brain::cold_start(name, 1) {\n 93|            return b;\n...\n 101|/// kill it (cross-OS).\n*102|fn idle_spawn_req(endpoint: &str) -> SpawnReq {\n### inject_control_wedge.rs\n*1|//! v0.13.0 W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) — the CONTROLLER-DELIVERY\n 2|//! non-wedge gate, on a REAL broker hosting a REAL PTY child over the real local\n 3|//! IPC (no mocks of the PTY or the transport).\n 4|//!\n*5|//! ## The bug this gate locks down (park (a), the output-stall / wedge root)\n 6|//!\n 7|//! PRE-FIX (v0.12.x), [`OutputLog::append`] fanned each output chunk to the\n*8|//! CONTROLLER via a SYNCHRONOUS, BLOCKING `write_frame` held INLINE on the\n 9|//! session's single drain thread, while the `Mutex<OutputLog>` was locked.\n*10|//! VIEWERS instead get a dedicated writer thread + a bounded, evicting\n*11|//! `sync_channel` and can never stall the drain. So a slow / non-draining\n*12|//! controller (here a controller connection that stops reading while the child\n 13|//! floods) parked the drain INSIDE `append` STILL HOLDING the log lock — output\n*14|//! stalled and any path needing that log lock (a fresh attach, or the global\n 15|//! `KIND_SESSIONS` enumeration, which reads each log's `delivered_through`)\n 16|//! blocked FOREVER, while the broker process stayed alive. The repro that proved\n 17|//! this (captured 2026-06-19): `APPEND_CTRL_ENTER sid=1 seq=2` with no matching\n...\n 19|//!\n*20|//! POST-FIX (W1): controller delivery moved off the drain thread onto a dedicated\n*21|//! writer thread + bounded channel (the viewer_writer pattern, but authoritative:\n 22|//! the writer advances `delivered_through`). The drain hands each chunk off with a\n*23|//! bounded `send_timeout` OFF the log lock; a fully-stalled controller is detached\n*24|//! after `CONTROLLER_WRITE_DEADLINE`. So even while the controller is fully backed\n 25|//! up, the session does NOT wedge — `KIND_SESSIONS` still answers. THIS test\n 26|//! asserts that fixed behavior.\n 27|//!\n...\n 56|\n*57|// ── F-023 leg 1 (REQ-WAN-SPT-HOSTED-DELIVERY / REQ-HAZARD-DELIVERY-STARVATION):\n 58|//    the WAN-ingress inject leg. `receive_wan` funnels a cross-node `spt send`;\n 59|//    the shared spt-hosted inject leg must reach the broker at\n 60|//    `broker_socket_name()` (the in-daemon dispatcher's socket) and drive the\n...\n 65|\n*66|// ── W1b additions (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE): the real-fixture attach\n 67|//    path, the Brain client API, and a net-backed broker for loopback attach. ──\n 68|use std::sync::atomic::{AtomicBool, AtomicU64};\n 69|use std::sync::OnceLock;\n...\n 80|static SEQ: AtomicU32 = AtomicU32::new(0);\n*81|fn unique_name() -> String {\n 82|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 83|    format!(\"spt-daemon-w1wedge-{}-{}.sock\", std::process::id(), n)\n 84|}\n...\n 95|/// wall-clock ONLY; it never relaxes the `subscribed` / `got_output` assertions.\n*96|fn attach_gate_watchdog() -> Duration {\n 97|    match std::env::var(\"SPT_ATTACH_GATE_WATCHDOG_MS\") {\n 98|        Ok(ms) => ms\n 99|            .parse::<u64>()\n...\n 107|/// `cold_start_pump` brain in this file — both attachers, both drivers, the\n*108|/// controller_by viewer, the keystroke op, AND the W2 result-gating helpers\n 109|/// (`output_contains_within` / `observe_with_xon` / `spawn_xlate_session`). NOT a\n 110|/// connect deadline: `cold_start_pump`'s socket connect is UNBOUNDED — the `Duration`\n 111|/// arg is stored as `io_timeout` and applied as `call_deadline = now + io_timeout` to\n...\n 126|/// / `got_output` assertions.\n*127|fn attach_ipc_deadline() -> Duration {\n 128|    match std::env::var(\"SPT_ATTACH_IPC_DEADLINE_MS\") {\n 129|        Ok(ms) => ms\n 130|            .parse::<u64>()\n...\n 136|\n*137|fn kill_pid(pid: u32) {\n*138|    #[cfg(windows)]\n 139|    let _ = std::process::Command::new(\"taskkill\")\n 140|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 141|        .output();\n*142|    #[cfg(unix)]\n 143|    let _ = std::process::Command::new(\"kill\")\n 144|        .args([\"-9\", &pid.to_string()])\n 145|        .output();\n### netbroker.rs\n 31|static SEQ: AtomicU32 = AtomicU32::new(0);\n*32|fn unique_name() -> String {\n 33|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 34|    format!(\"spt-daemon-d4a-{}-{}.sock\", std::process::id(), n)\n 35|}\n...\n 38|/// (no real interface touched; no per-test-binary Windows firewall prompt).\n*39|fn hermetic(identity: Identity) -> NetConfig {\n 40|    NetConfig {\n 41|        identity,\n 42|        relay: RelayPolicy::Disabled,\n...\n 54|/// block, giving the deterministic black hole the broker-side bound must cut.\n*55|fn proving(identity: Identity) -> NetConfig {\n 56|    let mut cfg = hermetic(identity);\n 57|    cfg.membership = Some(std::sync::Arc::new(|| {\n 58|        vec![spt_daemon::seedproofx::SubnetCred {\n...\n 60|            seed: vec![7u8; 32],\n*61|            epoch: 1,\n 62|            prev: None,\n 63|        }]\n 64|    }));\n...\n 69|/// for `identity`, with its effect journal in `dir`.\n*70|fn net_broker(name: &str, identity: Identity, dir: &std::path::Path) -> Arc<Broker> {\n 71|    let host = NetHost::start(hermetic(identity)).expect(\"net host start\");\n 72|    let broker =\n 73|        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n...\n 80|\n*81|fn connect_retry(name: &str) -> Brain {\n 82|    for _ in 0..300 {\n 83|        if let Ok(b) = Brain::cold_start(name, 1) {\n 84|            return b;\n...\n 94|// address and an empty conn table.\n*95|#[test]\n*96|fn broker_owns_endpoint_and_reports_status() {\n 97|    let id = Identity::generate();\n 98|    let expect_hex = id.public_key().to_hex();\n 99|    let dir = tempfile::tempdir().expect(\"tempdir\");\n...\n 114|// registers the inbound side — both visible over their own IPC status frames.\n*115|#[test]\n*116|fn brain_dials_second_broker_over_loopback() {\n 117|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 118|    let (name_a, name_b) = (unique_name(), unique_name());\n 119|    let id_b = Identity::generate();\n...\n 148|\n*149|// [int->REQ-HAZARD-RESTART-IDEMPOTENT] an `op_id`-carrying dial is exactly-once\n 150|// across a brain restart: a fresh brain life replaying the same durable id is\n 151|// deduped at the broker (no second QUIC connection — the conn table stays at\n 152|// one) and answered with the connection the first life opened. The broker-owned\n 153|// journal holds the net-namespaced key.\n*154|#[test]\n*155|fn replayed_dial_op_is_deduped_across_brain_restart() {\n 156|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 157|    let (name_a, name_b) = (unique_name(), unique_name());\n 158|    let broker_a = net_broker(&name_a, Identity::generate(), &dir.path().join(\"a\"));\n...\n 193|// holds for the D4a vocabulary too).\n*194|#[test]\n*195|fn unknown_net_frame_is_reported_not_fatal() {\n 196|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 197|    let name = unique_name();\n 198|    let _broker = net_broker(&name, Identity::generate(), dir.path());\n...\n 232|\n*233|// [int->REQ-HAZARD-BROKER-QUIC-DEADLINE] the broker-side QUIC-op bound (the\n*234|// pump-IPC-deadline B-half): a dial to a peer that ACCEPTS the connection but\n 235|// black-holes the seed-proof (a membership-mismatched/dead peer — the exact\n 236|// shape of the 2.2h hfenduleam wedge) must fail PROMPTLY with an ORDINARY broker\n 237|// error reply within the bound, NEVER an unbounded await. Exactly-once is\n...\n 241|// REPLIED an error, it did not wedge.\n*242|#[test]\n*243|fn dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error() {\n 244|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 245|    let (name_a, name_b) = (unique_name(), unique_name());\n 246|\n...\n 301|        0,\n*302|        \"no half-open phantom connection survives the timeout\"\n 303|    );\n 304|\n 305|    // The broker is not wedged: it keeps serving on the same connection.\n\n## spt/\n\n### tests/\n#### attach_wedge_e2e.rs\n*1|//! Wave 2 (v0.12.1) **L2** — REQ-HAZARD-ATTACH-WEDGE robustness gate, on the REAL\n*2|//! dummy-harness fixture + a REAL detached `spt daemon run` (no mocks, no in-proc\n 3|//! `reconcile_once`). Companion to `dummy_harness_e2e.rs` (L0 keystone bring-up).\n 4|//!\n 5|//! The hazard (v0.12.0 real-harness finding): a *legitimately* dead PTY child (real\n*6|//! crash/kill) plus an operator pump that was DROPPED without a clean detach (closed\n 7|//! tab / `taskkill /F`) must NOT wedge the broker — every NEW attach / `endpoint run`\n 8|//! must still be served, the dead endpoint must be offlined within a reconcile tick,\n 9|//! and `daemon stop` must complete bounded.\n...\n 16|//! NOT hold: the loopback duplex is drained broker-INTERNALLY by the operator row's own\n*17|//! read pump (`nethost.rs` `RecvHalf::Loopback`), which for an ordinary attach stream\n 18|//! (`retentive_cap == 0`) never parks — so `peer_w` never backs up on a dead `rc`; a\n 19|//! dead `rc` is merely a dropped IPC subscriber against a bounded (evicting) ring. The\n 20|//! `BROKER-QUIC-DEADLINE` bound (`bounded_block_on`, 10 s) is a further backstop on any\n...\n 37|\n*38|fn kill_pid(pid: u32) {\n*39|    #[cfg(windows)]\n 40|    let _ = Command::new(\"taskkill\")\n 41|        .no_window()\n 42|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 43|        .output();\n*44|    #[cfg(unix)]\n 45|    let _ = Command::new(\"kill\").args([\"-9\", &pid.to_string()]).output();\n 46|}\n 47|\n*48|fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {\n 49|    let (tx, rx) = std::sync::mpsc::channel();\n 50|    std::thread::spawn(move || {\n 51|        let _ = tx.send(cmd.output());\n...\n 57|\n*58|fn ready_pid(path: &Path) -> Option<u32> {\n 59|    let s = std::fs::read_to_string(path).ok()?;\n 60|    serde_json::from_str::<serde_json::Value>(&s)\n 61|        .ok()?\n...\n 66|\n*67|fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {\n 68|    let deadline = Instant::now() + budget;\n 69|    while Instant::now() < deadline {\n 70|        if let Some(p) = ready_pid(path) {\n...\n 77|\n*78|fn sibling_bin(name: &str) -> PathBuf {\n 79|    PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"))\n 80|        .with_file_name(format!(\"{name}{}\", std::env::consts::EXE_SUFFIX))\n 81|}\n...\n 85|/// `ENDPOINT_RUN:… pid=NNN` stderr machine token — the long-lived PTY child).\n*86|fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {\n 87|    let mut cmd = Command::new(spt_bin);\n 88|    cmd.no_window()\n 89|        .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id, \"--start\"])\n...\n 101|/// Poll `<id>`'s self-perch status until it equals `want` or the budget elapses.\n*102|fn wait_for_status(id: &str, want: &str, budget: Duration) -> bool {\n 103|    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\n 104|    let deadline = Instant::now() + budget;\n 105|    while Instant::now() < deadline {\n...\n 116|/// the window — the strong \"this endpoint is being SERVED\" proof. The rc child is then\n*117|/// reaped ABRUPTLY (`kill()` = TerminateProcess, no clean detach record) — exactly the\n 118|/// closed-tab / crashed-operator case (a DROPPED pump), which is what makes this the\n 119|/// L2 wedge trigger when called on the victim. The reader thread observes the killed\n 120|/// pipe (read → 0/Err) and exits.\n*121|fn rc_sees_tick(spt_bin: &Path, home: &Path, id: &str, window: Duration) -> bool {\n 122|    let mut rc: Child = Command::new(spt_bin)\n 123|        .no_window()\n 124|        .args([\"rc\", id])\n...\n 127|        .stderr(Stdio::null())\n*128|        .stdin(Stdio::piped()) // hold stdin open so the pump does not EOF-detach early\n 129|        .spawn()\n 130|        .expect(\"spawn spt rc <id>\");\n 131|    let _rc_stdin = rc.stdin.take();\n...\n 152|    let saw = rx.recv_timeout(window).unwrap_or(false);\n*153|    // Abrupt reap = the dropped-pump wedge trigger (no clean detach). `kill()` is\n 154|    // idempotent on an already-exited child; `wait()` reaps the handle (clippy\n 155|    // zombie_processes).\n 156|    let _ = rc.kill();\n...\n 161|\n*162|#[test]\n*163|fn attach_wedge_dead_child_plus_dropped_pump_does_not_wedge_the_broker() {\n 164|    // ── (1) Isolated, NET-ENABLED SPT_HOME (rc attaches over a broker loopback conn\n 165|    //    that surfaces as net-stream events — needs a net-bound broker). ──\n 166|    let home = tempfile::tempdir().unwrap();\n...\n 232|    let victim_online = wait_for_status(victim, STATUS_ONLINE, Duration::from_secs(20));\n*233|    // rc_sees_tick reaps its rc ABRUPTLY before returning = the dropped-pump half of\n#### resume_no_control_steal_e2e.rs\n*1|//! UPDATE-WEDGE round 2 (REQ-BRAIN-RESUME-NO-CONTROL-STEAL) — the multi-broker-PTY-\n 2|//! session RESPAWN rig (doyle-ruled 2026-07-09). This is the coverage the counter-54\n 3|//! single-black-holed-session A'-rig (`trial_drain_drive_e2e.rs`) LACKED: N REAL broker\n*4|//! PTY sessions, each with its OWN local controller producing output, PRESENT at a real\n 5|//! brain respawn — the exact field condition (7 spt-hosted PTYs at a `fetch --apply`) the\n 6|//! resume-steal bug needed and the single-session rig never exercised.\n 7|//!\n...\n 14|//! `subscribe_with(AttachIntent::Control, by:None)`. The docstring's \"a None identity never\n*15|//! displaces (falls back to viewer)\" is a MYTH for FREE / SAME-LOCAL-IDENTITY slots:\n*16|//! `resolve_subscribe` (broker.rs) stall-evicts first, then `become_controller` when the\n*17|//! slot is free OR the incumbent is also local (`None == None`) — viewer-fallback fires\n*18|//! ONLY against a DIFFERENT *remote* controller. So the respawned brain STOLE the by:None\n*19|//! controller of every free/local-controlled session (incl. the operator's local `spt rc`\n*20|//! and each spt-hosted PTY's spawn-time controller), which the daemon brain never drains\n 21|//! (it hosts no PTY sessions — brainproc.rs) → the session FREEZES the instant the brain takes\n 22|//! over + head-of-line-blocked the shared brain↔broker conn on the N-session replay → every\n 23|//! journaled `rc` retake deadlined (Failure B).\n 24|//!\n*25|//! All N controllers on the field box were LOCAL (by:None — the operator's local `spt rc` +\n*26|//! each PTY's spawn-time controller). The respawn's by:None Control re-attach →\n*27|//! `become_controller` SILENTLY re-takes (same-local-identity `None==None`, the gate-#7 path\n*28|//! that fires NO `Displaced` notice) → the incumbent local controller is ORPHANED: the broker\n 29|//! stops writing its output but does NOT close the conn → no EOF, no `PumpEnd`, no 30s\n*30|//! reconnect/detach splash (that fires only on `BrokerGone`/`Severed`). So the session FREEZES\n*31|//! (output stops entirely, not just control) the instant the brain takes over. The stall-evict\n 32|//! (>15s) is a SEPARATE, LATER, output-dependent symptom: sessions actively producing output\n 33|//! make the stolen writer block >15s → evict (the field's 5); idle sessions never block →\n 34|//! never evict (the field's 2) — but ALL are silently stolen+frozen. The 5-vs-2 split is\n 35|//! ACTIVE-vs-IDLE, not remote-vs-local. This rig asserts the IMMEDIATE, deterministic signal —\n*36|//! a held controller's output-tick freezes on the steal (RED) vs keeps ticking under the Viewer\n*37|//! fix (GREEN) — NOT the flaky, timing-dependent stall-evict.\n 38|//!\n 39|//! ## The FIX and what this rig discriminates\n 40|//!\n*41|//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` no longer\n 42|//! calls `resume_sessions` at all — it uses the cursor-only `resume_session_cursors` (NO\n*43|//! subscribe), which supersedes the Viewer re-attach as the no-steal cure (no attach ⇒ no\n 44|//! steal, a fortiori) AND fixes the SharedSend self-deadlock. This rig's no-steal\n*45|//! discriminator (the operator's `A_i` controllers keep ticking across the swap) therefore\n*46|//! passes for the stronger reason — the daemon brain never attaches. The Viewer text below\n 47|//! describes the round-2 shape this superseded; the assertions are unchanged and still hold.\n 48|//!\n\n### src/\n#### rc.rs\n*1|//! `spt rc <id>` — the operator-side attach pump (M12-W1, REQ-RC-1): attach a\n 2|//! local terminal to a broker-held PTY session.\n 3|//!\n 4|//! **One pump, swappable transport** (the doyle invariant). The attach protocol\n...\n 12|//!\n*13|//! **Detach never kills the session.** The broker owns the PTY; closing our send\n 14|//! side ends the viewport with the session still running (`serve_attach` →\n*15|//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS\n 16|//! attach-lifecycle invariant).\n 17|//!\n 18|//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread\n 19|//! owns it: a stdin reader thread only *produces* raw bytes onto an mpsc channel\n*20|//! (and watches for the detach keybind); the main loop interleaves draining that\n 21|//! channel into `send_attach_input` with `read_event_until` for inbound `Output`.\n*22|// [impl->REQ-RC-1]\n 23|\n 24|use std::io::{Read, Write};\n 25|use std::sync::atomic::{AtomicU64, Ordering};\n...\n 34|\n*35|/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the\n*36|/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one\n 37|/// literal ctrl-b to the session; any other byte after the prefix is sent\n 38|/// through verbatim (prefix + byte). Documented in the CLI help.\n*39|const DETACH_PREFIX: u8 = 0x02; // ctrl-b\n*40|const DETACH_KEY: u8 = b'd';\n 41|\n 42|/// How long the main loop blocks for an inbound event before looping back to\n 43|/// drain stdin — the full-duplex interleave granularity.\n...\n 49|    Bytes(Vec<u8>),\n*50|    /// The operator hit the detach keybind — end the viewport, leave the session.\n*51|    Detach,\n 52|}\n 53|\n 54|/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET\n*55|/// sequences (REQ-RC-MOUSE-FORWARD). On Windows `spt rc` captures the mouse (for\n*56|/// right-click paste, REQ-RC-WIN-PASTE), which means WT forwards scroll-wheel\n 57|/// events to us instead of scrolling its own buffer — so to keep scroll working we\n 58|/// must FORWARD wheel events to the harness, but ONLY when the harness has asked\n 59|/// for mouse reporting (else the bytes are garbage). The pump thread WRITES these\n...\n 73|/// Scans a harness OUTPUT byte stream for the mouse-reporting DECSET set/reset\n*74|/// sequences and updates a [`MouseMode`] (REQ-RC-MOUSE-FORWARD). Pump-local +\n 75|/// single-threaded, so it owns a small `carry` buffer to survive a sequence SPLIT\n 76|/// across two output chunks (the harness's mouse-enable can land across a decoder\n 77|/// boundary). Recognizes `ESC[?<n>[;<n>...]h|l` private-mode sequences; for each\n...\n 92|    /// `mode`, carrying a trailing partial sequence to the next call.\n*93|    fn feed(&mut self, mode: &MouseMode, chunk: &[u8]) {\n 94|        // Fast path: no ESC anywhere and nothing carried ⇒ nothing to do.\n 95|        if self.carry.is_empty() && !chunk.contains(&0x1b) {\n 96|            return;\n...\n 142|/// start of `s`. Pure, so the split-across-chunk behavior unit-tests directly.\n*143|fn parse_decset_private(s: &[u8]) -> DecsetParse {\n 144|    // Need at least `ESC [ ?`.\n 145|    if s.len() < 3 {\n 146|        // Could be the very start of one split across the boundary.\n...\n 189|/// Apply one DECSET private-mode toggle to the tracked [`MouseMode`].\n*190|fn apply_mouse_mode(mode: &MouseMode, param: u16, set: bool) {\n 191|    use std::sync::atomic::Ordering::Release;\n 192|    match param {\n 193|        1000 | 1002 | 1003 => mode.enabled.store(set, Release),\n...\n 198|\n*199|/// Bug #14 (REQ-RC-IDMARKER-DISABLE): ship the top-right endpoint-id status\n 200|/// marker DISABLED. As a one-shot absolutely-positioned paint it scrolls off\n 201|/// screen and is never re-stickied (#14), and its DECSC/clear/SGR injection\n 202|/// splices into the harness's in-flight drawing, leaving residual artifacts on\n...\n 209|\n*210|/// Whether the controller reserved-row identity marker is active for this attach:\n*211|/// only for a controller (a viewer's viewport is read-only) AND only while the\n 212|/// marker is enabled (bug #14 ships it off).\n\nSkipped missing paths: crates/spt-daemon/tests/attach_wedge_e2e.rs"}],"details":{"scopePath":"crates/spt-daemon/tests/attach.rs, crates/spt-daemon/tests/broker.rs, crates/spt-daemon/tests/control_stamp_lifetime.rs, crates/spt-daemon/tests/inject_control_wedge.rs, crates/spt-daemon/tests/netbroker.rs, crates/spt/tests/attach_wedge_e2e.rs, crates/spt/tests/resume_no_control_steal_e2e.rs, crates/spt/src/rc.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":160,"fileCount":8,"files":["crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/broker.rs","crates/spt-daemon/tests/control_stamp_lifetime.rs","crates/spt-daemon/tests/inject_control_wedge.rs","crates/spt-daemon/tests/netbroker.rs","crates/spt/tests/attach_wedge_e2e.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt/src/rc.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/attach.rs","count":20},{"path":"crates/spt-daemon/tests/broker.rs","count":20},{"path":"crates/spt-daemon/tests/control_stamp_lifetime.rs","count":20},{"path":"crates/spt-daemon/tests/inject_control_wedge.rs","count":20},{"path":"crates/spt-daemon/tests/netbroker.rs","count":20},{"path":"crates/spt/tests/attach_wedge_e2e.rs","count":20},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","count":20},{"path":"crates/spt/src/rc.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/tests/\n### attach.rs\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  27│use spt_daemon::attach::{\n *28│    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n *29│    AttachServeOutcome, ControllerIrrecoverablyBehind,\n  30│};\n  31│// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n *32│#[cfg(unix)]\n  33│use spt_daemon::attach::request_attach_endpoint;\n  34│use spt_daemon::brain::{Brain, BrainState, BrokerEvent};\n  35│use spt_daemon::effect::{Minter, MintedOp};\n    │...\n  49│\n *50│fn init_home() {\n  51│    HOME.get_or_init(|| {\n  52│        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n  53│        std::env::set_var(\"SPT_HOME\", dir.path());\n    │...\n  58│static SEQ: AtomicU32 = AtomicU32::new(0);\n *59│fn unique_name() -> String {\n  60│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n  61│    format!(\"spt-daemon-d5b-{}-{}.sock\", std::process::id(), n)\n  62│}\n  63│\n *64│fn hermetic() -> NetConfig {\n  65│    NetConfig {\n  66│        identity: Identity::generate(),\n  67│        relay: RelayPolicy::Disabled,\n    │...\n  73│\n *74│fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n  75│    let host = NetHost::start(hermetic()).expect(\"net host start\");\n  76│    let broker =\n  77│        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n    │...\n  84│\n *85│fn connect_retry(name: &str) -> Brain {\n  86│    for _ in 0..300 {\n  87│        if let Ok(b) = Brain::cold_start(name, 1) {\n  88│            return b;\n    │...\n  94│\n *95│fn echo_spawn_req() -> SpawnReq {\n *96│    #[cfg(unix)]\n  97│    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n *98│    #[cfg(windows)]\n  99│    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n 100│    SpawnReq {\n 101│        program,\n    │...\n 109│        adapter: String::new(),\n*110│        install_dir: None,\n 111│    }\n 112│}\n 113│\n    │...\n 118│/// row and the cross-wired peer row: the serve target is always the peer row.\n*119│fn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n 120│    for _ in 0..400 {\n 121│        let reply = brain.net_streams().expect(\"net-streams\");\n 122│        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {\n    │...\n 140│/// origin), drive `marker` through the PTY and render it back over the wire,\n*141│/// then detach and confirm the serve loop ended `Detached` (session outlives).\n 142│/// Returns the rendered viewport bytes + the (still-live) target brain so the\n 143│/// caller can assert session survival and clean up.\n 144│#[allow(clippy::too_many_arguments)]\n*145│fn attach_drive_detach(\n 146│    operator: &mut Brain,\n 147│    target_broker: &str,\n 148│    mut target: Brain,\n    │...\n 191│\n*192│    // Detach: the viewport ends with the session still running.\n 193│    operator\n 194│        .net_stream_send(stream_b, &[], None, true)\n*195│        .expect(\"detach\");\n 196│    let (outcome, target) = server.join().expect(\"serve thread\");\n*197│    assert_eq!(outcome, AttachServeOutcome::Detached);\n 198│    (viewport, target)\n 199│}\n 200│\n    │...\n 203│/// the rendered-through cursor (last accepted seq + 1).\n*204│fn render_until(\n 205│    brain: &mut Brain,\n 206│    stream_id: u64,\n 207│    decoder: &mut AttachDecoder,\n### broker.rs\n  37│static SEQ: AtomicU32 = AtomicU32::new(0);\n *38│fn unique_name() -> String {\n  39│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n  40│    format!(\"spt-daemon-b1-{}-{}.sock\", std::process::id(), n)\n  41│}\n    │...\n  43│/// The echo child: reads stdin, writes each line to stdout.\n *44│fn echo_spawn_req() -> SpawnReq {\n *45│    #[cfg(unix)]\n  46│    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n *47│    #[cfg(windows)]\n  48│    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n  49│    SpawnReq {\n  50│        program,\n    │...\n  58│        adapter: String::new(),\n *59│        install_dir: None,\n  60│    }\n  61│}\n  62│\n *63│fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n  64│    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n  65│}\n  66│\n *67│fn main_test() {\n  68│    let name = unique_name();\n  69│    let broker = Broker::bind(&name).expect(\"bind broker\");\n  70│\n    │...\n  91│    // always pushes `spawned`/`output`/`exit` frames, so the blocking reads\n *92│    // below return; a genuine stall is caught by the test runner's own timeout.)\n  93│\n  94│    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n  95│\n    │...\n 175│\n*176│fn accumulate_output(frame: &Envelope, out: &mut Vec<u8>) {\n 177│    let ev: OutputEvent = serde_json::from_value(frame.payload.clone()).unwrap();\n 178│    out.extend_from_slice(&decode_bytes(&ev.data_b64).unwrap());\n 179│}\n 180│\n*181│#[test]\n*182│fn broker_hosts_pty_child_streams_output_and_accepts_input() {\n 183│    main_test();\n 184│}\n 185│\n    │...\n 197│// [int->REQ-MSG-IDLE-TRANSLATION-BINARY]\n*198│// [int->REQ-HAZARD-IDLE-SILENT-NONDELIVERY]\n*199│fn endpoint_input_test() {\n 200│    let name = unique_name();\n 201│    let broker = Broker::bind(&name).expect(\"bind broker\");\n 202│    let server_broker = Arc::clone(&broker);\n    │...\n 311│\n*312│#[test]\n*313│fn endpoint_keyed_inject_without_binary_spools_not_pty() {\n 314│    endpoint_input_test();\n 315│}\n 316│\n*317│/// REQ-HAZARD-ENV-SUBST: a SpawnReq's `env` map reaches the spawned PTY child's\n 318│/// environment — the broker leg of the F-013 fix (a flagless harness gets\n 319│/// SPT_ENDPOINT_ID populated, instead of an empty value → seed-by-PPID → zero\n 320│/// perch). Spawn a child that ECHOES the injected env var; its output must carry\n 321│/// the value.\n*322│// [int->REQ-HAZARD-ENV-SUBST]\n*323│fn env_inject_test() {\n 324│    let name = unique_name();\n 325│    let broker = Broker::bind(&name).expect(\"bind broker\");\n 326│    let server_broker = Arc::clone(&broker);\n    │...\n 344│    // A child that prints the injected env var, cross-OS.\n*345│    #[cfg(unix)]\n 346│    let (program, args) = (\n 347│        \"sh\".to_string(),\n 348│        vec![\"-c\".to_string(), \"echo ENVCHECK=$SPT_ENDPOINT_ID\".to_string()],\n 349│    );\n*350│    #[cfg(windows)]\n 351│    let (program, args) = (\n 352│        \"cmd\".to_string(),\n 353│        vec![\"/c\".to_string(), \"echo ENVCHECK=%SPT_ENDPOINT_ID%\".to_string()],\n### control_stamp_lifetime.rs\n  *1│//! F-026 W1 #2 (REQ-HAZARD-CONTROL-STAMP-LIFETIME) — a control/viewer stamp never\n   2│//! outlives its session, on a REAL broker over the real local IPC (no PTY/transport\n   3│//! mocks). Red-first: the exit-waiter used to `sessions.remove(&id)` with NO stamp\n  *4│//! clear, so `/exit` (which kills the CHILD, not the controller conn) left\n  *5│//! `controlled` / `driven_by` / `viewer_count` latched in info.json forever — and\n  *6│//! gossiped a stale `controller_node` cross-node.\n   7│//!\n   8│//! ## The bug this gate locks down\n   9│//!\n    │...\n  11│//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n *12│//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n *13│//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n *14│//! path dropped the OutputLog with the controller slot still occupied, so the\n  15│//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\n  16│//! hall-a's info.json still read `controlled:true` and hfenduleam still gossiped\n *17│//! `controller_node=self`.\n  18│//!\n  19│//! ## What the fix does\n  20│//!\n    │...\n  22│//! three stamps (`set_driven_by(None)`, `set_controlled(false)`,\n *23│//! `set_viewer_count(0)`) via the known endpoint id. The broker stays the single\n *24│//! writer; the session is dead so no live controller re-stamps concurrently (the\n  25│//! same race-free argument as the W5 no-session self-heal).\n  26│//!\n  27│//! ## Why kill the CHILD (not drop the conn)\n  28│//!\n *29│//! Dropping the spawner conn would run the EXISTING `clear_controller` path and\n  30│//! prove nothing about the reap leg. Killing the PTY child fires the exit-waiter\n *31│//! while the controller conn stays open — isolating the reap clear as the ONLY\n  32│//! path that can unlatch the stamps.\n  33│//!\n  34│//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n    │...\n  55│static SEQ: AtomicU32 = AtomicU32::new(0);\n *56│fn unique_name() -> String {\n  57│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n  58│    format!(\"spt-daemon-ctlstamp-{}-{}.sock\", std::process::id(), n)\n  59│}\n    │...\n  61│static STAMP_HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n *62│fn init_stamp_home() {\n  63│    STAMP_HOME.get_or_init(|| {\n  64│        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n  65│        std::env::set_var(\"SPT_HOME\", dir.path());\n    │...\n  69│\n *70│fn kill_pid(pid: u32) {\n *71│    #[cfg(windows)]\n  72│    let _ = std::process::Command::new(\"taskkill\")\n  73│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n  74│        .output();\n *75│    #[cfg(unix)]\n  76│    let _ = std::process::Command::new(\"kill\")\n  77│        .args([\"-9\", &pid.to_string()])\n  78│        .output();\n    │...\n  80│\n *81│fn start_broker(name: &str) -> Arc<Broker> {\n  82│    let broker = Broker::bind(name).expect(\"bind broker\");\n  83│    let serve = Arc::clone(&broker);\n  84│    thread::spawn(move || {\n    │...\n  89│\n *90│fn connect_brain(name: &str) -> Brain {\n  91│    for _ in 0..300 {\n  92│        if let Ok(b) = Brain::cold_start(name, 1) {\n  93│            return b;\n    │...\n 101│/// kill it (cross-OS).\n*102│fn idle_spawn_req(endpoint: &str) -> SpawnReq {\n### inject_control_wedge.rs\n  *1│//! v0.13.0 W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) — the CONTROLLER-DELIVERY\n   2│//! non-wedge gate, on a REAL broker hosting a REAL PTY child over the real local\n   3│//! IPC (no mocks of the PTY or the transport).\n   4│//!\n  *5│//! ## The bug this gate locks down (park (a), the output-stall / wedge root)\n   6│//!\n   7│//! PRE-FIX (v0.12.x), [`OutputLog::append`] fanned each output chunk to the\n  *8│//! CONTROLLER via a SYNCHRONOUS, BLOCKING `write_frame` held INLINE on the\n   9│//! session's single drain thread, while the `Mutex<OutputLog>` was locked.\n *10│//! VIEWERS instead get a dedicated writer thread + a bounded, evicting\n *11│//! `sync_channel` and can never stall the drain. So a slow / non-draining\n *12│//! controller (here a controller connection that stops reading while the child\n  13│//! floods) parked the drain INSIDE `append` STILL HOLDING the log lock — output\n *14│//! stalled and any path needing that log lock (a fresh attach, or the global\n  15│//! `KIND_SESSIONS` enumeration, which reads each log's `delivered_through`)\n  16│//! blocked FOREVER, while the broker process stayed alive. The repro that proved\n  17│//! this (captured 2026-06-19): `APPEND_CTRL_ENTER sid=1 seq=2` with no matching\n    │...\n  19│//!\n *20│//! POST-FIX (W1): controller delivery moved off the drain thread onto a dedicated\n *21│//! writer thread + bounded channel (the viewer_writer pattern, but authoritative:\n  22│//! the writer advances `delivered_through`). The drain hands each chunk off with a\n *23│//! bounded `send_timeout` OFF the log lock; a fully-stalled controller is detached\n *24│//! after `CONTROLLER_WRITE_DEADLINE`. So even while the controller is fully backed\n  25│//! up, the session does NOT wedge — `KIND_SESSIONS` still answers. THIS test\n  26│//! asserts that fixed behavior.\n  27│//!\n    │...\n  56│\n *57│// ── F-023 leg 1 (REQ-WAN-SPT-HOSTED-DELIVERY / REQ-HAZARD-DELIVERY-STARVATION):\n  58│//    the WAN-ingress inject leg. `receive_wan` funnels a cross-node `spt send`;\n  59│//    the shared spt-hosted inject leg must reach the broker at\n  60│//    `broker_socket_name()` (the in-daemon dispatcher's socket) and drive the\n    │...\n  65│\n *66│// ── W1b additions (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE): the real-fixture attach\n  67│//    path, the Brain client API, and a net-backed broker for loopback attach. ──\n  68│use std::sync::atomic::{AtomicBool, AtomicU64};\n  69│use std::sync::OnceLock;\n    │...\n  80│static SEQ: AtomicU32 = AtomicU32::new(0);\n *81│fn unique_name() -> String {\n  82│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n  83│    format!(\"spt-daemon-w1wedge-{}-{}.sock\", std::process::id(), n)\n  84│}\n    │...\n  95│/// wall-clock ONLY; it never relaxes the `subscribed` / `got_output` assertions.\n *96│fn attach_gate_watchdog() -> Duration {\n  97│    match std::env::var(\"SPT_ATTACH_GATE_WATCHDOG_MS\") {\n  98│        Ok(ms) => ms\n  99│            .parse::<u64>()\n    │...\n 107│/// `cold_start_pump` brain in this file — both attachers, both drivers, the\n*108│/// controller_by viewer, the keystroke op, AND the W2 result-gating helpers\n 109│/// (`output_contains_within` / `observe_with_xon` / `spawn_xlate_session`). NOT a\n 110│/// connect deadline: `cold_start_pump`'s socket connect is UNBOUNDED — the `Duration`\n 111│/// arg is stored as `io_timeout` and applied as `call_deadline = now + io_timeout` to\n    │...\n 126│/// / `got_output` assertions.\n*127│fn attach_ipc_deadline() -> Duration {\n 128│    match std::env::var(\"SPT_ATTACH_IPC_DEADLINE_MS\") {\n 129│        Ok(ms) => ms\n 130│            .parse::<u64>()\n    │...\n 136│\n*137│fn kill_pid(pid: u32) {\n*138│    #[cfg(windows)]\n 139│    let _ = std::process::Command::new(\"taskkill\")\n 140│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 141│        .output();\n*142│    #[cfg(unix)]\n 143│    let _ = std::process::Command::new(\"kill\")\n 144│        .args([\"-9\", &pid.to_string()])\n 145│        .output();\n### netbroker.rs\n  31│static SEQ: AtomicU32 = AtomicU32::new(0);\n *32│fn unique_name() -> String {\n  33│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n  34│    format!(\"spt-daemon-d4a-{}-{}.sock\", std::process::id(), n)\n  35│}\n    │...\n  38│/// (no real interface touched; no per-test-binary Windows firewall prompt).\n *39│fn hermetic(identity: Identity) -> NetConfig {\n  40│    NetConfig {\n  41│        identity,\n  42│        relay: RelayPolicy::Disabled,\n    │...\n  54│/// block, giving the deterministic black hole the broker-side bound must cut.\n *55│fn proving(identity: Identity) -> NetConfig {\n  56│    let mut cfg = hermetic(identity);\n  57│    cfg.membership = Some(std::sync::Arc::new(|| {\n  58│        vec![spt_daemon::seedproofx::SubnetCred {\n    │...\n  60│            seed: vec![7u8; 32],\n *61│            epoch: 1,\n  62│            prev: None,\n  63│        }]\n  64│    }));\n    │...\n  69│/// for `identity`, with its effect journal in `dir`.\n *70│fn net_broker(name: &str, identity: Identity, dir: &std::path::Path) -> Arc<Broker> {\n  71│    let host = NetHost::start(hermetic(identity)).expect(\"net host start\");\n  72│    let broker =\n  73│        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n    │...\n  80│\n *81│fn connect_retry(name: &str) -> Brain {\n  82│    for _ in 0..300 {\n  83│        if let Ok(b) = Brain::cold_start(name, 1) {\n  84│            return b;\n    │...\n  94│// address and an empty conn table.\n *95│#[test]\n *96│fn broker_owns_endpoint_and_reports_status() {\n  97│    let id = Identity::generate();\n  98│    let expect_hex = id.public_key().to_hex();\n  99│    let dir = tempfile::tempdir().expect(\"tempdir\");\n    │...\n 114│// registers the inbound side — both visible over their own IPC status frames.\n*115│#[test]\n*116│fn brain_dials_second_broker_over_loopback() {\n 117│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 118│    let (name_a, name_b) = (unique_name(), unique_name());\n 119│    let id_b = Identity::generate();\n    │...\n 148│\n*149│// [int->REQ-HAZARD-RESTART-IDEMPOTENT] an `op_id`-carrying dial is exactly-once\n 150│// across a brain restart: a fresh brain life replaying the same durable id is\n 151│// deduped at the broker (no second QUIC connection — the conn table stays at\n 152│// one) and answered with the connection the first life opened. The broker-owned\n 153│// journal holds the net-namespaced key.\n*154│#[test]\n*155│fn replayed_dial_op_is_deduped_across_brain_restart() {\n 156│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 157│    let (name_a, name_b) = (unique_name(), unique_name());\n 158│    let broker_a = net_broker(&name_a, Identity::generate(), &dir.path().join(\"a\"));\n    │...\n 193│// holds for the D4a vocabulary too).\n*194│#[test]\n*195│fn unknown_net_frame_is_reported_not_fatal() {\n 196│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 197│    let name = unique_name();\n 198│    let _broker = net_broker(&name, Identity::generate(), dir.path());\n    │...\n 232│\n*233│// [int->REQ-HAZARD-BROKER-QUIC-DEADLINE] the broker-side QUIC-op bound (the\n*234│// pump-IPC-deadline B-half): a dial to a peer that ACCEPTS the connection but\n 235│// black-holes the seed-proof (a membership-mismatched/dead peer — the exact\n 236│// shape of the 2.2h hfenduleam wedge) must fail PROMPTLY with an ORDINARY broker\n 237│// error reply within the bound, NEVER an unbounded await. Exactly-once is\n    │...\n 241│// REPLIED an error, it did not wedge.\n*242│#[test]\n*243│fn dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error() {\n 244│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 245│    let (name_a, name_b) = (unique_name(), unique_name());\n 246│\n    │...\n 301│        0,\n*302│        \"no half-open phantom connection survives the timeout\"\n 303│    );\n 304│\n 305│    // The broker is not wedged: it keeps serving on the same connection.\n\n## spt/\n\n### tests/\n#### attach_wedge_e2e.rs\n  *1│//! Wave 2 (v0.12.1) **L2** — REQ-HAZARD-ATTACH-WEDGE robustness gate, on the REAL\n  *2│//! dummy-harness fixture + a REAL detached `spt daemon run` (no mocks, no in-proc\n   3│//! `reconcile_once`). Companion to `dummy_harness_e2e.rs` (L0 keystone bring-up).\n   4│//!\n   5│//! The hazard (v0.12.0 real-harness finding): a *legitimately* dead PTY child (real\n  *6│//! crash/kill) plus an operator pump that was DROPPED without a clean detach (closed\n   7│//! tab / `taskkill /F`) must NOT wedge the broker — every NEW attach / `endpoint run`\n   8│//! must still be served, the dead endpoint must be offlined within a reconcile tick,\n   9│//! and `daemon stop` must complete bounded.\n    │...\n  16│//! NOT hold: the loopback duplex is drained broker-INTERNALLY by the operator row's own\n *17│//! read pump (`nethost.rs` `RecvHalf::Loopback`), which for an ordinary attach stream\n  18│//! (`retentive_cap == 0`) never parks — so `peer_w` never backs up on a dead `rc`; a\n  19│//! dead `rc` is merely a dropped IPC subscriber against a bounded (evicting) ring. The\n  20│//! `BROKER-QUIC-DEADLINE` bound (`bounded_block_on`, 10 s) is a further backstop on any\n    │...\n  37│\n *38│fn kill_pid(pid: u32) {\n *39│    #[cfg(windows)]\n  40│    let _ = Command::new(\"taskkill\")\n  41│        .no_window()\n  42│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n  43│        .output();\n *44│    #[cfg(unix)]\n  45│    let _ = Command::new(\"kill\").args([\"-9\", &pid.to_string()]).output();\n  46│}\n  47│\n *48│fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {\n  49│    let (tx, rx) = std::sync::mpsc::channel();\n  50│    std::thread::spawn(move || {\n  51│        let _ = tx.send(cmd.output());\n    │...\n  57│\n *58│fn ready_pid(path: &Path) -> Option<u32> {\n  59│    let s = std::fs::read_to_string(path).ok()?;\n  60│    serde_json::from_str::<serde_json::Value>(&s)\n  61│        .ok()?\n    │...\n  66│\n *67│fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {\n  68│    let deadline = Instant::now() + budget;\n  69│    while Instant::now() < deadline {\n  70│        if let Some(p) = ready_pid(path) {\n    │...\n  77│\n *78│fn sibling_bin(name: &str) -> PathBuf {\n  79│    PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"))\n  80│        .with_file_name(format!(\"{name}{}\", std::env::consts::EXE_SUFFIX))\n  81│}\n    │...\n  85│/// `ENDPOINT_RUN:… pid=NNN` stderr machine token — the long-lived PTY child).\n *86│fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {\n  87│    let mut cmd = Command::new(spt_bin);\n  88│    cmd.no_window()\n  89│        .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id, \"--start\"])\n    │...\n 101│/// Poll `<id>`'s self-perch status until it equals `want` or the budget elapses.\n*102│fn wait_for_status(id: &str, want: &str, budget: Duration) -> bool {\n 103│    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\n 104│    let deadline = Instant::now() + budget;\n 105│    while Instant::now() < deadline {\n    │...\n 116│/// the window — the strong \"this endpoint is being SERVED\" proof. The rc child is then\n*117│/// reaped ABRUPTLY (`kill()` = TerminateProcess, no clean detach record) — exactly the\n 118│/// closed-tab / crashed-operator case (a DROPPED pump), which is what makes this the\n 119│/// L2 wedge trigger when called on the victim. The reader thread observes the killed\n 120│/// pipe (read → 0/Err) and exits.\n*121│fn rc_sees_tick(spt_bin: &Path, home: &Path, id: &str, window: Duration) -> bool {\n 122│    let mut rc: Child = Command::new(spt_bin)\n 123│        .no_window()\n 124│        .args([\"rc\", id])\n    │...\n 127│        .stderr(Stdio::null())\n*128│        .stdin(Stdio::piped()) // hold stdin open so the pump does not EOF-detach early\n 129│        .spawn()\n 130│        .expect(\"spawn spt rc <id>\");\n 131│    let _rc_stdin = rc.stdin.take();\n    │...\n 152│    let saw = rx.recv_timeout(window).unwrap_or(false);\n*153│    // Abrupt reap = the dropped-pump wedge trigger (no clean detach). `kill()` is\n 154│    // idempotent on an already-exited child; `wait()` reaps the handle (clippy\n 155│    // zombie_processes).\n 156│    let _ = rc.kill();\n    │...\n 161│\n*162│#[test]\n*163│fn attach_wedge_dead_child_plus_dropped_pump_does_not_wedge_the_broker() {\n 164│    // ── (1) Isolated, NET-ENABLED SPT_HOME (rc attaches over a broker loopback conn\n 165│    //    that surfaces as net-stream events — needs a net-bound broker). ──\n 166│    let home = tempfile::tempdir().unwrap();\n    │...\n 232│    let victim_online = wait_for_status(victim, STATUS_ONLINE, Duration::from_secs(20));\n*233│    // rc_sees_tick reaps its rc ABRUPTLY before returning = the dropped-pump half of\n#### resume_no_control_steal_e2e.rs\n *1│//! UPDATE-WEDGE round 2 (REQ-BRAIN-RESUME-NO-CONTROL-STEAL) — the multi-broker-PTY-\n  2│//! session RESPAWN rig (doyle-ruled 2026-07-09). This is the coverage the counter-54\n  3│//! single-black-holed-session A'-rig (`trial_drain_drive_e2e.rs`) LACKED: N REAL broker\n *4│//! PTY sessions, each with its OWN local controller producing output, PRESENT at a real\n  5│//! brain respawn — the exact field condition (7 spt-hosted PTYs at a `fetch --apply`) the\n  6│//! resume-steal bug needed and the single-session rig never exercised.\n  7│//!\n   │...\n 14│//! `subscribe_with(AttachIntent::Control, by:None)`. The docstring's \"a None identity never\n*15│//! displaces (falls back to viewer)\" is a MYTH for FREE / SAME-LOCAL-IDENTITY slots:\n*16│//! `resolve_subscribe` (broker.rs) stall-evicts first, then `become_controller` when the\n*17│//! slot is free OR the incumbent is also local (`None == None`) — viewer-fallback fires\n*18│//! ONLY against a DIFFERENT *remote* controller. So the respawned brain STOLE the by:None\n*19│//! controller of every free/local-controlled session (incl. the operator's local `spt rc`\n*20│//! and each spt-hosted PTY's spawn-time controller), which the daemon brain never drains\n 21│//! (it hosts no PTY sessions — brainproc.rs) → the session FREEZES the instant the brain takes\n 22│//! over + head-of-line-blocked the shared brain↔broker conn on the N-session replay → every\n 23│//! journaled `rc` retake deadlined (Failure B).\n 24│//!\n*25│//! All N controllers on the field box were LOCAL (by:None — the operator's local `spt rc` +\n*26│//! each PTY's spawn-time controller). The respawn's by:None Control re-attach →\n*27│//! `become_controller` SILENTLY re-takes (same-local-identity `None==None`, the gate-#7 path\n*28│//! that fires NO `Displaced` notice) → the incumbent local controller is ORPHANED: the broker\n 29│//! stops writing its output but does NOT close the conn → no EOF, no `PumpEnd`, no 30s\n*30│//! reconnect/detach splash (that fires only on `BrokerGone`/`Severed`). So the session FREEZES\n*31│//! (output stops entirely, not just control) the instant the brain takes over. The stall-evict\n 32│//! (>15s) is a SEPARATE, LATER, output-dependent symptom: sessions actively producing output\n 33│//! make the stolen writer block >15s → evict (the field's 5); idle sessions never block →\n 34│//! never evict (the field's 2) — but ALL are silently stolen+frozen. The 5-vs-2 split is\n 35│//! ACTIVE-vs-IDLE, not remote-vs-local. This rig asserts the IMMEDIATE, deterministic signal —\n*36│//! a held controller's output-tick freezes on the steal (RED) vs keeps ticking under the Viewer\n*37│//! fix (GREEN) — NOT the flaky, timing-dependent stall-evict.\n 38│//!\n 39│//! ## The FIX and what this rig discriminates\n 40│//!\n*41│//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` no longer\n 42│//! calls `resume_sessions` at all — it uses the cursor-only `resume_session_cursors` (NO\n*43│//! subscribe), which supersedes the Viewer re-attach as the no-steal cure (no attach ⇒ no\n 44│//! steal, a fortiori) AND fixes the SharedSend self-deadlock. This rig's no-steal\n*45│//! discriminator (the operator's `A_i` controllers keep ticking across the swap) therefore\n*46│//! passes for the stronger reason — the daemon brain never attaches. The Viewer text below\n 47│//! describes the round-2 shape this superseded; the assertions are unchanged and still hold.\n 48│//!\n\n### src/\n#### rc.rs\n  *1│//! `spt rc <id>` — the operator-side attach pump (M12-W1, REQ-RC-1): attach a\n   2│//! local terminal to a broker-held PTY session.\n   3│//!\n   4│//! **One pump, swappable transport** (the doyle invariant). The attach protocol\n    │...\n  12│//!\n *13│//! **Detach never kills the session.** The broker owns the PTY; closing our send\n  14│//! side ends the viewport with the session still running (`serve_attach` →\n *15│//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS\n  16│//! attach-lifecycle invariant).\n  17│//!\n  18│//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread\n  19│//! owns it: a stdin reader thread only *produces* raw bytes onto an mpsc channel\n *20│//! (and watches for the detach keybind); the main loop interleaves draining that\n  21│//! channel into `send_attach_input` with `read_event_until` for inbound `Output`.\n *22│// [impl->REQ-RC-1]\n  23│\n  24│use std::io::{Read, Write};\n  25│use std::sync::atomic::{AtomicU64, Ordering};\n    │...\n  34│\n *35│/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the\n *36│/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one\n  37│/// literal ctrl-b to the session; any other byte after the prefix is sent\n  38│/// through verbatim (prefix + byte). Documented in the CLI help.\n *39│const DETACH_PREFIX: u8 = 0x02; // ctrl-b\n *40│const DETACH_KEY: u8 = b'd';\n  41│\n  42│/// How long the main loop blocks for an inbound event before looping back to\n  43│/// drain stdin — the full-duplex interleave granularity.\n    │...\n  49│    Bytes(Vec<u8>),\n *50│    /// The operator hit the detach keybind — end the viewport, leave the session.\n *51│    Detach,\n  52│}\n  53│\n  54│/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET\n *55│/// sequences (REQ-RC-MOUSE-FORWARD). On Windows `spt rc` captures the mouse (for\n *56│/// right-click paste, REQ-RC-WIN-PASTE), which means WT forwards scroll-wheel\n  57│/// events to us instead of scrolling its own buffer — so to keep scroll working we\n  58│/// must FORWARD wheel events to the harness, but ONLY when the harness has asked\n  59│/// for mouse reporting (else the bytes are garbage). The pump thread WRITES these\n    │...\n  73│/// Scans a harness OUTPUT byte stream for the mouse-reporting DECSET set/reset\n *74│/// sequences and updates a [`MouseMode`] (REQ-RC-MOUSE-FORWARD). Pump-local +\n  75│/// single-threaded, so it owns a small `carry` buffer to survive a sequence SPLIT\n  76│/// across two output chunks (the harness's mouse-enable can land across a decoder\n  77│/// boundary). Recognizes `ESC[?<n>[;<n>...]h|l` private-mode sequences; for each\n    │...\n  92│    /// `mode`, carrying a trailing partial sequence to the next call.\n *93│    fn feed(&mut self, mode: &MouseMode, chunk: &[u8]) {\n  94│        // Fast path: no ESC anywhere and nothing carried ⇒ nothing to do.\n  95│        if self.carry.is_empty() && !chunk.contains(&0x1b) {\n  96│            return;\n    │...\n 142│/// start of `s`. Pure, so the split-across-chunk behavior unit-tests directly.\n*143│fn parse_decset_private(s: &[u8]) -> DecsetParse {\n 144│    // Need at least `ESC [ ?`.\n 145│    if s.len() < 3 {\n 146│        // Could be the very start of one split across the boundary.\n    │...\n 189│/// Apply one DECSET private-mode toggle to the tracked [`MouseMode`].\n*190│fn apply_mouse_mode(mode: &MouseMode, param: u16, set: bool) {\n 191│    use std::sync::atomic::Ordering::Release;\n 192│    match param {\n 193│        1000 | 1002 | 1003 => mode.enabled.store(set, Release),\n    │...\n 198│\n*199│/// Bug #14 (REQ-RC-IDMARKER-DISABLE): ship the top-right endpoint-id status\n 200│/// marker DISABLED. As a one-shot absolutely-positioned paint it scrolls off\n 201│/// screen and is never re-stickied (#14), and its DECSC/clear/SGR injection\n 202│/// splices into the harness's in-flight drawing, leaving residual artifacts on\n    │...\n 209│\n*210│/// Whether the controller reserved-row identity marker is active for this attach:\n*211│/// only for a controller (a viewer's viewport is read-only) AND only while the\n 212│/// marker is enabled (bug #14 ships it off).","missingPaths":["crates/spt-daemon/tests/attach_wedge_e2e.rs"]},"isError":false,"timestamp":1784195780518}}
{"type":"message","id":"9a8cd3b0","parentId":"51689ab9","timestamp":"2026-07-16T09:56:29.125Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Requesting specific code sections**"},{"type":"toolCall","id":"call_bWO5zCI0h99bdO0X6D7WADi8|fc_008390f508d1d09b016a58aacd129c819abaaddb55cd5c9e56","name":"read","arguments":{"i":"Reading output log transitions","path":"crates/spt-daemon/src/broker.rs","selector":"620-970"}},{"type":"toolCall","id":"call_wnh9DQyJ7v3rm9pmBRXHgFgG|fc_008390f508d1d09b016a58aacd12ac819aaf3de57a5d08f61b","name":"read","arguments":{"i":"Reading sink writer tasks","path":"crates/spt-daemon/src/broker.rs","selector":"1420-1605"}},{"type":"toolCall","id":"call_ZAu1tShC9pWj3aGICZPob6ee|fc_008390f508d1d09b016a58aacd12b4819abd2dce6db56b7706","name":"read","arguments":{"i":"Reading connection cleanup path","path":"crates/spt-daemon/src/broker.rs","selector":"2960-3220"}},{"type":"toolCall","id":"call_JG0LmRf32ILv5dtUUpU30Sqi|fc_008390f508d1d09b016a58aacd12b8819aad53e758be42c469","name":"read","arguments":{"i":"Reading subscribe dispatch path","path":"crates/spt-daemon/src/broker.rs","selector":"3425-3510"}},{"type":"toolCall","id":"call_5J5UdBqxO88nvrwByIiw10Wh|fc_008390f508d1d09b016a58aacd12c8819a97178412350b722b","name":"read","arguments":{"i":"Reading current wedge hazard rules","path":"docs/KNOWN-HAZARDS.md","selector":"408-460,480-515,556-590,604-660"}},{"type":"toolCall","id":"call_H0aFWGgrBn2X2bp2gsSzJjFB|fc_008390f508d1d09b016a58aacd12d0819aaedda88c0a7081b7","name":"read","arguments":{"i":"Reading attach restart tests","path":"crates/spt-daemon/tests/attach.rs","selector":"1-230,760-1035"}},{"type":"toolCall","id":"call_0v4wgXCf3bUcx09HbxN656Dy|fc_008390f508d1d09b016a58aacd12d8819ab08b1b7689188663","name":"read","arguments":{"i":"Reading brain resume wedge test","path":"crates/spt/tests/resume_no_control_steal_e2e.rs","selector":"1-80,430-500"}},{"type":"toolCall","id":"call_9fozOaCtfEa0DzFMGad6KoUg|fc_008390f508d1d09b016a58aacd12dc819a89f894d5d984cfbc","name":"read","arguments":{"i":"Reading reconnection test names","path":"crates/spt/src/rc.rs","selector":"2700-3214"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":32994,"output":371,"cacheRead":119296,"cacheWrite":0,"totalTokens":152661,"reasoningTokens":35,"cost":{"input":0.16497,"output":0.011130000000000001,"cacheRead":0.059648,"cacheWrite":0,"total":0.235748}},"stopReason":"toolUse","timestamp":1784195780535,"responseId":"resp_008390f508d1d09b016a58aac4dab0819a84c5b63d3dda5f5b","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_008390f508d1d09b016a58aac66380819aa2767b99dc160e9f","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKrND_23YFssJ2UcfashXNJFxp3qABAy0J3qvv283vcSXuW14qPv9mhxiB-nJDPYX9cqw88pjGpVtIJ8UFRW8agEgqYqfChW2aHOgQwgg9pRMxxcA-XUBik-V8HbQQWcpk0Df2BMcHzYXkkRzBV4cih092yYGMYCMZlsuBGGpE5xQt5A5yy8GA2YUnCK1yjrZAiIleQT5BMcrmogMI1xOB3blol6zl4KywHiaA-KmY6EHJpguxoJZOG1GN1hkHfAfIggK55wEQaWdZqdjIqClQQWsEQGJp_vZ8Y4CTesUi76YaheykB28EQlIw5tF470y00JuFknYtX87SAnX4PNkw4kraMRnC2Xojrrtet5-xIFBV5JpCiFqSJqRoHoCZFQnj3ly26az_etQyLT4gFfmwbwRSCJmNddVg6d4k89XoB6liCwsFS3wSsEsJYdBWKU7XBf1korD74GWFVyJMTkxdb2WeN1Al5zkDrPIQa1MDQVkdchOmyhksx_ojTiPjrdBfistn5JuEPdHcklwUZY210DHOxLgxc7SOvrFQY2ZMWA2x0hadS4Dw19sNUYiW3dfHnIx9kvX3audVkzpD0bsQ44B8bZYbBJT7fHdUaKfp_e23HnXqSX3TMGMxPOj5BLhC2tynGOkzBRMJsgYJAifTsuw4y1MdikGGHXrf6pP3nibk77NJa5SIOpMApFvFN5lZsRc2XnsRMiwNup2n_tS9S-5wvkj1gsO2K-9kejKSwyuWPuPyDjWIHzsdpValDd9Ruvj40o2w_TG1QP8ts5rGDKyfAU9trpwCw_blC2QQ7gPa66M74fLdB2xx2RkBzcHC8QuyVwiLkjwn99vD406S787zXrEEKr7LwX-6tc_3olsnebQCy3fW9WsHQAjdDudg_QMqSOeAb_qiUfLyxkNnkyd7ogWTNWQfHKMlESiCFuKTCGZStWSB5BeoK3hPT-bnibQ447K9rqAwuNu3sBHtRTTc1j_w1UUV1zNuHL3VlnCz4_xSfCKqchq67_BiIj2ho7","summary":[{"type":"summary_text","text":"**Requesting specific code sections**"}]},{"id":"fc_008390f508d1d09b016a58aacd129c819abaaddb55cd5c9e56","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading output log transitions\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"620-970\"}","call_id":"call_bWO5zCI0h99bdO0X6D7WADi8","name":"read"},{"id":"fc_008390f508d1d09b016a58aacd12ac819aaf3de57a5d08f61b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading sink writer tasks\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"1420-1605\"}","call_id":"call_wnh9DQyJ7v3rm9pmBRXHgFgG","name":"read"},{"id":"fc_008390f508d1d09b016a58aacd12b4819abd2dce6db56b7706","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading connection cleanup path\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"2960-3220\"}","call_id":"call_ZAu1tShC9pWj3aGICZPob6ee","name":"read"},{"id":"fc_008390f508d1d09b016a58aacd12b8819aad53e758be42c469","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading subscribe dispatch path\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"3425-3510\"}","call_id":"call_JG0LmRf32ILv5dtUUpU30Sqi","name":"read"},{"id":"fc_008390f508d1d09b016a58aacd12c8819a97178412350b722b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading current wedge hazard rules\",\"path\":\"docs/KNOWN-HAZARDS.md\",\"selector\":\"408-460,480-515,556-590,604-660\"}","call_id":"call_5J5UdBqxO88nvrwByIiw10Wh","name":"read"},{"id":"fc_008390f508d1d09b016a58aacd12d0819aaedda88c0a7081b7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach restart tests\",\"path\":\"crates/spt-daemon/tests/attach.rs\",\"selector\":\"1-230,760-1035\"}","call_id":"call_H0aFWGgrBn2X2bp2gsSzJjFB","name":"read"},{"id":"fc_008390f508d1d09b016a58aacd12d8819ab08b1b7689188663","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain resume wedge test\",\"path\":\"crates/spt/tests/resume_no_control_steal_e2e.rs\",\"selector\":\"1-80,430-500\"}","call_id":"call_0v4wgXCf3bUcx09HbxN656Dy","name":"read"},{"id":"fc_008390f508d1d09b016a58aacd12dc819a89f894d5d984cfbc","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading reconnection test names\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"2700-3214\"}","call_id":"call_9fozOaCtfEa0DzFMGad6KoUg","name":"read"}]},"duration":8589.346499999985,"ttft":1833.0750000001863,"contextSnapshot":{"promptTokens":152290,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_bWO5zCI0h99bdO0X6D7WADi8|fc_008390f508d1d09b016a58aacd129c819abaaddb55cd5c9e56","toolName":"read","startedAt":"2026-07-16T09:56:29.126Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading output log transitions"},"id":"15e24b6e","parentId":"9a8cd3b0","timestamp":"2026-07-16T09:56:29.126Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_wnh9DQyJ7v3rm9pmBRXHgFgG|fc_008390f508d1d09b016a58aacd12ac819aaf3de57a5d08f61b","toolName":"read","startedAt":"2026-07-16T09:56:29.126Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading sink writer tasks"},"id":"1bf8fd1c","parentId":"15e24b6e","timestamp":"2026-07-16T09:56:29.126Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZAu1tShC9pWj3aGICZPob6ee|fc_008390f508d1d09b016a58aacd12b4819abd2dce6db56b7706","toolName":"read","startedAt":"2026-07-16T09:56:29.126Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading connection cleanup path"},"id":"01dd21ec","parentId":"1bf8fd1c","timestamp":"2026-07-16T09:56:29.126Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_JG0LmRf32ILv5dtUUpU30Sqi|fc_008390f508d1d09b016a58aacd12b8819aad53e758be42c469","toolName":"read","startedAt":"2026-07-16T09:56:29.126Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading subscribe dispatch path"},"id":"f3904fd4","parentId":"01dd21ec","timestamp":"2026-07-16T09:56:29.126Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_5J5UdBqxO88nvrwByIiw10Wh|fc_008390f508d1d09b016a58aacd12c8819a97178412350b722b","toolName":"read","startedAt":"2026-07-16T09:56:29.126Z","args":{"path":"docs/KNOWN-HAZARDS.md"},"intent":"Reading current wedge hazard rules"},"id":"bb6507be","parentId":"f3904fd4","timestamp":"2026-07-16T09:56:29.126Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_H0aFWGgrBn2X2bp2gsSzJjFB|fc_008390f508d1d09b016a58aacd12d0819aaedda88c0a7081b7","toolName":"read","startedAt":"2026-07-16T09:56:29.126Z","args":{"path":"crates/spt-daemon/tests/attach.rs"},"intent":"Reading attach restart tests"},"id":"8ef7da74","parentId":"bb6507be","timestamp":"2026-07-16T09:56:29.126Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_0v4wgXCf3bUcx09HbxN656Dy|fc_008390f508d1d09b016a58aacd12d8819ab08b1b7689188663","toolName":"read","startedAt":"2026-07-16T09:56:29.126Z","args":{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs"},"intent":"Reading brain resume wedge test"},"id":"c8be5560","parentId":"8ef7da74","timestamp":"2026-07-16T09:56:29.126Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_9fozOaCtfEa0DzFMGad6KoUg|fc_008390f508d1d09b016a58aacd12dc819a89f894d5d984cfbc","toolName":"read","startedAt":"2026-07-16T09:56:29.126Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading reconnection test names"},"id":"e40a41b7","parentId":"c8be5560","timestamp":"2026-07-16T09:56:29.126Z"}
{"type":"message","id":"74319db8","parentId":"e40a41b7","timestamp":"2026-07-16T09:56:29.175Z","message":{"role":"toolResult","toolCallId":"call_H0aFWGgrBn2X2bp2gsSzJjFB|fc_008390f508d1d09b016a58aacd12d0819aaedda88c0a7081b7","toolName":"read","content":[{"type":"text","text":"//! D5b regression — remote terminal attach (REQ-INST-8): drive a real PTY\n//! session on daemon A from daemon B, byte-stream viewport over the\n//! broker-owned wire; compute stays on A.\n//!\n//! Two in-process brokers (hermetic loopback endpoints, relays disabled) play\n//! the two daemons; a real echo child runs under A's broker PTY. The\n//! invariants:\n//!\n//! - **remote drive**: keystrokes sent from B arrive at A's PTY through the\n//!   journaled input path, and the session's output streams back to B as\n//!   sequenced records — B sees its own marker echoed by A's child;\n//! - **origin is transport truth** (KNOWN-HAZARDS 7.5): the serve loop gates\n//!   on the stream table's handshake-proven `remote_id_hex`;\n//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies\n//!   mid-attach; the broker keeps the QUIC stream, the session, the output\n//!   log, and the effect journal. The successor brain re-serves the SAME\n//!   stream from seq 0 — the worst-case stale cursor: the replayed input\n//!   dedups at the PTY-write journal (typed exactly once), re-transmitted\n//!   output dedups at B's render cursor (rendered exactly once) — including\n//!   input B sent INTO the dead window.\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\nuse std::time::Duration;\n\nuse spt_daemon::attach::{\n    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n    AttachServeOutcome, ControllerIrrecoverablyBehind,\n};\n// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n#[cfg(unix)]\nuse spt_daemon::attach::request_attach_endpoint;\nuse spt_daemon::brain::{Brain, BrainState, BrokerEvent};\nuse spt_daemon::effect::{Minter, MintedOp};\nuse spt_daemon::msg::{decode_bytes, SpawnReq};\nuse spt_daemon::nethost::{NetConfig, NetHost};\nuse spt_daemon::Broker;\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\nuse spt_net::net::ndjson;\nuse spt_proto::identity::Identity;\nuse spt_term::ScreenGrid;\n\n/// Hold the temp home for the whole test binary so it isn't deleted mid-run.\n/// Every test inits it: serve_attach best-effort-stamps the endpoint perch's\n/// `driven_by` marker, and that resolution must never touch a real home.\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n\nfn init_home() {\n    HOME.get_or_init(|| {\n        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n        std::env::set_var(\"SPT_HOME\", dir.path());\n        dir\n    });\n}\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique_name() -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-d5b-{}-{}.sock\", std::process::id(), n)\n}\n\nfn hermetic() -> NetConfig {\n    NetConfig {\n        identity: Identity::generate(),\n        relay: RelayPolicy::Disabled,\n        discovery: LocalDiscovery::Off,\n        scope: BindScope::Loopback,\n        membership: None,\n    }\n}\n\nfn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n    let host = NetHost::start(hermetic()).expect(\"net host start\");\n    let broker =\n        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n    broker\n}\n\nfn connect_retry(name: &str) -> Brain {\n    for _ in 0..300 {\n        if let Ok(b) = Brain::cold_start(name, 1) {\n            return b;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"brain could not connect\");\n}\n\nfn echo_spawn_req() -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n    #[cfg(windows)]\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n    SpawnReq {\n        program,\n        args,\n        rows: 24,\n        cols: 80,\n        endpoint: String::new(),\n        cwd: None,\n        env: Default::default(),\n        translation_binary: None,\n        adapter: String::new(),\n        install_dir: None,\n    }\n}\n\n/// Poll the brain's `net-streams` until a PEER-initiated stream appears; return\n/// `(stream_id, remote_id_hex)`. Filtering on `!initiated_locally` is what the\n/// production dispatch loop does (dispatch.rs) — and it is REQUIRED for the\n/// single-broker loopback case, where the table holds BOTH the operator's own\n/// row and the cross-wired peer row: the serve target is always the peer row.\nfn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n    for _ in 0..400 {\n        let reply = brain.net_streams().expect(\"net-streams\");\n        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {\n            return (s.stream_id, s.remote_id_hex.clone());\n        }\n        thread::sleep(Duration::from_millis(5));\n    }\n    panic!(\"attach stream never appeared at the target's broker\");\n}\n\n/// The operator+serve drive shared by EVERY attach transport — the M12 W1.5\n/// one-pump invariant made STRUCTURAL: cross-node (QUIC conn) and local\n/// (broker loopback conn) call this identically; they diverge ONLY in how\n/// `conn_id` was acquired (`net_dial` vs `net_dial_loopback`) and in\n/// `expected_origin`, never in the `serve_attach`+render pump below. There is no\n/// second code path that could drift.\n///\n/// Given an acquired `conn_id` on `operator` and the `target` broker connection\n/// holding `session_id` for `endpoint`: open the attach stream, serve it on\n/// `target` under `expected_origin` (asserted == the stream table's proven\n/// origin), drive `marker` through the PTY and render it back over the wire,\n/// then detach and confirm the serve loop ended `Detached` (session outlives).\n/// Returns the rendered viewport bytes + the (still-live) target brain so the\n/// caller can assert session survival and clean up.\n#[allow(clippy::too_many_arguments)]\nfn attach_drive_detach(\n    operator: &mut Brain,\n    target_broker: &str,\n    mut target: Brain,\n    conn_id: u64,\n    session_id: u64,\n    endpoint: &str,\n    expected_origin: &str,\n    marker: &[u8],\n) -> (Vec<u8>, Brain) {\n    let stream_b = request_attach(operator, conn_id, session_id, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\n    operator\n        .net_stream_subscribe(stream_b, 0)\n        .expect(\"operator subscribe\");\n\n    // The target serves the inbound attach stream under the proven origin\n    // (KH 7.5 — the stream table, never payload). Cross-node: the handshake id.\n    // Loopback: this node's own id (broker-minted), which gates same-node.\n    let (stream_a, origin) = wait_for_stream(&mut target);\n    assert_eq!(\n        origin, expected_origin,\n        \"serve gate subject IS the stream table's proven origin\"\n    );\n    let endpoint_owned = endpoint.to_string();\n    let broker_owned = target_broker.to_string();\n    let server = thread::spawn(move || {\n        let outcome =\n            serve_attach(&mut target, &broker_owned, stream_a, &endpoint_owned, &origin, 0, None)\n                .expect(\"serve\");\n        (outcome, target)\n    });\n\n    // Drive the marker through; the echo streams back over the SAME pump.\n    let mut keys = marker.to_vec();\n    keys.push(b'\\r');\n    send_attach_input(operator, stream_b, &keys, 1).expect(\"input\");\n    let mut decoder = AttachDecoder::new();\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\n    render_until(\n        operator,\n        stream_b,\n        &mut decoder,\n        &mut cursor,\n        &mut viewport,\n        marker,\n    );\n\n    // Detach: the viewport ends with the session still running.\n    operator\n        .net_stream_send(stream_b, &[], None, true)\n        .expect(\"detach\");\n    let (outcome, target) = server.join().expect(\"serve thread\");\n    assert_eq!(outcome, AttachServeOutcome::Detached);\n    (viewport, target)\n}\n\n/// Operator-side viewport: consume attach-stream records, dedup `Output` by\n/// seq, append rendered bytes to `buf`, until `needle` is rendered. Returns\n/// the rendered-through cursor (last accepted seq + 1).\nfn render_until(\n    brain: &mut Brain,\n    stream_id: u64,\n    decoder: &mut AttachDecoder,\n    next_seq: &mut u64,\n    buf: &mut Vec<u8>,\n    needle: &[u8],\n) {\n    let contains = |hay: &[u8], needle: &[u8]| hay.windows(needle.len()).any(|w| w == needle);\n    while !contains(buf, needle) {\n        match brain.read_event().expect(\"operator read event\") {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    if let AttachRecord::Output { seq, data_b64 } = rec {\n                        if seq < *next_seq {\n                            continue; // replay duplicate — dedup by cursor\n                        }\n                        // #6 (ADR-0031): the initial batch is now a single synthesized\n                        // repaint at pseudo-seq `log.next_seq - 1`, so the FIRST frame a\n                        // fresh (from_seq=0) attach delivers is not seq 0 — it baselines\n                        // the cursor wherever the current screen sits. Every subsequent\n                        // live frame must then be contiguous (no forward gap).\n                        if *next_seq != 0 {\n…\n                        }\n…\n                    }\n                }\n            }\n…\n        }\n    }\n}\n…\nfn broker_spawns_the_pty_child_in_the_requested_cwd() {\n…\n    let mut out = Vec::new();\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    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.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\n    let stream_b = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).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    'life1: loop {\n        match life1.read_event().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.send_effect(MintedOp::new(Minter::Rc, op_id), &b).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(&mut life2, &serve_broker, stream_a, \"ling\", &origin2, 0, None)\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    // idempotent truth: a GAP from the restart/replay (a lost record) would leave a\n    // marker off the final screen. MARKER_ONE proves the pre-crash output survived\n    // the handoff + re-serve; MARKER_TWO proves the dead-window buffered input was\n    // delivered after life 2 took over. Rendering (vs a raw substring scan) is what\n    // makes this robust to the mid-stream repaint's clear/repaint bytes.\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(\"MARKER_ONE\"),\n        \"the operator's rendered screen still shows the pre-crash marker across the \\\n         brain restart (no gap in the re-served output): {painted:?}\"\n    );\n    assert!(\n        painted.contains(\"MARKER_TWO\"),\n        \"the operator's rendered screen shows the dead-window marker delivered after \\\n         the handoff (buffered input survived the restart): {painted:?}\"\n    );\n\n    // Ground truth: the session's OWN authoritative screen (a fresh local attach →\n    // repaint) also carries both markers exactly-once at the source.\n    life2.attach(sid, 0).expect(\"local ground-truth attach\");\n    let mut local: Vec<u8> = Vec::new();\n    life2\n        .read_output_until(b\"MARKER_TWO\", &mut local)\n        .expect(\"local replay\");\n    assert!(count(&local, b\"MARKER_ONE\") >= 1 && count(&local, b\"MARKER_TWO\") >= 1);\n\n    life2.kill_session().expect(\"cleanup kill\");\n}\n\n// [unit->REQ-REACH-1] remote-drive detection is a daemon fact: serving a\n// net-attach stamps the endpoint perch's `driven_by` with the\n// handshake-proven origin node (what the agent's `spt api driven-by` query\n// reads), and the marker clears when the viewport ends — the endpoint reads\n// locally driven again.\n#[test]\nfn attach_registers_remote_drive_detection() {\n    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    // The driven endpoint has a real perch on A (unique id: the shared test\n    // home outlives this test).\n    let endpoint = format!(\n        \"ling-d5c-{}-{}\",\n        std::process::id(),\n        SEQ.fetch_add(1, Ordering::Relaxed)\n    );\n    let perch_path =\n        spt_store::perch::resolve_perch_path(&endpoint, spt_store::perch::ParentHint::Infer);\n    std::fs::create_dir_all(&perch_path).expect(\"perch dir\");\n    let rec =\n        spt_store::info::InfoJson::new(&endpoint, \"now\", std::process::id(), \"sess\", \"live_agent\");\n    spt_store::info::write_info(&perch_path, &rec).expect(\"info.json\");\n\n    let mut target = connect_retry(&name_a);\n    // Spawn the session LABELED with this endpoint: driven_by is now broker-owned\n    // (single writer, REQ-RCVIEW-1) and keyed on the session's endpoint label —\n    // which in production equals the serve gate's endpoint (both resolved from\n    // the spawn). So the labeled spawn is the faithful shape.\n    let sid = target\n        .spawn_session(SpawnReq {\n            endpoint: endpoint.clone(),\n            ..echo_spawn_req()\n        })\n        .expect(\"spawn echo child\");\n    let a_addr = target.net_status().expect(\"a status\").addr;\n\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.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\n    let stream_b = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\n    operator\n        .net_stream_subscribe(stream_b, 0)\n        .expect(\"operator subscribe\");\n\n    let (stream_a, origin) = wait_for_stream(&mut target);\n    let endpoint_serve = endpoint.clone();\n    let serve_broker = name_a.clone();\n    let server = thread::spawn(move || {\n        let outcome =\n            serve_attach(&mut target, &serve_broker, stream_a, &endpoint_serve, &origin, 0, None)\n                .expect(\"serve\");\n        (outcome, target)\n    });\n\n    // Drive a marker through: once it renders at B, the serve loop has long\n    // since processed the Request — the stamp is durably on the perch.\n    send_attach_input(&mut operator, stream_b, b\"DETECT_ME\\r\", 1).expect(\"input\");\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\"DETECT_ME\",\n    );\n    let driven = spt_store::info::read_info(&perch_path)\n        .expect(\"info\")\n        .driven_by;\n    assert_eq!(\n        driven.as_deref(),\n        Some(b_node.as_str()),\n        \"the marker IS the handshake-proven origin node\"\n    );\n\n    // Detach: the viewport ends; the endpoint reads locally driven again.\n    operator\n        .net_stream_send(stream_b, &[], None, true)\n        .expect(\"detach\");\n    let (outcome, mut target) = server.join().expect(\"serve thread\");\n    assert_eq!(outcome, AttachServeOutcome::Detached);\n    // The clear is now BROKER-owned (REQ-RCVIEW-1, single writer): the worker's\n    // detach releases the controller slot over IPC, and the broker re-stamps\n    // driven_by=None asynchronously — so poll for it (production clears the same\n    // async way when the worker connection drops).\n    let cleared = (0..200).any(|_| {\n        if spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.driven_by)\n            .is_none()\n        {\n            return true;\n        }\n        std::thread::sleep(std::time::Duration::from_millis(10));\n        false\n    });\n    assert!(cleared, \"viewport over → marker cleared (broker re-stamps None)\");\n    target.kill_session().expect(\"cleanup kill\");\n}\n\n// ── W2.5 controller/viewer model — broker-IPC-level gate evidence ────────────\n// These exercise the OutputLog controller/viewer fan-out directly over the local\n// broker IPC: each `attach_as` carries a distinct `by` identity, so the role"}],"details":{"displayContent":{"text":"//! D5b regression — remote terminal attach (REQ-INST-8): drive a real PTY\n//! session on daemon A from daemon B, byte-stream viewport over the\n//! broker-owned wire; compute stays on A.\n//!\n//! Two in-process brokers (hermetic loopback endpoints, relays disabled) play\n//! the two daemons; a real echo child runs under A's broker PTY. The\n//! invariants:\n//!\n//! - **remote drive**: keystrokes sent from B arrive at A's PTY through the\n//!   journaled input path, and the session's output streams back to B as\n//!   sequenced records — B sees its own marker echoed by A's child;\n//! - **origin is transport truth** (KNOWN-HAZARDS 7.5): the serve loop gates\n//!   on the stream table's handshake-proven `remote_id_hex`;\n//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies\n//!   mid-attach; the broker keeps the QUIC stream, the session, the output\n//!   log, and the effect journal. The successor brain re-serves the SAME\n//!   stream from seq 0 — the worst-case stale cursor: the replayed input\n//!   dedups at the PTY-write journal (typed exactly once), re-transmitted\n//!   output dedups at B's render cursor (rendered exactly once) — including\n//!   input B sent INTO the dead window.\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\nuse std::time::Duration;\n\nuse spt_daemon::attach::{\n    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n    AttachServeOutcome, ControllerIrrecoverablyBehind,\n};\n// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n#[cfg(unix)]\nuse spt_daemon::attach::request_attach_endpoint;\nuse spt_daemon::brain::{Brain, BrainState, BrokerEvent};\nuse spt_daemon::effect::{Minter, MintedOp};\nuse spt_daemon::msg::{decode_bytes, SpawnReq};\nuse spt_daemon::nethost::{NetConfig, NetHost};\nuse spt_daemon::Broker;\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\nuse spt_net::net::ndjson;\nuse spt_proto::identity::Identity;\nuse spt_term::ScreenGrid;\n\n/// Hold the temp home for the whole test binary so it isn't deleted mid-run.\n/// Every test inits it: serve_attach best-effort-stamps the endpoint perch's\n/// `driven_by` marker, and that resolution must never touch a real home.\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n\nfn init_home() {\n    HOME.get_or_init(|| {\n        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n        std::env::set_var(\"SPT_HOME\", dir.path());\n        dir\n    });\n}\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique_name() -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-d5b-{}-{}.sock\", std::process::id(), n)\n}\n\nfn hermetic() -> NetConfig {\n    NetConfig {\n        identity: Identity::generate(),\n        relay: RelayPolicy::Disabled,\n        discovery: LocalDiscovery::Off,\n        scope: BindScope::Loopback,\n        membership: None,\n    }\n}\n\nfn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n    let host = NetHost::start(hermetic()).expect(\"net host start\");\n    let broker =\n        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n    broker\n}\n\nfn connect_retry(name: &str) -> Brain {\n    for _ in 0..300 {\n        if let Ok(b) = Brain::cold_start(name, 1) {\n            return b;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"brain could not connect\");\n}\n\nfn echo_spawn_req() -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n    #[cfg(windows)]\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n    SpawnReq {\n        program,\n        args,\n        rows: 24,\n        cols: 80,\n        endpoint: String::new(),\n        cwd: None,\n        env: Default::default(),\n        translation_binary: None,\n        adapter: String::new(),\n        install_dir: None,\n    }\n}\n\n/// Poll the brain's `net-streams` until a PEER-initiated stream appears; return\n/// `(stream_id, remote_id_hex)`. Filtering on `!initiated_locally` is what the\n/// production dispatch loop does (dispatch.rs) — and it is REQUIRED for the\n/// single-broker loopback case, where the table holds BOTH the operator's own\n/// row and the cross-wired peer row: the serve target is always the peer row.\nfn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n    for _ in 0..400 {\n        let reply = brain.net_streams().expect(\"net-streams\");\n        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {\n            return (s.stream_id, s.remote_id_hex.clone());\n        }\n        thread::sleep(Duration::from_millis(5));\n    }\n    panic!(\"attach stream never appeared at the target's broker\");\n}\n\n/// The operator+serve drive shared by EVERY attach transport — the M12 W1.5\n/// one-pump invariant made STRUCTURAL: cross-node (QUIC conn) and local\n/// (broker loopback conn) call this identically; they diverge ONLY in how\n/// `conn_id` was acquired (`net_dial` vs `net_dial_loopback`) and in\n/// `expected_origin`, never in the `serve_attach`+render pump below. There is no\n/// second code path that could drift.\n///\n/// Given an acquired `conn_id` on `operator` and the `target` broker connection\n/// holding `session_id` for `endpoint`: open the attach stream, serve it on\n/// `target` under `expected_origin` (asserted == the stream table's proven\n/// origin), drive `marker` through the PTY and render it back over the wire,\n/// then detach and confirm the serve loop ended `Detached` (session outlives).\n/// Returns the rendered viewport bytes + the (still-live) target brain so the\n/// caller can assert session survival and clean up.\n#[allow(clippy::too_many_arguments)]\nfn attach_drive_detach(\n    operator: &mut Brain,\n    target_broker: &str,\n    mut target: Brain,\n    conn_id: u64,\n    session_id: u64,\n    endpoint: &str,\n    expected_origin: &str,\n    marker: &[u8],\n) -> (Vec<u8>, Brain) {\n    let stream_b = request_attach(operator, conn_id, session_id, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\n    operator\n        .net_stream_subscribe(stream_b, 0)\n        .expect(\"operator subscribe\");\n\n    // The target serves the inbound attach stream under the proven origin\n    // (KH 7.5 — the stream table, never payload). Cross-node: the handshake id.\n    // Loopback: this node's own id (broker-minted), which gates same-node.\n    let (stream_a, origin) = wait_for_stream(&mut target);\n    assert_eq!(\n        origin, expected_origin,\n        \"serve gate subject IS the stream table's proven origin\"\n    );\n    let endpoint_owned = endpoint.to_string();\n    let broker_owned = target_broker.to_string();\n    let server = thread::spawn(move || {\n        let outcome =\n            serve_attach(&mut target, &broker_owned, stream_a, &endpoint_owned, &origin, 0, None)\n                .expect(\"serve\");\n        (outcome, target)\n    });\n\n    // Drive the marker through; the echo streams back over the SAME pump.\n    let mut keys = marker.to_vec();\n    keys.push(b'\\r');\n    send_attach_input(operator, stream_b, &keys, 1).expect(\"input\");\n    let mut decoder = AttachDecoder::new();\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\n    render_until(\n        operator,\n        stream_b,\n        &mut decoder,\n        &mut cursor,\n        &mut viewport,\n        marker,\n    );\n\n    // Detach: the viewport ends with the session still running.\n    operator\n        .net_stream_send(stream_b, &[], None, true)\n        .expect(\"detach\");\n    let (outcome, target) = server.join().expect(\"serve thread\");\n    assert_eq!(outcome, AttachServeOutcome::Detached);\n    (viewport, target)\n}\n\n/// Operator-side viewport: consume attach-stream records, dedup `Output` by\n/// seq, append rendered bytes to `buf`, until `needle` is rendered. Returns\n/// the rendered-through cursor (last accepted seq + 1).\nfn render_until(\n    brain: &mut Brain,\n    stream_id: u64,\n    decoder: &mut AttachDecoder,\n    next_seq: &mut u64,\n    buf: &mut Vec<u8>,\n    needle: &[u8],\n) {\n    let contains = |hay: &[u8], needle: &[u8]| hay.windows(needle.len()).any(|w| w == needle);\n    while !contains(buf, needle) {\n        match brain.read_event().expect(\"operator read event\") {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    if let AttachRecord::Output { seq, data_b64 } = rec {\n                        if seq < *next_seq {\n                            continue; // replay duplicate — dedup by cursor\n                        }\n                        // #6 (ADR-0031): the initial batch is now a single synthesized\n                        // repaint at pseudo-seq `log.next_seq - 1`, so the FIRST frame a\n                        // fresh (from_seq=0) attach delivers is not seq 0 — it baselines\n                        // the cursor wherever the current screen sits. Every subsequent\n                        // live frame must then be contiguous (no forward gap).\n                        if *next_seq != 0 {\n…\n                        }\n…\n                    }\n                }\n            }\n…\n        }\n    }\n}\n…\nfn broker_spawns_the_pty_child_in_the_requested_cwd() {\n…\n    let mut out = Vec::new();\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    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.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\n    let stream_b = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).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    'life1: loop {\n        match life1.read_event().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.send_effect(MintedOp::new(Minter::Rc, op_id), &b).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(&mut life2, &serve_broker, stream_a, \"ling\", &origin2, 0, None)\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    // idempotent truth: a GAP from the restart/replay (a lost record) would leave a\n    // marker off the final screen. MARKER_ONE proves the pre-crash output survived\n    // the handoff + re-serve; MARKER_TWO proves the dead-window buffered input was\n    // delivered after life 2 took over. Rendering (vs a raw substring scan) is what\n    // makes this robust to the mid-stream repaint's clear/repaint bytes.\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(\"MARKER_ONE\"),\n        \"the operator's rendered screen still shows the pre-crash marker across the \\\n         brain restart (no gap in the re-served output): {painted:?}\"\n    );\n    assert!(\n        painted.contains(\"MARKER_TWO\"),\n        \"the operator's rendered screen shows the dead-window marker delivered after \\\n         the handoff (buffered input survived the restart): {painted:?}\"\n    );\n\n    // Ground truth: the session's OWN authoritative screen (a fresh local attach →\n    // repaint) also carries both markers exactly-once at the source.\n    life2.attach(sid, 0).expect(\"local ground-truth attach\");\n    let mut local: Vec<u8> = Vec::new();\n    life2\n        .read_output_until(b\"MARKER_TWO\", &mut local)\n        .expect(\"local replay\");\n    assert!(count(&local, b\"MARKER_ONE\") >= 1 && count(&local, b\"MARKER_TWO\") >= 1);\n\n    life2.kill_session().expect(\"cleanup kill\");\n}\n\n// [unit->REQ-REACH-1] remote-drive detection is a daemon fact: serving a\n// net-attach stamps the endpoint perch's `driven_by` with the\n// handshake-proven origin node (what the agent's `spt api driven-by` query\n// reads), and the marker clears when the viewport ends — the endpoint reads\n// locally driven again.\n#[test]\nfn attach_registers_remote_drive_detection() {\n    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    // The driven endpoint has a real perch on A (unique id: the shared test\n    // home outlives this test).\n    let endpoint = format!(\n        \"ling-d5c-{}-{}\",\n        std::process::id(),\n        SEQ.fetch_add(1, Ordering::Relaxed)\n    );\n    let perch_path =\n        spt_store::perch::resolve_perch_path(&endpoint, spt_store::perch::ParentHint::Infer);\n    std::fs::create_dir_all(&perch_path).expect(\"perch dir\");\n    let rec =\n        spt_store::info::InfoJson::new(&endpoint, \"now\", std::process::id(), \"sess\", \"live_agent\");\n    spt_store::info::write_info(&perch_path, &rec).expect(\"info.json\");\n\n    let mut target = connect_retry(&name_a);\n    // Spawn the session LABELED with this endpoint: driven_by is now broker-owned\n    // (single writer, REQ-RCVIEW-1) and keyed on the session's endpoint label —\n    // which in production equals the serve gate's endpoint (both resolved from\n    // the spawn). So the labeled spawn is the faithful shape.\n    let sid = target\n        .spawn_session(SpawnReq {\n            endpoint: endpoint.clone(),\n            ..echo_spawn_req()\n        })\n        .expect(\"spawn echo child\");\n    let a_addr = target.net_status().expect(\"a status\").addr;\n\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.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\n    let stream_b = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\n    operator\n        .net_stream_subscribe(stream_b, 0)\n        .expect(\"operator subscribe\");\n\n    let (stream_a, origin) = wait_for_stream(&mut target);\n    let endpoint_serve = endpoint.clone();\n    let serve_broker = name_a.clone();\n    let server = thread::spawn(move || {\n        let outcome =\n            serve_attach(&mut target, &serve_broker, stream_a, &endpoint_serve, &origin, 0, None)\n                .expect(\"serve\");\n        (outcome, target)\n    });\n\n    // Drive a marker through: once it renders at B, the serve loop has long\n    // since processed the Request — the stamp is durably on the perch.\n    send_attach_input(&mut operator, stream_b, b\"DETECT_ME\\r\", 1).expect(\"input\");\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\"DETECT_ME\",\n    );\n    let driven = spt_store::info::read_info(&perch_path)\n        .expect(\"info\")\n        .driven_by;\n    assert_eq!(\n        driven.as_deref(),\n        Some(b_node.as_str()),\n        \"the marker IS the handshake-proven origin node\"\n    );\n\n    // Detach: the viewport ends; the endpoint reads locally driven again.\n    operator\n        .net_stream_send(stream_b, &[], None, true)\n        .expect(\"detach\");\n    let (outcome, mut target) = server.join().expect(\"serve thread\");\n    assert_eq!(outcome, AttachServeOutcome::Detached);\n    // The clear is now BROKER-owned (REQ-RCVIEW-1, single writer): the worker's\n    // detach releases the controller slot over IPC, and the broker re-stamps\n    // driven_by=None asynchronously — so poll for it (production clears the same\n    // async way when the worker connection drops).\n    let cleared = (0..200).any(|_| {\n        if spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.driven_by)\n            .is_none()\n        {\n            return true;\n        }\n        std::thread::sleep(std::time::Duration::from_millis(10));\n        false\n    });\n    assert!(cleared, \"viewport over → marker cleared (broker re-stamps None)\");\n    target.kill_session().expect(\"cleanup kill\");\n}\n\n// ── W2.5 controller/viewer model — broker-IPC-level gate evidence ────────────\n// These exercise the OutputLog controller/viewer fan-out directly over the local\n// broker IPC: each `attach_as` carries a distinct `by` identity, so the role","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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,null,232,null,235,236,237,null,241,242,243,null,721,null,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,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,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,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\attach.rs"}}},"isError":false,"timestamp":1784195789175}}
{"type":"message","id":"76e742b5","parentId":"74319db8","timestamp":"2026-07-16T09:56:29.175Z","message":{"role":"toolResult","toolCallId":"call_5J5UdBqxO88nvrwByIiw10Wh|fc_008390f508d1d09b016a58aacd12c8819a97178412350b722b","toolName":"read","content":[{"type":"text","text":"### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`\n<!-- [doc->REQ-HAZARD-VIEWER-ISOLATION] -->\n- **Failure:** the W2.5 controller/viewer model lets ANY number of read-only `--view` attachers ride one session's broker `OutputLog`. The single drain thread fans each output chunk to every attacher. If a viewer's socket is fanned out with a **blocking** write under the log lock (the controller's authoritative path), one wedged viewer (a slow terminal, a black-holed WAN peer, a hostile non-reader) stalls the drain — freezing the controller's stream and backing up the PTY child. A single watcher must never be able to degrade or hang the driver.\n- **Invariant:** the drain writes the **controller** on the authoritative blocking bounded path (it alone advances `delivered_through`), but each **viewer** gets an **isolated bounded SPSC queue + a dedicated writer thread**; the drain `try_send`s under the log lock and **evicts** any viewer whose queue is `Full` (fell behind the live stream) or `Disconnected` (its writer died on a dead socket) — the drain thread **never touches a viewer socket**, so no viewer write can backpressure it. A **soft cap** (`MAX_VIEWERS`) bounds the writer-thread count (a viewer attach beyond it is refused). Viewer eviction never perturbs the controller stream, the `delivered_through` cursor, or the child. Ring **replay-at-attach** is owned by the writer thread (not the bounded l…\n- **spt-core mapping:** `OutputLog::append` (controller blocking + `viewer_send_evicts` `try_send` fan-out), `OutputLog::add_viewer` (bounded `sync_channel(VIEWER_CHANNEL_DEPTH)` + `viewer_writer` thread), `MAX_VIEWERS` soft cap, `ViewerSink`. Unit: `viewer_overflow_or_disconnect_evicts_never_blocks`. Int: `wedged_viewer_does_not_stall_controller` (a non-reading viewer is evicted while the controller keeps receiving past a 200KB burst).\n- **Source:** M12 W2.5 controller/viewer model (doyle ruling 2026-06-14, Q1).\n\n### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`\n<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\n- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only via its OWN 30s read-deadline (the 7.6 A-half) — a 30s stall + a full pump restart EACH round, and the supervised restart re-dials the SAME dead peer and re-wedges. Root cause of the 2.2h hfenduleam wedge (2026-06-11) and its recurrence (2026-06-16).\n- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`, so the BROKER fires FIRST — the brain reconstructs `ErrorKind::Other` (its `net_dial`/`net_open_stream`/`net_stream_send` map a broker error reply to `io::Error::other`), NOT its own read-deadline `TimedOut`, so `pump::peer_outcome` takes the ordinary per-pee…\n- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIES an ordinary error within the bound + exactly-once-on-timeout: journal un-applied, conn table empty, clean redial) and `pump_survives_a_black_holing_peer_heartbeat_advances_no_restart` (the production pump against a dead peer — heartbeat monotonic-advances, `run…\n- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.\n\n### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`\n<!-- [doc->REQ-HAZARD-BROKER-SEED-WIRE-SKEW] -->\n- **Failure:** the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004's no-terminate-during-update pillar forbids auto-killing it — 6.7). A self-update that changes a daemon-state WIRE FORMAT — e.g. the v0.9.0 adapter-agnostic `Seed` (the `adapter` field dropped) — therefore lands a NEW-version CLI talking to the STILL-RESIDENT OLD broker. The old broker cannot deserialize the new `Seed` (its formerly-required `adapter` is absent), so it drops the seed-control conn without acking; the CLI's `put_seed` ack-read hits EOF and surfaces a raw `UnexpectedEof` \"failed to fill whole buffer\" — a cryptic footgun that hides the real cause (perri PREP-4 FINDING 1: v0.9.0 CLI ↔ stale 0.8.x broker).\n- **Invariant:** (a) spt-core surfaces an ACTIONABLE diagnostic on the seed-ack EOF — naming the stale-broker cause + the fix (`spt daemon stop`; the broker restarts on the next `spt api` call) — never the bare io error (scoped to `UnexpectedEof` on the seed-ack path, so it never mis-fires on an unrelated error like a refused connect). (b) A daemon-state wire change requires a DELIBERATE full broker restart; this is NOT automatic — ADR-0004 forbids auto-killing the resident broker, and a brain-only update keeps the old broker. (c) FORWARD discipline: daemon-state / `Seed` schema changes stay ADDITIVE + serde-default, so a resident OLD broker tolerates a NEW CLI across a brain-only update. (This would NOT have rescued 0.9.0 itself — the old broker's `adapter`…\n- **spt-core mapping:** `startup::seed_fail_message` (the `UnexpectedEof` → actionable-hint branch) fired from `cmd_seed`'s `put_seed` error arm; the broker's seed-control residency (`seedmap::serve_seed_control`, held across brain restarts — 6.7). Unit: `seed_fail_eof_gives_actionable_stale_broker_hint` (EOF → the `spt daemon stop` hint naming the stale broker; a non-EOF kind → the plain message, no mis-fire).\n- **Source:** perri PREP-4 FINDING 1 (v0.9.0 dogfood) → v0.9.1.\n\n### 7.10 A VIEW is independent from the endpoint — closing the launching tab must NOT reap the daemon-hosted harness  `[REQ-HAZARD-VIEWER-CLOSE-DETACH]`\n<!-- [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH] -->\n- **Failure (Windows):** `spt endpoint run` autostarts the daemon (`ensure_running` → `detached_no_inherit`), which INHERITS the launching terminal's Windows Job Object. Windows Terminal / VS Code place the shell AND every descendant in a Job with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; closing the tab drops the job's last handle → the OS reaps the daemon + its broker-spawned ConPTY harness subtree. The `rc` pump detaching should end ONLY the viewport; the harness must keep running + stay re-attachable. (ConPTY isolation itself is already correct — portable-pty builds the pseudoconsole in the daemon; the leaking lifetime binding is the Job Object, not the console.)\n- **Invariant:** both daemon spawn paths request `CREATE_BREAKAWAY_FROM_JOB` (0x0100_0000), best-effort with an in-job FALLBACK on `ERROR_ACCESS_DENIED`(5) / `ERROR_INVALID_PARAMETER`(87) so a breakaway-denying job NEVER regresses the spawn. **A real job CAN deny breakaway** (proven: the cargo/CI runner's own ancestor job ACCESS_DENIES it), so efficacy against the real terminal is UNKNOWN until measured. **int stage = OPERATOR MANUAL ACCEPTANCE, not CI** (doyle ruling 2026-06-18): the runner sits inside a breakaway-denying job and every test-created job nests inside it → a faithful \"harness survives tab-close\" test is a guaranteed FALSE-RED; the two units (escape-where-permitted self-skip + fallback-where-denied) are the CI evidence; `required_stages` stays …\n- **spt-core mapping:** `daemon.rs::detached_no_inherit` + `deelevate.rs::create_with_token` (the breakaway flag + fallback). Units: `detached_no_inherit_falls_back_under_a_breakaway_denying_job` (no-regression — the shared runner exercises the fallback directly), `breakaway_spawn_escapes_a_kill_on_close_job` (the OS escape mechanism — self-SKIPS where the ancestor job forbids breakaway). Unix: the daemon's `setsid` session-detach already keeps a closing terminal's SIGHUP off its children (guard test, no code).\n- **Source:** v0.12.0 real-harness defect (operator) → v0.12.1 L1 @5ae68f8; doyle design+int ruling 2026-06-18.\n\n### 7.11 A dead PTY child + a dropped operator pump must NOT wedge the broker for other clients  `[REQ-HAZARD-ATTACH-WEDGE]`\n- **Failure (hypothesized, v0.12.0):** a legitimately dead PTY child (crash/kill) plus an `rc` pump dropped without a clean detach (closed tab) was thought to make the broker's loopback forward `write_all` block forever on a full 64 KB duplex, park a worker in the 2-worker net runtime, saturate both, and stall every new attach / `endpoint run` (with `daemon stop` unable to join).\n- **Invariant — DISPOSITION = PROVE-DON'T-CHANGE** (doyle GATE-PASS @e883f45, 2026-06-18): the post-L0 code ALREADY prevents the wedge; NO fail-fast / worker-count code was added. (1) `serve_attach` forwards fire-and-forget (`net_stream_send` `op_id=None`) and the broker-side `send_stream` is already deadline-bounded (`bounded_block_on`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s — hazard 7.8), not forever. (2) the loopback duplex is drained broker-INTERNALLY by the operator row's OWN read pump (`nethost.rs` `RecvHalf::Loopback`), which for an ordinary attach stream (`retentive_cap == 0`) NEVER parks → `peer_w` never backs up on a dead `rc` (a dead `rc` is just a dropped IPC subscriber against a bounded, EVICTING ring). (3) `bounded_block_on` = `runtime.handle().bloc…\n- **spt-core mapping:** int `crates/spt/tests/attach_wedge_e2e.rs` (REAL detached daemon + dummy-harness fixture): serve the victim (rc sees its tick), abruptly kill rc (dropped pump) + kill the PTY child → a NEW endpoint still comes online + is served (no wedge), the dead endpoint is offlined within a tick (`LIVENESS_RECONCILE_OFFLINE`), `daemon stop` bounded. Leans on 7.8 (the QUIC-op deadline) + 7.7 (viewer isolation).\n- **Source:** v0.12.0 real-harness finding (operator) → v0.12.1 L2 @e883f45; doyle GATE-PASS 2026-06-18.\n\n### 7.12 Controller output must NOT be written inline on the drain thread — a backed-up controller wedges the session  `[REQ-HAZARD-INJECT-CONTROL-COEXIST]`\n- **Failure (v0.12.x, operator dogfooding + doyle /diagnose 2026-06-19):** `OutputLog::append` fanned each live chunk to the CONTROLLER via a SYNCHRONOUS, blocking `write_frame` held INLINE on the session's single drain thread while `Mutex<OutputLog>` was locked (viewers already had a dedicated writer thread + bounded evicting channel). A backed-up controller socket — a slow operator, or the full 64 KB `rc` loopback duplex under heavy TUI redraw — parked the drain thread WITH THE LOG LOCK HELD: output + keystroke-echo stalled and every attach / resize / `KIND_SESSIONS` that needs the log lock blocked FOREVER, while the broker process stayed alive. Fires on NORMAL interactive `rc` use under heavy output (not only message injection); REOPENED the wedge facet o…\n- **Invariant:** controller delivery runs OFF the drain thread — a dedicated controller writer thread + bounded channel (the 7.7 viewer_writer pattern) does the blocking socket write; the drain hands each chunk off with a BOUNDED, OFF-LOCK send (deadline → detach + `clear_controller`, never park forever, never silently evict a LIVE operator's authoritative view). The controller is AUTHORITATIVE (unlike a viewer): its writer advances the `delivered_through` resume cursor (atomic, only-on-success, monotonic). `clear_controller` is the ONE unlatch path (clears `driven_by`, drops the sink) — W5's reconcile self-heal reuses it.\n- **spt-core mapping:** impl `broker.rs` `OutputLog::append`/`controller_writer`/`become_controller`/`ControllerJob::deliver`/`clear_controller`; int `crates/spt-daemon/tests/inject_control_wedge.rs` (a fully-backed-up controller keeps `KIND_SESSIONS` answering = no wedge); unit the bounded-deliver + monotonic-cursor kernels. The input-side parks (write_input `write_all` on a full buffer; DSR-answer writer-mutex contention) are BENIGN on Windows ConPTY (absorbs a large inject), real only on Unix forkpty → bounded by W2's atomic-write substrate.\n- **Source:** v0.12.x operator wedge → v0.13.0 W1 (doyle GATE-PASS 2026-06-19).\n\n### 7.13 `spt rc` must SWAP Windows legacy-console Backspace/Ctrl+Backspace (`^H`↔DEL); do NOT enable VT console input  `[REQ-HAZARD-RC-INPUT-KEY-ENCODING]`\n- **Failure (operator dogfooding):** `spt rc` is a raw verbatim stdin byte pump; the legacy Windows console delivers Backspace as `^H` (0x08), and Claude Code maps `^H` → backward-kill-word, so every Backspace deletes a whole word. Confirmed bytes (operator HITL capture, real Windows Terminal): Backspace=0x08, Ctrl+Backspace=0x7f.\n- **REJECTED fix (dc07c39, reverted):** enabling `ENABLE_VIRTUAL_TERMINAL_INPUT` on the stdin console BACKFIRED — on Windows Terminal that flag yields **win32-input-mode**, not legacy xterm VT. Every key then arrives as a 6-field `ESC[Vk;Sc;Uc;Kd;Cs;Rc_` key-down/up record: (1) the ctrl-b detach broke (0x02 is wrapped, so `parse_stdin_chunk` never sees the raw `DETACH_PREFIX`), and (2) win32-input-mode was forwarded into Claude Code's own ConPTY input negotiation (garbage). The VT lever is wrong for Windows Terminal. (Lesson: verify a terminal-input fix on the REAL terminal via the operator re-capture — unit/clippy never exercise the live key path.)\n- **Invariant:** stay a raw byte pump and NARROW-normalize in the forward path — **SWAP** the two delete bytes: `0x08` → `0x7f` (VT DEL, char-delete) AND `0x7f` → `0x08` (^H, kill-word), AFTER the detach state machine consumes any `0x02`, leaving `0x02` and every other byte untouched. Because CC reads `^H`=word-delete and DEL=char-delete, swapping the *input* bytes gives Backspace=char-delete and Ctrl+Backspace=word-delete — the native Win11 convention (the operator rejected losing word-delete; the earlier one-way `0x08`→`0x7f` map dropped it). cfg(windows) only (Unix is already VT; a real `ctrl+h`/`ctrl+?` there must pass through). Accepted minor loss: a real `ctrl+h` (0x08) and a real DEL (0x7f) are swapped on Windows — both are delete keys, so the practic…\n- **spt-core mapping:** the Backspace/Ctrl+Backspace SWAP intent persists; the `normalize_key_byte` byte-swap was SUPERSEDED on Windows by **7.16** (`REQ-RC-KEY-VT-TRANSLATE`, v0.13.0 bug 2) — the agnostic key-event→xterm-VT `translate_key_event` now emits Backspace → `0x7f` and Ctrl+Backspace → `0x08` natively (carrying the relocated `[impl]`/`[unit]` evidence), so `normalize_key_byte` + its unit were removed. The live keypress is HITL operator re-capture.\n- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19; superseded-on-Windows by the bug-2 translator 2026-06-19).\n\n### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`\n- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two facets, one root: **(A) stutter** — measured fsync on `%LOCALAPPDATA%\\spt-core` median 6.5 ms / spikes 198 ms, ×2 per keystroke → choppy, worsens with volume; **(B) HARD WEDGE** — a blocking `PtyWrite` (ConPTY/forkpty input buffer not draining) held the lock indefi…\n- **Invariant:** `apply_once` RELEASES the inner lock across `effect()` — reserve the key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable, mark applied). Crash-idempotency comes from the per-key reservation + the applied-set, not from holding the lock across the effect (the brain-crash guarantee is unchanged: the broker survives the brain, so reserve→effect→finalize runs uninterrupted on the broker thread; a replay hits the applied-set and dedups). `EffectKind::PtyWrite` is **EPHEMERAL** — NO journal lines, NO fsync, in-memory dedup only (a keystroke lost to a broker crash is retyped; PTY state is never rebuilt from keystroke replay) — while durable kinds (spool/r…\n- **spt-core mapping:** impl `effect.rs` `apply_once` (reserve/release/finalize) + `EffectKind::is_durable`; unit (`effect.rs`) a barrier proving the lock is NOT held across `effect()` (a different key's `apply_once` completes while one effect blocks) + `PtyWrite` writes no journal line while a durable kind (`NetSend`) does (the no-fsync proxy); int `crates/spt-daemon/tests/inject_control_wedge.rs` (real broker+PTY, a stalled consumer + sustained blocking input effect: a concurrent `spt rc` attach stays serviceable AND actually receives PTY bytes — the assertion W1's gate lacked). Unix park-(b) folded here, gated on gravity-linux. doyle GATE-GUARD: assert structurally, NEVER on fsync wall-clock (env-dependent, flaky); the stutter is fixed as a consequence of…\n- **Source:** v0.13.0 W1b (operator dogfooding post-W1 escape; doyle /diagnose + measurement 2026-06-19).\n\n…\n### 7.18 `spt rc` paste is client-originated on Windows — read the LOCAL clipboard, inject a BRACKETED paste  `[REQ-RC-WIN-PASTE]`\n- **Failure (operator dogfooding):** in an `spt rc` session neither ctrl+V nor right-click pasted (CC explicitly supports ctrl+V). `RawGuard` did 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 translated to a 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. A multi-line paste-as-keys also became a `\\r` submit-storm.\n…\n- **spt-core mapping:** impl `rc.rs` `wrap_bracketed_paste` + `mouse_is_paste` + `clipboard_paste` + `read_clipboard` + `windows_mouse_wanted` + `RawGuard` (mouse capture/restore) + the `spawn_stdin_reader_events` right-mouse arm; unit the exact bracketed framing + content-verbatim + mouse classify (right-down ⇒ paste, all else ⇒ drop) + the injected-reader paste decision (non-empty ⇒ wrapped, empty/fail ⇒ no-op); NO int (live clipboard + console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent).\n- **Source:** v0.13.0 P1 (operator HITL; doyle design 2026-06-19). Depends on P0 (7.17). AMENDED v0.13.0 P1b: scope narrowed to right-click-only (the dead ctrl+V interception removed); scroll-forward is 7.20; the input-flood non-wedge is 7.19.\n\n<!-- [doc->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] -->\n### 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- **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 → the broker→brain return direction fills (~10 frames = the IPC pipe buffer) → `send_frame` BLOCKS → the handler stops reading → the brain's writes block → mutual full-duplex DEADLOCK. (Capture: 11 frames, `write_input` 11/11 — P0 holds; `ack send` START=11/END=10…\n- **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=true`). EXACTLY-ONCE preserved: the broker dedups by `(session, op_id)` at the applied-set regardless of the ack. **N-1 caveat:** an OLD resident broker (the self-update window) ignores `ack=false` → still acks → the deadlock persists until a broker restart (inher…\n- **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 session stays live + a concurrent attach opens (real broker+brain, no mocks).\n- **Source:** v0.13.0 P1b (operator HITL re-open; doyle /diagnose on the enhanced rc+broker capture 2026-06-19). The real ship-blocker behind the ctrl+V wedge.\n\n<!-- [doc->REQ-RC-MOUSE-FORWARD] -->\n### 7.20 `spt rc` must forward the scroll wheel to the harness (our mouse capture steals WT's native scroll)  `[REQ-RC-MOUSE-FORWARD]`\n- **Failure (operator HITL):** scroll stopped working in `spt rc`. P1's `EnableMouseCapture` (for right-click paste, 7.18) makes Windows Terminal forward ALL mouse — including the wheel — to rc instead of scrolling its own buffer, but the rc mouse handler dropped everything except right-button-down → scroll DIED (and WT's native scrollback is stolen under the capture).\n- **Invariant:** on Windows, TRACK the harness's mouse-reporting mode from its OUTPUT — scan for DECSET `ESC[?1000h/1002h/1003h` (mouse on) + `ESC[?1006h` (SGR ext) and their `…l` (off) into a shared `MouseMode{enabled,sgr}` (the pump writes from the output render path, the stdin reader reads); the scan survives a sequence SPLIT across output chunks (a bounded carry buffer). The mouse handler: right-button-DOWN → bracketed clipboard paste (7.18, unchanged); `ScrollUp/Down` → an xterm SGR mouse report (`ESC[<64;col+1;row+1M` up / `ESC[<65;…M` down; 0-based crossterm → 1-based xterm), forwarded ONLY when `enabled && sgr` (else DROP — a legacy report the harness may misread is garbage); Moved/drag/left/middle DROP (scroll is the need; click-forward risks garbag…\n- **spt-core mapping:** impl `rc.rs` `MouseMode` + `MouseModeScanner`/`parse_decset_private`/`apply_mouse_mode` (carry-buffer scan, fed from the pump's output path) + `scroll_dir` + `scroll_sgr` + the `spawn_stdin_reader_events` scroll arm (threaded via `Arc<MouseMode>`); unit `scroll_dir` classify + `scroll_sgr` exact bytes + the DECSET scan (set/reset, combined, mixed, and split-across-chunks); NO int (live console mouse = HITL).\n- **Source:** v0.13.0 P1b (operator HITL; doyle design 2026-06-19). Bundled with 7.19.\n\n<!-- [doc->REQ-HAZARD-CONTROLLER-WRITER-REORDER] -->\n### 7.21 Exactly ONE `controller_writer` per brain↔broker connection — a superseded writer must write nothing further  `[REQ-HAZARD-CONTROLLER-WRITER-REORDER]`\n- **Failure (doyle instrumented RACEDIAG repro, kitsubito):** on a brain-restart re-serve the handoff brain registered as controller on the SAME session TWICE over the SAME socket — `Brain::handoff` eagerly `subscribe(prior.next_seq=1)` → `become_controller(from_seq=1)`, spawning writer-A (writes seq 1); then `serve_attach` re-handled the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, spawning writer-B (writes 0,1). `become_controller` dropped the prior `ControllerSink` (its `tx`) but did NOT stop the prior writer — writer-A kept flushing its OWNED `initial` batch, and both writers held clones of one `SharedSend` (`Arc<Mutex<socket>>`) with no inter-thread ordering. When writer-A's seq 1 beat writer-B's seq 0, the stri…\n- **Invariant:** on a single brain↔broker connection exactly ONE `controller_writer` is ever the LIVE writer; a SUPERSEDED writer writes no further frames after the epoch bump it observes; and **every `controller_writer` emits a strictly ASCENDING seq stream** (sorted initial batch + ascending live frames). The CORRECTNESS guarantee that falls out: a snap-above consumer over any interleaving of ascending writers — where the surviving writer (`serve_attach`'s `attach_as(sid,0)`) offers the COMPLETE range `[0, end]` — delivers `[K, end]` with **no skip and no dup** (the first sighting of any seq `>M` is always preceded by a sighting of `M` on that same ascending writer, so `M` is delivered before the cursor can pass it). Enforced/relied-on three ways (NB: fix …\n- **spt-core mapping:** impl `brain.rs` `Brain::handoff` (KEEP the eager `subscribe`; seed `session_cursors`) + `Brain::subscribe_with` (resume-mode dedup-cursor reset to `from_seq`, shared by `attach`/`attach_as` — the operator-stream boundary fix) + `broker.rs` `OutputLog.controller_epoch: Arc<AtomicU64>` / `become_controller` (atomic `fetch_add`, passes the new epoch + `Arc::clone` into the writer) / `controller_writer` (epoch gate read UNDER the lock on both loops) / `mark_controller_gone` + the `ControllerJob` epoch read; unit (white-box, `src/broker.rs`) the epoch-gated writer (a superseded writer flushes nothing — only the latest writer's monotonic stream reaches the wire) + `handoff` seeds `session_cursors`/resubscribes; int (keystone, `tests/broker.…\n- **Source:** v0.13.0 P1c (operator-ruled root-fix before ship; doyle root-cause via instrumented repro 2026-06-20; design corrected across two gate rounds — fix #1 reverted, then the kitsubito RACEDIAG pinned the residual to the consumer-side operator-stream boundary, fixed by the `subscribe_with` cursor reset on `attach_as` re-subscribe). The last v0.13.0 ship-blocker.\n\n### 7.22 An idle delivery with no working translation binary must SPOOL, never raw-inject a pseudo-delivery reported as delivered  `[REQ-HAZARD-IDLE-SILENT-NONDELIVERY]`\n- **Failure (F-019 post-mortem):** the v0.11.0 path raw-injected `payload+\\r` into the spt-hosted PTY whenever no working translation binary handled an inbound message — none declared, spawn-failed, FAULTED (commit-deadline miss / binary death), or its inject-worker channel dropped — AND acked `delivered=true`. But a bare `payload+\\r` does NOT submit on a modern TUI (Claude Code): the message was TYPED into the harness but never sent, a silent pseudo-delivery reported as success. That silent degrade-to-raw-inject is precisely what MASKED F-019 (`[REQ-INSTALL-11]`, above) through a multi-hour black-box hunt — every behavioral hypothesis was moot because the \"delivery\" never delivered.\n- **Invariant:** spt-hosted idle delivery is translation-binary-ONLY (ADR-0022 amendment). `dispatch_endpoint_input` with no working binary replies `endpoint_injected_envelope(ep, delivered=false)` and writes NOTHING to the PTY; the caller (`try_broker_inject` → `cmd_send`) reads `delivered=false` and falls through to `deliver::send` = SPOOL (poll-fed, never lost), reporting an honest `QUEUED`, never a confident-but-false `SENT`. The failure is LOUD (`ENDPOINT_INJECT:<ep>: no working translation binary … -> SPOOLED, not injected`). The raw-inject fallback (`input.enqueue`) is removed from the no-binary, worker-dropped, AND post-fault paths; the operator-keystroke floor-flush on FAULT is UNCHANGED (operator input is never stranded). Out of scope (follow-up): …\n- **Boundary — what this does NOT cover (the STATE-vs-transient precision):** the guarantee is the steady FAILED STATE — once a binary IS faulted/absent/worker-gone, subsequent deliveries spool (`faulted` is MONOTONIC: set once, never respawns, so the state is reached deterministically). It does NOT cover the FAULT-TRANSIENT: a delivery that lands in the worker's commit window — BEFORE `event_rx` is dropped / `faulted` is set — can be optimistically enqueue-acked (`delivered=true` the instant `event_tx.send` succeeds) then DROPPED when the worker faults+returns. This is a SEPARATE, PRE-EXISTING hazard (raw-inject removal did not touch it — the old code dropped that queued event too; v0.14.3 makes nothing worse) and is tracked for **v0.15.0 under `[REQ-MSG-DE…\n- **spt-core mapping:** impl `broker.rs` `dispatch_endpoint_input` (no-working-binary → `delivered=false` + loud log, no `input.enqueue`; the `input` writer is no longer resolved from the sessions table) + `build_translation`/`fault_translation` (`None`/fault now MEAN spool); unit `msg.rs` `endpoint_injected_envelope_carries_delivered_both_ways` (the `delivered=false` spool signal round-trips); int (real broker+PTY) `tests/broker.rs` `endpoint_keyed_inject_without_binary_spools_not_pty` + `tests/inject_control_wedge.rs` `large_endpoint_inject_to_a_no_binary_session_spools_promptly_without_wedging` + `g2_no_commit_deadline_faults_binary_and_does_not_wedge_controller_input` (post-FAULT inbound EVENTUALLY spools via bounded-retry-until-spool, marker NEVER on PT…\n- **Source:** v0.14.3 (ADR-0022 amendment @1eaeef1, operator-ruled; doyle-scoped, the F-019 follow-up). CHANGE-5 (`cli.rs` honest report) verified a NO-OP — the spool fall-through already reported `QUEUED`, never `SENT`.\n<!-- [doc->REQ-HAZARD-IDLE-SILENT-NONDELIVERY] -->\n\n---\n\n### 7.23 A message that has REACHED a node's spool must NEVER depend on an adapter hook-poll cadence to reach an spt-hosted (relay-less) endpoint — the daemon drives delivery on the events it owns  `[REQ-HAZARD-DELIVERY-STARVATION]`\n- **Failure (F-023, RCA @ `f023-f024-wan-idle-starvation`):** a message that has already landed in a node's spool — WAN-arrived, or locally spooled-while-active — stranded INDEFINITELY on an spt-hosted, relay-LESS endpoint whenever the adapter's hooks went quiet (an idle session with no user turns). An spt-hosted perch has no `api listen` relay to wake it, and NOTHING in the daemon re-offered a spooled message once the arrival moment passed — delivery silently depended on the adapter happening to call `api poll`. Presented as \"sent but never lands\" with a HEALTHY translation binary and a visibly-idle perch (F-023: verified 7 min idle, binary alive, zero injection). TWO gaps: (1) the WAN-ingress path (`receive_wan`) tried the harness-hosted TCP relay then fel…\n…\n- **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).\n…\n\n…\n### 7.26 Concurrent first-touch of ONE fresh BranchStore must ALL succeed — a non-atomic `git init` race must never strand a first-toucher  `[REQ-HAZARD-STORE-INIT-RACE]`\n…\n<!-- [doc->REQ-HAZARD-CONTROL-STAMP-LIFETIME] -->\n### 7.27 A control/viewer stamp must NEVER outlive its session — every teardown path clears what attach stamped  `[REQ-HAZARD-CONTROL-STAMP-LIFETIME]`\n- **Failure (F-026 #2, live evidence HFENDULEAM):** an spt-hosted endpoint stayed `ONLINE+CONTROLLED` after the operator's RC `/exit` — hours later hall-a's `info.json` still read `controlled:true` (status offline, dormant) and hfenduleam still GOSSIPED `controller_node=self` cross-node (SPT_DEV.json snapshot). ROOT: `/exit` kills the CHILD, not the controller connection, so the endpoint suspends via child termination and the teardown runs the REAP path (`broker.rs` exit-waiter → `sessions.remove(&id)`), NEVER the controller-detach path (`detach_if`→`clear_controller`→`stamp_driven_by`, the ONLY stamp-clear). The exit-waiter had no stamp clear, so the OutputLog dropped with the controller slot occupied and `controlled` / `viewer_count` / (`driven_by` for a r…\n- **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-effort like the sibling stampers (empty endpoint ⇒ nowhere to record). The residual ~1 min `ONLINE` tail after child death is the roster liveness-decay window (separate, lower priority) — but `CONTROLLED` no longer lies.\n- **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(2)` past reap). Killing the child (not dropping the conn) isolates the reap leg — the detach path never runs.\n- **Source:** F-026 W1 #2 (BUILD-F026-PICKERTRUTH; doyle triage + gate, todlando build 2026-07-03).\n\n---\n\n<!-- [doc->REQ-STORE-CONTEXT-BRANCH-FILL] -->\n### 7.28 A relative manifest path resolves against the ENDPOINT, never the daemon  `[REQ-STORE-CONTEXT-BRANCH-FILL]`\n- **Class rule:** a relative manifest path naming a filesystem location the ENDPOINT reads or writes MUST resolve against the endpoint's own cwd (its `info.json.cwd`, read at USE time), never the daemon's process cwd. A single daemon hosts many endpoints with different cwds, so a relative path frozen or read against the daemon cwd points at a directory no endpoint uses. Keys that resolve via `{cwd}` template substitution (`[digest].source`, `[history].locate_template` — `{cwd}` filled from `info.cwd`) or that the daemon fills with a computed ABSOLUTE path (`[message-idle-translation-binary].path` ← install-dir/PATH program resolution) are already endpoint-anchored and out of scope; the verbatim-`PathBuf::from` keys are the risk.\n- **Failure (F-026 SI-1, box-wide, evidence-complete):** the two-tier context BranchStore (`tracked/.seed.git`) held ZERO `a-*`/`p-*` branches on HFENDULEAM despite months of live-agent use — 0 objects, unborn HEAD, no `projects/` worktree checkouts, no commune drop files anywhere (psyche context flowed via the SEPARATE spt-agent-storage sync, masking it). ROOT: `BrainLifecycle::with_config_in` used `manifest.session.commune_dir` / `signoff_dir` VERBATIM (`PathBuf::from`). claude-spt declares `commune_dir = \".claude\"` (RELATIVE, documented \"resolved per-endpoint against its cwd\"), so `drop_dirs()` yielded a bare `.claude` that `ingest_drops` joined against the DAEMON's cwd (`drop_path.exists()==false` every pulse → `continue` → zero ingest → zero commit). Co…\n- **Invariant:** drop-dir resolution + project-id derivation happen AT INGEST TIME against the endpoint's fresh `info.cwd` (`pulse_tick`, not frozen at construction): absolute → as-is; relative + a recorded cwd → joined under it; relative + NO cwd → SKIPPED with a LOUD-once `DROP_INGEST_SKIP` diagnostic (never guessed, never the daemon cwd). Project-tier routing is owlery-gated — an owlery-internal anchor (psyche-host session) resolves its drops verbatim but routes NO `p-<project>` branch (empty project_id → `route_slices` skips `commit_project`; mirrors the picker's owlery exclusion); a non-owlery anchor derives `project_id` via the SAME `project_id_for_dir` the picker uses (category-key parity). Pure back-compat: absolute manifests unchanged, no wire chang…\n- **spt-core mapping:** impl `spt-daemon/src/lifecycle.rs` `resolve_endpoint_drop_dir` + `is_owlery_internal` + `warn_no_cwd_once` + `pulse_tick` (reads `info.cwd`, resolves each drop dir, derives owlery-gated `project_id`) + `spt-live/src/ingest.rs` `route_slices` (empty-project_id skips the project tier); unit `resolve_endpoint_drop_dir_cases` + `is_owlery_internal_cases` (lifecycle.rs) + `empty_project_id_skips_project_tier` (ingest.rs); int (real pulse + real store) `relative_commune_dir_resolves_against_endpoint_cwd_and_fills_project_branch` — a RELATIVE `.claude` + a real endpoint cwd → the drop is found and `a-<id>` + `p-<project_id_for_dir(cwd)>` commit (RED-first: verbatim-relative resolution → 0 ingested, store pristine).\n- **Source:** F-026 SI-1 (BUILD-F026-PICKERTRUTH; doyle RCA-gate + design ruling, todlando build 2026-07-03). Hazard-class sweep confirmed commune_dir+signoff_dir the ONLY verbatim instances.\n\n---\n\n<!-- [doc->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE] -->\n### 7.29 Control/viewer stamps CONVERGE to broker session-table truth, not merely edge-trigger  `[REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]`\n- **Failure (F-026 stamp-gap, hall-b + the original ball-b):** a picker-created endpoint (`endpoint run` → new) read plain `ONLINE` in the list + picker while genuinely driven — `info.json` `controlled:false` throughout. ROOT (the UPWARD companion to 7.27's downward edge-clear): the broker spawn path's `become_controller` → `stamp_driven_by` → `set_controlled(true)` fires at SPAWN time, but a FRESH endpoint has NO PERCH yet (the adapter binds it after claude boots), so `mutate_info` returns `NotFound` and every stamper swallows it (`let _`). The adapter's bind then writes `InfoJson::new` with `controlled:false` DEFAULT, and since the operator never re-attaches, no later EDGE ever re-stamps → the endpoint reads uncontrolled FOREVER while driven. The 7.15/7.27…\n- **Invariant:** the broker (SINGLE writer) re-asserts each live session's control/viewer stamps to session-table TRUTH, DIVERGENCE-GATED — read the perch's recorded `(driven_by, controlled, viewer_count)`, compare to the live truth (`controller_by()` / `has_controller()` / live `viewer_count`), and write ONLY on a real difference (no per-poll fsync storm). The re-assert rides the `KIND_SESSIONS` handler (the daemon reconcile + picker poll it on cadence), so a fresh perch CONVERGES within one reconcile-poll window after bind — a BOUNDED window, no new timer. Truth is SNAPSHOTTED under each log lock; the convergence writes run OFF the lock (the lock-across-effect discipline, 7.12/5.16). Event-on-input is INSUFFICIENT (an idle controlled session — the operator…\n- **Coverage boundary (who MAY clear vs who MUST NOT — cross-ref 7.15):** the convergence's `driven_by`-clear leg fires ONLY when `has_controller()==false` — the DEFINITIVE broker signal (it reads its OWN controller slot, not the ambiguous `controller_by`). That is why the broker MAY clear a stale `driven_by` where the BRAIN reconcile (7.15) MUST NOT: the brain only sees `controller_by==None`, which a live LOCAL controller also reads, so a brain clear would false-heal a genuinely-driven session. This leg is belt-and-suspenders over the clean-disconnect `detach_if`→`clear_controller` (which already zeroes `driven_by` on a clean drop). The convergence does NOT close the HALF-OPEN wedged-remote residual — an abandoned REMOTE controller whose conn stays OPEN (no…\n- **spt-core mapping:** impl `broker.rs` `stamp_divergence` (pure gate) + `converge_perch_stamps` (off-lock, single-writer, skips a perch-less/corrupt endpoint) + `OutputLog::has_controller`/`live_viewer_count` (snapshot accessors) + the `KIND_SESSIONS` handler (snapshot-under-lock → converge-off-lock); unit `broker.rs` `stamp_divergence_gates_writes` (no-diff → no write; `controlled false→true` = the stamp-before-bind case; driven_by / viewer / both); int (real broker+PTY) `control_stamp_lifetime.rs` `converge_stamps_on_sessions_poll_after_late_bind` (spawn with NO perch → spawn stamp swallowed → LATE bind writes `controlled:false` → a `KIND_SESSIONS` poll converges `controlled=true`; RED-first: hook disabled leaves `controlled:false` forever).\n- **Source:** F-026 W1-addendum (BUILD-F026-PICKERTRUTH; doyle stamp-gap dispatch + convergence ruling + KIND_SESSIONS-hook approval, todlando build 2026-07-03).\n\n### 7.30 A Psyche failure of ANY shape must NEVER remove or alter the parent endpoint's ready/hosted state  `[REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION]`\n<!-- [doc->REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION] -->\n- **Failure (paid-for, field brick 2026-07-04, adapter v0.13.2):** the pre-F-030 model kept a **resident** Psyche process the daemon supervised, with residency machinery (`confirm_residency_or_unhost`) that **un-hosted the parent endpoint** when the resident child went missing. A bad adapter ship (v0.13.2) made the psyche shim exit on every turn; the residency machinery read that as a lost resident and **tore down the parent's hosted state — the ready marker was removed and never re-stamped**, so every `--force-native` delivery then gated `cli-gate-not-hosted` PERMANENTLY. A Psyche problem silently bricked the parent LiveAgent. (Companion churn class: 7.31.)\n- **Invariant:** a Psyche is a **bounded per-event turn**, not a resident process — its liveness is that turns succeed, so there is no \"resident is gone\" signal to react to. A psyche turn failure of ANY shape (spawn fail, non-zero exit, timeout, wedge) stamps **psyche fields only** (`psyche_host_error` on the Self perch) and **NEVER** touches the parent endpoint's `status` / ready marker / hosted state; the endpoint stays online and deliverable while its Psyche is unwell. The teardown-on-psyche-trouble path is **deleted** with the residency machinery. `REQ-HAZARD-LIVEHOST-NONRESIDENT`'s spirit (a psyche that cannot work must be visible on the perch, hosting must not churn) transfers to the per-event failure budget (7.31); its traceable entry carries a SUPERS…\n- **spt-core mapping:** the residency/reap DELETIONS in `spt_daemon::livehost` (`reconcile_once`'s stop-side no longer un-hosts on psyche trouble; `host_one` holds no resident child) + `spt_daemon::lifecycle::run_psyche_event_turn` (a turn failure stamps `psyche_host_error` only) + the `first_turn_psyche_context` non-empty guarantee (a zero-context fresh turn can't masquerade as continue → reseed churn). Conformance int = the hall-bf shape: multi-subnet home, live endpoint, a psyche turn that fails every fire → parent ready marker PRESENT + deliverable stays TRUE, endpoint NOT un-hosted, no rehost churn, `psyche_host_error` stamped (the wave's heart).\n- **Source:** F-030 (psyche-ephemeral) — paid-for by the hall-bf churn (ordinal 6491+) and the adapter v0.13.2 bad-ship brick, 2026-07-04.\n\n### 7.31 The Psyche failure budget must count REAL per-event attempts — a resident rate-guard is blind to per-event churn  `[REQ-HAZARD-THRASH-GUARD-BLIND]`\n…\n\n…\n### 7.33 NO bare `.lock().unwrap()` on a broker-resident lock reachable from serve/dispatch — a poison permanently wedges every attach  `[REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]`\n<!-- [doc->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON] -->\n- **Failure (paid-for class):** a brain-only self-update restarts the BRAIN but keeps the BROKER process — and every one of its `Mutex`es — ALIVE by design (REQ-UPD-3). So a single panic while another thread held a broker-resident lock POISONS it permanently: the next `.lock().unwrap()` panics too, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines (`spt rc` → \"brain IPC read deadline elapsed\") while non-locked ops keep working. The effect journal (bug #16, `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`) and the inject floor (`[REQ-HAZARD-INJECT-WORKER-POISON]`) were fixed one lock at a time; the SURVIVING class was the attach-path locks — the session map (`sessions` ×18 + its `sessions_exit` alias), the per-session `OutputLog` ring (×1…\n- **Invariant:** NO broker-resident lock reachable from serve/dispatch may use a bare `.lock().unwrap()`. Recover via the shared `recover` (`into_inner` — safe for the short, coherent-on-recovery map ops of `sessions`/`pair_holds`) or, for the `OutputLog` ring, `recover_log` — which adds a COHERENCE CLAMP on the recovery path: a panic mid-`append` can leave the ring torn (over-cap, a last seq not below `next_seq`, non-monotonic), and serving those bytes risks garbage, so `clamp_or_reset` cheap-checks the invariants and RESETS the ring empty (`next_seq` preserved — cursors never rewind) + loud-logs on violation. Fail-fast on the log would REINTRODUCE the wedge; blind recover would serve torn bytes; clamp-or-reset costs only scrollback that self-heals on the n…\n- **spt-core mapping:** `broker::recover` / `broker::recover_log` / `OutputLog::clamp_or_reset`. unit = a poisoned session-map recovers to a usable guard (the next attach still opens), a torn ring clamps/resets-empty only when torn (coherent untouched, `next_seq` preserved), and `recover_log` wires the clamp onto the poison branch — each RED-first against a bare `.lock().unwrap()` / an un-clamped `into_inner`. int (scripted panic-under-lock during concurrent attach → next attach opens) deferred to the two-host rig.\n- **Source:** REMOTE-TRUTH B-1 (pivoted — the triage's named floor sites were already closed by `[REQ-HAZARD-INJECT-WORKER-POISON]`; the surviving attach-path lock class is the real root), doyle B-1 ruling (recover all three via one helper + the OutputLog ring coherence clamp).\n\n### 7.34 A dead `rec.pid` on an spt-hosted perch is EXPECTED — no reader may alive-gate on `rec.pid` alone  `[REQ-HAZARD-DEAD-REC-PID]`\n<!-- [doc->REQ-HAZARD-DEAD-REC-PID] -->\n- **Failure (paid-for, field evidence — the F-026 #11 dead-pid class):** an spt-hosted endpoint's `rec.pid` records the ephemeral bind-CLI pid, which dies IMMEDIATELY after bind (the broker holds the PTY; there is no resident harness process at that pid). Readers that alive-gated on `rec.pid` alone treated the perch as stale: self-detect leg (c) could NEVER resolve an spt-hosted sender (its from-stamp degraded to `cli@NODE`, operator #7, and replies bounced `NO_PERCH` — sighted on hall-bf/ball-b, v0.24.0).\n- **Invariant:** a dead `rec.pid` on an spt-hosted perch is an EXPECTED state, not staleness. NO reader may alive-gate on `rec.pid` alone: spt-hosted LIVENESS comes from the daemon-managed `status` field (KH 2.5 — status present ⇒ authoritative, never a per-pid probe); IDENTITY comes from session/ancestry resolution, where `rec.parent_pid` (the harness pid, the stable session-binding anchor) is the ancestry candidate (ADR-0021 seed-hint discipline: pid is a bind-time SEED, not a truth anchor — re-anchoring truth in `rec.pid` is overruled by design). Any newly sighted `rec.pid`-alive-gating reader gets the same scoped fix and extends THIS requirement's evidence — no new REQ per reader.\n- **spt-core mapping:** `roster.rs` `detect_self_by_ancestry` (the first sighted reader, fixed by `[REQ-SELF-DETECT-PARENT-PID]`: `parent_pid` candidates alongside `rec.pid`). int = the E-1 red-first (`detect_self_resolves_spt_hosted_perch_via_parent_pid`: dead `rec.pid` + live-ancestor `parent_pid` resolves), dual-tagged as this class's test.\n- **Source:** REMOTE-TRUTH E-1 rider (doyle ruling 2026-07-05: scoped reader-side (b) over re-stamping `rec.pid` (a) — ADR-0021 ground truth, migration hole, blast radius).\n\n### 7.35 The cached ceremony-clock NTP offset must NOT survive an OS clock STEP — an offset measured against the pre-step clock strands every pairing for the TTL  `[REQ-HAZARD-CEREMONY-CLOCK-STEP]`\n- **Failure (paid-for, field evidence — the enlyzeam BIGNET-join RCA 2026-07-06):** enlyzeam's `w32time` was STOPPED, so its system clock drifted +3m30s; the pairing ceremony offset (REQ-PAIR-8, `ntp.rs`) correctly corrected the TOTP clock — until an operator `w32tm /resync` STEPPED the OS clock back −210s under the LIVE daemon. The cached offset had been measured against the OLD (drifted) clock and its snapshot was monotonic-only (`when: Option<Instant>`), so the step was invisible to the cache: for up to the 15-min TTL the ceremony clock stayed ~7 TOTP steps in the past, every `subnet join` returned NO_SEED_HOLDER while fresh-process probes met in <500ms, and only a daemon bounce (which forced a fresh query) healed it. Timeline-proven: refresh cadence 15:0…\n- **Invariant:** the offset cache MUST detect an OS clock step and re-query, never apply a stale offset across it. The snapshot stores an `(Instant, SystemTime)` PAIR; on every read, if the wall clock diverged from the monotonic clock since the snapshot beyond a small tolerance (`|wall_elapsed − mono_elapsed| > ~2s`) the OS clock STEPPED ⇒ force an immediate re-query (offset recomputed against the new clock) regardless of TTL. Belt-and-braces at the join edge: `meet_seed_holder`, on search-deadline exhaustion, forces ONE fresh NTP query and runs one final sweep at the corrected step before the NO_SEED_HOLDER verdict — so a stepped-clock join self-heals WITHOUT a daemon bounce. The step tolerance sits far below one TOTP step (30 s) and well above scheduling j…\n- **spt-core mapping:** `spt_net::net::pairing::ntp` — `CachedOffset{when, wall_at}` pair + `refresh_needed`/`clock_stepped`/`signed_secs` (step-detect) + `force_refresh`/`invalidate` (the meet-exhaustion re-query seam); `spt_daemon::pairhost::meet_seed_holder` via `sweep_then_final_retry` (force-refresh + one final sweep on exhaustion). unit = `clock_step_forces_refresh_via_injected_reads` (injected mono/wall reads: a wall step forward OR back with mono barely moved re-queries; agreeing clocks within TTL do not; TTL-expiry and never-queried also refresh — no 15-min sleep) + `invalidate_clears_the_snapshot` + `meet_retries_once_after_refresh_on_exhaustion`/`meet_final_retry_failing_surrenders_once` (exactly one post-refresh retry, then surrender with the ric…\n- **Source:** JOIN-TRUTH W1/D3 (doyle /diagnose enlyzeam RCA; the offset defeated by a live clock step is the D3 of four defects, D1 multihome + D2 loud-fail + D4 verbose-clock the siblings).\n\n### 7.36 The broker control plane and PTY fan-out must NEVER block on a single subscriber connection — a suspended brain conn must not wedge control  `[REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]`\n<!-- [doc->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE] -->\n- **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 controller BLOCKED (not exited) on an IDLE session was evicted by NEITHER. Within seconds: attached output froze, `detach` did not release the control stamp (release routed through the wedged conn), reattach was REFUSED (`controlled-by` latched), `rc --take` hung, and `d…\n- **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 stall-evicts it. Eviction is triggered TIME-based, not output-driven: at the take/reattach path (`resolve_subscribe`, so control self-heals against broker truth even on an idle session) AND at the `KIND_SESSIONS` reap. (2) The stamp release is BROKER-side — `clear_c…\n- **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` (`KIND_STALL_EVICTS` IPC); `brain.rs` `Brain::stall_evicts`; `cli.rs` `render_stall_evict_line` (daemon-status surface, public wording). unit = `controller_write_stall_predicate_distinguishes_wedged_from_idle` + `stalled_incumbent_is_evicted_on_reattach_and_cont…\n- **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\n### 7.37 The Layer-1 settle-gate must RE-ARM per delivery on an observable PTY — a mid-session reader reattach (`/clear`) re-creates the head-swallow race  `[REQ-HAZARD-INJECT-SETTLE-REARM]`\n<!-- [doc->REQ-HAZARD-INJECT-SETTLE-REARM] -->\n- **Failure (paid-for, field-CONFIRMED on 0.29.0 — doyle diagnosis + perri screenshot `WindowsTerminal_6iSjya8pMt.png`):** the shipped W5-A settle-gate (7-of-W5, `REQ-INJECT-MULTILINE-INTEGRITY`) gated Layer 1 behind a worker-local ONE-SHOT (`settled_once`) on the premise that the head-swallow race is a STARTUP condition (the input reader not yet attached after spawn). That premise is FALSE: a mid-session `/clear` re-enters the harness's raw-mode input reader, re-attaching it and RE-CREATING the pre-settle window — but the one-shot had already fired at spawn, so `settle_before_inject` was SKIPPED and the head was eaten again. A checkpoint-wake payload injected right after `/clear` arrived head-truncated (mid-path `spt/Cargo.toml)`); echo-verify (Layer 2) is …\n- **Invariant:** the settle-gate re-arms before EVERY delivery on an OBSERVABLE (echoing/interactive) PTY — the bug-prone class, where re-settling is cheap (a tick or two once the reader answers) and necessary (a mid-session reader reattach must be re-confirmed). The steady-state settle is latch-skipped ONLY where the probe is known UNOBSERVABLE on this PTY (a non-echoing ConPTY whose DSR answer never surfaces): there is no reader-reattach race to guard, and each settle would burn the full `INJECT_SETTLE` deadline. The class is discriminated by the settle's OWN return value — `settle_before_inject` returns `true` iff the session ring advanced (probe observed) — latched on the FIRST attempt only; a RE-DRIVE (`attempt > 1`, reached only on a swallowed head) AL…\n- **spt-core mapping:** `broker.rs` `run_inject_worker` — the `settled_once: bool` one-shot is replaced by a `probe_unobservable: bool` latch (init `false` = assume observable → settle first delivery); the gate is the pure `should_settle(attempt, probe_unobservable) = attempt > 1 || !probe_unobservable`, and the first-attempt settle's `observed` return sets `probe_unobservable = !observed`. `settle_before_inject` gains a `bool` return (observed vs timed-out). unit = `should_settle_rearms_on_observable_pty` (observable PTY re-settles each delivery; unobservable-probe PTY skips the steady-state settle; a re-drive settles in both classes — RED-first: restore the one-shot and the observable-PTY assert flips). No int edit: the `inject_control_wedge.rs` mocks (`fi…\n- **Source:** post-0.29.0 field fix (doyle diagnosis + design ruling `BUILD-SPEC-inject-settle-rearm`; todlando build 2026-07-08). An impl bug in the shipped W5-A Layer-1 fix, not a new invariant class — the settle-gate was correct but under-armed.\n\n### 7.38 EVERY write on a physical broker connection is bounded + cancelable + poison-on-failure — no writer holds the send gate across an UNBOUNDED OS write  `[REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]`\n<!-- [doc->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK] -->\n- **Failure (paid-for, field capture + deterministic Windows repro 2026-07-09 — hertz RCA, UPDATE-WEDGE round-4):** `controller_writer` held the `SharedSend = Arc<Mutex<SendHalf>>` guard ACROSS a blocking `write_frame`. On Windows, `interprocess 2.4.2` routes the send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — this local-socket send path exposes **no supported write timeout** (`set_timeout` → `Unsupported`). An `rc --take` controller consumer that stopped reading blocked the write **indefinitely** (~127.95 s in the capture, released only when a brain restart tore the connections down). The 7.36 logical `stall_evict_controller` removed the controller ROLE but neither canceled the in-flight pipe write, closed the physical connection, nor called `Cance…\n- **Invariant:** every write on a physical broker connection rides ONE broker-owned bounded/cancelable framed-write primitive with an **independent out-of-band aborter**. The `SendHalf` never leaves the conn object (a bypass cannot compile). An ABSOLUTE deadline stamped at write entry covers BOTH the serialized gate-wait AND OS write completion; a per-conn watchdog fires at that deadline and aborts the in-flight op — never relying on the write returning or on opportunistic stall-evict sampling. On deadline / partial write / cancellation / unknown completion: (1) poison the whole physical conn idempotently, (2) abort read+write so `handle_conn` reaches existing EOF cleanup, (3) wait for the canceled op to report completion before releasing its buffer, (4) NEV…\n- **spt-core mapping:** `spt-daemon/src/conn.rs` `BrokerConn` (Condvar-gated serialized write gate + poisoned state + per-in-flight op id + watchdog + `abort_physical()`: cfg(windows) `CancelIoEx` → completion handshake → `DisconnectNamedPipe` raw kernel32 externs; cfg(unix) `UnixStream::shutdown(Both)`); `broker.rs` `SharedSend = Arc<BrokerConn>` + `controller_writer`/`viewer_writer`/`send_frame`/`send_error` routed; `nethost.rs` stream-log/presence sends routed. int = `brain_decouple.rs` `non_draining_controller_stall_evict_releases_writer_and_connection` (`#[cfg(windows)]`, the hertz 9-step: real PTY burst, black-holed remote controller, exactly one logical stall-evict via an independent draining conn, the black-holed conn's `handle_conn` finishes ≤2 s of…\n- **Source:** UPDATE-WEDGE round-4 (hertz root doc `docs/UPDATE-WEDGE-ROOT-CAUSE.md` @f8596ca; doyle gate ruling `docs/UPDATE-WEDGE-RCA-DISPATCH-todlando.md`; todlando build 2026-07-09). The residual 7.36 left open: logical eviction without physical retirement.\n\n---\n\n### 7.39 Per-session identity env (`SPT_ENDPOINT_ID`/`OWL_SESSION_ID`/`SPT_AGENT_ID`) is NEVER inherited — the daemon scrubs it at startup AND on every role spawn, regardless of any role's declared `env_remove`  `[REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]`\n<!-- [doc->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE] -->\n- **Failure (paid-for, perri field RCA 2026-07-09/10 — F-036 psyche seat-theft):** a daemon restarted from inside an agent session (routine during core dev / `spt update apply`) carried the session's `SPT_ENDPOINT_ID=doyle` and passed it verbatim into every `[session.psyche_resume]` spawn — core stripped only each role's DECLARED `env_remove` list (runtime.rs `command_for`), so ONE adapter `env_remove` miss infected the whole node. Every psyche claude turn fired SessionStart, the adapter hook saw the leaked endpoint id, took the bind path, and ROTATED the victim's perch onto the psyche's own custody sid with a valid prior-sid proof — every pulse. Field blast radius: lia/deployah/doyle psyches ALL briefed as `<sptc-active-perch id=doyle>`; 37 peer msgs draine…\n- **Invariant:** the identity set (`spt_runtime::IDENTITY_ENV_VARS`) is per-session state and NEVER correct inherited state for a daemon or any child it spawns. The daemon (broker AND brain entries) scrubs its own process env first thing (`scrub_identity_env`); the shared role-spawn command builder scrubs the set unconditionally and LAST (after `env_remove`, read-env stamps, and the recursion guard), so no adapter declaration — missing or pathological — can leak identity into a role child. A spawn that NEEDS an identity var (a hosted harness) receives it by explicit per-spawn injection (manifest `[env] direction = \"inject\"`), never by inheritance. The same startup scrub also clears the inject-echo knobs (`spt_runtime::INJECT_ECHO_ENV_VARS`: `SPT_INJECT_VERIF…\n- **spt-core mapping:** `spt-runtime/src/runtime.rs` `IDENTITY_ENV_VARS` + `scrub_identity_env` + the `command_for` unconditional scrub; `spt-daemon/src/daemon.rs` `Daemon::run` / `Daemon::run_brain` startup scrub; `spt/src/api/startup.rs` `establish_perch` custody-squat refusal over `spt-store/src/psyche_custody.rs` `custody_squatter`.\n- **Source:** MSG-IDENTITY W1 (perri F-036 RCA; doyle dispatch `docs/MSG-IDENTITY-DISPATCH.md`; adapter half shipped claude-spt v0.18.8 — this is the core-layer defense so no adapter miss can ever leak identity again).\n\n---\n\n### 7.40 A commune/signoff drop is deleted ONLY after every applicable tier is durably committed — an un-committable slice preserves the drop, never delete-then-lose  `[REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]`\n<!-- [doc->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE] -->\n- **Failure (paid-for, perri field finding 2026-07-08 — F-032, data-loss):** `ingest_drops` unconditionally deleted the drop after `route_slices`, but the project tier is GATED on a non-empty `project_id` — when the endpoint's anchor cwd was unresolved/owlery-internal at ingest time, the `<project-context>` slice was parsed but never committed, yet the source drop was still deleted → the project-context content permanently lost. perri's two-sliced echo-commune INGESTED (file deleted) yet never surfaced at her next SessionStart resume-pull; adapter exonerated. Legacy spt held commit-first-then-delete parity; the modern two-slice ingest broke it.\n- **Invariant:** delete-after-commit, per slice. Distinguish a write **SUPPRESSED by precedence** (incoming older than durable → already superseded → safe to delete) from a slice **un-committable now** (empty `project_id`, or any write error → the `?` retry path). An un-committable non-empty project slice is preserved as a **project-only pending slice under the COMMUNE suffix, whatever the source drop's kind** — a signoff-suffix pending would be reaped by `sweep_stale_signoff` on the next listener start (the signoff sentinel is write-once-per-generation), recreating the black-hole on the signoff path; the commune-named pending is a pure project-slice carrier both suffixes ingest identically and the sweep never sees. The already-committed live slice is droppe…\n- **spt-core mapping:** `spt-live/src/ingest.rs` `ingest_drops` (deferred-project preserve + `Ingested.preserved`); `route_slices` project gate unchanged (`REQ-STORE-CONTEXT-BRANCH-FILL`); kin `REQ-HAZARD-DROP-FILE-SINGLE-WRITER` (core stays the single writer — the preserve rewrite is core's own write).\n…\n\n…\n\n\n[Some lines truncated to 768 chars]"}],"details":{"displayContent":{"text":"### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`\n<!-- [doc->REQ-HAZARD-VIEWER-ISOLATION] -->\n- **Failure:** the W2.5 controller/viewer model lets ANY number of read-only `--view` attachers ride one session's broker `OutputLog`. The single drain thread fans each output chunk to every attacher. If a viewer's socket is fanned out with a **blocking** write under the log lock (the controller's authoritative path), one wedged viewer (a slow terminal, a black-holed WAN peer, a hostile non-reader) stalls the drain — freezing the controller's stream and backing up the PTY child. A single watcher must never be able to degrade or hang the driver.\n- **Invariant:** the drain writes the **controller** on the authoritative blocking bounded path (it alone advances `delivered_through`), but each **viewer** gets an **isolated bounded SPSC queue + a dedicated writer thread**; the drain `try_send`s under the log lock and **evicts** any viewer whose queue is `Full` (fell behind the live stream) or `Disconnected` (its writer died on a dead socket) — the drain thread **never touches a viewer socket**, so no viewer write can backpressure it. A **soft cap** (`MAX_VIEWERS`) bounds the writer-thread count (a viewer attach beyond it is refused). Viewer eviction never perturbs the controller stream, the `delivered_through` cursor, or the child. Ring **replay-at-attach** is owned by the writer thread (not the bounded l…\n- **spt-core mapping:** `OutputLog::append` (controller blocking + `viewer_send_evicts` `try_send` fan-out), `OutputLog::add_viewer` (bounded `sync_channel(VIEWER_CHANNEL_DEPTH)` + `viewer_writer` thread), `MAX_VIEWERS` soft cap, `ViewerSink`. Unit: `viewer_overflow_or_disconnect_evicts_never_blocks`. Int: `wedged_viewer_does_not_stall_controller` (a non-reading viewer is evicted while the controller keeps receiving past a 200KB burst).\n- **Source:** M12 W2.5 controller/viewer model (doyle ruling 2026-06-14, Q1).\n\n### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`\n<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\n- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only via its OWN 30s read-deadline (the 7.6 A-half) — a 30s stall + a full pump restart EACH round, and the supervised restart re-dials the SAME dead peer and re-wedges. Root cause of the 2.2h hfenduleam wedge (2026-06-11) and its recurrence (2026-06-16).\n- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`, so the BROKER fires FIRST — the brain reconstructs `ErrorKind::Other` (its `net_dial`/`net_open_stream`/`net_stream_send` map a broker error reply to `io::Error::other`), NOT its own read-deadline `TimedOut`, so `pump::peer_outcome` takes the ordinary per-pee…\n- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIES an ordinary error within the bound + exactly-once-on-timeout: journal un-applied, conn table empty, clean redial) and `pump_survives_a_black_holing_peer_heartbeat_advances_no_restart` (the production pump against a dead peer — heartbeat monotonic-advances, `run…\n- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.\n\n### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`\n<!-- [doc->REQ-HAZARD-BROKER-SEED-WIRE-SKEW] -->\n- **Failure:** the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004's no-terminate-during-update pillar forbids auto-killing it — 6.7). A self-update that changes a daemon-state WIRE FORMAT — e.g. the v0.9.0 adapter-agnostic `Seed` (the `adapter` field dropped) — therefore lands a NEW-version CLI talking to the STILL-RESIDENT OLD broker. The old broker cannot deserialize the new `Seed` (its formerly-required `adapter` is absent), so it drops the seed-control conn without acking; the CLI's `put_seed` ack-read hits EOF and surfaces a raw `UnexpectedEof` \"failed to fill whole buffer\" — a cryptic footgun that hides the real cause (perri PREP-4 FINDING 1: v0.9.0 CLI ↔ stale 0.8.x broker).\n- **Invariant:** (a) spt-core surfaces an ACTIONABLE diagnostic on the seed-ack EOF — naming the stale-broker cause + the fix (`spt daemon stop`; the broker restarts on the next `spt api` call) — never the bare io error (scoped to `UnexpectedEof` on the seed-ack path, so it never mis-fires on an unrelated error like a refused connect). (b) A daemon-state wire change requires a DELIBERATE full broker restart; this is NOT automatic — ADR-0004 forbids auto-killing the resident broker, and a brain-only update keeps the old broker. (c) FORWARD discipline: daemon-state / `Seed` schema changes stay ADDITIVE + serde-default, so a resident OLD broker tolerates a NEW CLI across a brain-only update. (This would NOT have rescued 0.9.0 itself — the old broker's `adapter`…\n- **spt-core mapping:** `startup::seed_fail_message` (the `UnexpectedEof` → actionable-hint branch) fired from `cmd_seed`'s `put_seed` error arm; the broker's seed-control residency (`seedmap::serve_seed_control`, held across brain restarts — 6.7). Unit: `seed_fail_eof_gives_actionable_stale_broker_hint` (EOF → the `spt daemon stop` hint naming the stale broker; a non-EOF kind → the plain message, no mis-fire).\n- **Source:** perri PREP-4 FINDING 1 (v0.9.0 dogfood) → v0.9.1.\n\n### 7.10 A VIEW is independent from the endpoint — closing the launching tab must NOT reap the daemon-hosted harness  `[REQ-HAZARD-VIEWER-CLOSE-DETACH]`\n<!-- [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH] -->\n- **Failure (Windows):** `spt endpoint run` autostarts the daemon (`ensure_running` → `detached_no_inherit`), which INHERITS the launching terminal's Windows Job Object. Windows Terminal / VS Code place the shell AND every descendant in a Job with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; closing the tab drops the job's last handle → the OS reaps the daemon + its broker-spawned ConPTY harness subtree. The `rc` pump detaching should end ONLY the viewport; the harness must keep running + stay re-attachable. (ConPTY isolation itself is already correct — portable-pty builds the pseudoconsole in the daemon; the leaking lifetime binding is the Job Object, not the console.)\n- **Invariant:** both daemon spawn paths request `CREATE_BREAKAWAY_FROM_JOB` (0x0100_0000), best-effort with an in-job FALLBACK on `ERROR_ACCESS_DENIED`(5) / `ERROR_INVALID_PARAMETER`(87) so a breakaway-denying job NEVER regresses the spawn. **A real job CAN deny breakaway** (proven: the cargo/CI runner's own ancestor job ACCESS_DENIES it), so efficacy against the real terminal is UNKNOWN until measured. **int stage = OPERATOR MANUAL ACCEPTANCE, not CI** (doyle ruling 2026-06-18): the runner sits inside a breakaway-denying job and every test-created job nests inside it → a faithful \"harness survives tab-close\" test is a guaranteed FALSE-RED; the two units (escape-where-permitted self-skip + fallback-where-denied) are the CI evidence; `required_stages` stays …\n- **spt-core mapping:** `daemon.rs::detached_no_inherit` + `deelevate.rs::create_with_token` (the breakaway flag + fallback). Units: `detached_no_inherit_falls_back_under_a_breakaway_denying_job` (no-regression — the shared runner exercises the fallback directly), `breakaway_spawn_escapes_a_kill_on_close_job` (the OS escape mechanism — self-SKIPS where the ancestor job forbids breakaway). Unix: the daemon's `setsid` session-detach already keeps a closing terminal's SIGHUP off its children (guard test, no code).\n- **Source:** v0.12.0 real-harness defect (operator) → v0.12.1 L1 @5ae68f8; doyle design+int ruling 2026-06-18.\n\n### 7.11 A dead PTY child + a dropped operator pump must NOT wedge the broker for other clients  `[REQ-HAZARD-ATTACH-WEDGE]`\n- **Failure (hypothesized, v0.12.0):** a legitimately dead PTY child (crash/kill) plus an `rc` pump dropped without a clean detach (closed tab) was thought to make the broker's loopback forward `write_all` block forever on a full 64 KB duplex, park a worker in the 2-worker net runtime, saturate both, and stall every new attach / `endpoint run` (with `daemon stop` unable to join).\n- **Invariant — DISPOSITION = PROVE-DON'T-CHANGE** (doyle GATE-PASS @e883f45, 2026-06-18): the post-L0 code ALREADY prevents the wedge; NO fail-fast / worker-count code was added. (1) `serve_attach` forwards fire-and-forget (`net_stream_send` `op_id=None`) and the broker-side `send_stream` is already deadline-bounded (`bounded_block_on`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s — hazard 7.8), not forever. (2) the loopback duplex is drained broker-INTERNALLY by the operator row's OWN read pump (`nethost.rs` `RecvHalf::Loopback`), which for an ordinary attach stream (`retentive_cap == 0`) NEVER parks → `peer_w` never backs up on a dead `rc` (a dead `rc` is just a dropped IPC subscriber against a bounded, EVICTING ring). (3) `bounded_block_on` = `runtime.handle().bloc…\n- **spt-core mapping:** int `crates/spt/tests/attach_wedge_e2e.rs` (REAL detached daemon + dummy-harness fixture): serve the victim (rc sees its tick), abruptly kill rc (dropped pump) + kill the PTY child → a NEW endpoint still comes online + is served (no wedge), the dead endpoint is offlined within a tick (`LIVENESS_RECONCILE_OFFLINE`), `daemon stop` bounded. Leans on 7.8 (the QUIC-op deadline) + 7.7 (viewer isolation).\n- **Source:** v0.12.0 real-harness finding (operator) → v0.12.1 L2 @e883f45; doyle GATE-PASS 2026-06-18.\n\n### 7.12 Controller output must NOT be written inline on the drain thread — a backed-up controller wedges the session  `[REQ-HAZARD-INJECT-CONTROL-COEXIST]`\n- **Failure (v0.12.x, operator dogfooding + doyle /diagnose 2026-06-19):** `OutputLog::append` fanned each live chunk to the CONTROLLER via a SYNCHRONOUS, blocking `write_frame` held INLINE on the session's single drain thread while `Mutex<OutputLog>` was locked (viewers already had a dedicated writer thread + bounded evicting channel). A backed-up controller socket — a slow operator, or the full 64 KB `rc` loopback duplex under heavy TUI redraw — parked the drain thread WITH THE LOG LOCK HELD: output + keystroke-echo stalled and every attach / resize / `KIND_SESSIONS` that needs the log lock blocked FOREVER, while the broker process stayed alive. Fires on NORMAL interactive `rc` use under heavy output (not only message injection); REOPENED the wedge facet o…\n- **Invariant:** controller delivery runs OFF the drain thread — a dedicated controller writer thread + bounded channel (the 7.7 viewer_writer pattern) does the blocking socket write; the drain hands each chunk off with a BOUNDED, OFF-LOCK send (deadline → detach + `clear_controller`, never park forever, never silently evict a LIVE operator's authoritative view). The controller is AUTHORITATIVE (unlike a viewer): its writer advances the `delivered_through` resume cursor (atomic, only-on-success, monotonic). `clear_controller` is the ONE unlatch path (clears `driven_by`, drops the sink) — W5's reconcile self-heal reuses it.\n- **spt-core mapping:** impl `broker.rs` `OutputLog::append`/`controller_writer`/`become_controller`/`ControllerJob::deliver`/`clear_controller`; int `crates/spt-daemon/tests/inject_control_wedge.rs` (a fully-backed-up controller keeps `KIND_SESSIONS` answering = no wedge); unit the bounded-deliver + monotonic-cursor kernels. The input-side parks (write_input `write_all` on a full buffer; DSR-answer writer-mutex contention) are BENIGN on Windows ConPTY (absorbs a large inject), real only on Unix forkpty → bounded by W2's atomic-write substrate.\n- **Source:** v0.12.x operator wedge → v0.13.0 W1 (doyle GATE-PASS 2026-06-19).\n\n### 7.13 `spt rc` must SWAP Windows legacy-console Backspace/Ctrl+Backspace (`^H`↔DEL); do NOT enable VT console input  `[REQ-HAZARD-RC-INPUT-KEY-ENCODING]`\n- **Failure (operator dogfooding):** `spt rc` is a raw verbatim stdin byte pump; the legacy Windows console delivers Backspace as `^H` (0x08), and Claude Code maps `^H` → backward-kill-word, so every Backspace deletes a whole word. Confirmed bytes (operator HITL capture, real Windows Terminal): Backspace=0x08, Ctrl+Backspace=0x7f.\n- **REJECTED fix (dc07c39, reverted):** enabling `ENABLE_VIRTUAL_TERMINAL_INPUT` on the stdin console BACKFIRED — on Windows Terminal that flag yields **win32-input-mode**, not legacy xterm VT. Every key then arrives as a 6-field `ESC[Vk;Sc;Uc;Kd;Cs;Rc_` key-down/up record: (1) the ctrl-b detach broke (0x02 is wrapped, so `parse_stdin_chunk` never sees the raw `DETACH_PREFIX`), and (2) win32-input-mode was forwarded into Claude Code's own ConPTY input negotiation (garbage). The VT lever is wrong for Windows Terminal. (Lesson: verify a terminal-input fix on the REAL terminal via the operator re-capture — unit/clippy never exercise the live key path.)\n- **Invariant:** stay a raw byte pump and NARROW-normalize in the forward path — **SWAP** the two delete bytes: `0x08` → `0x7f` (VT DEL, char-delete) AND `0x7f` → `0x08` (^H, kill-word), AFTER the detach state machine consumes any `0x02`, leaving `0x02` and every other byte untouched. Because CC reads `^H`=word-delete and DEL=char-delete, swapping the *input* bytes gives Backspace=char-delete and Ctrl+Backspace=word-delete — the native Win11 convention (the operator rejected losing word-delete; the earlier one-way `0x08`→`0x7f` map dropped it). cfg(windows) only (Unix is already VT; a real `ctrl+h`/`ctrl+?` there must pass through). Accepted minor loss: a real `ctrl+h` (0x08) and a real DEL (0x7f) are swapped on Windows — both are delete keys, so the practic…\n- **spt-core mapping:** the Backspace/Ctrl+Backspace SWAP intent persists; the `normalize_key_byte` byte-swap was SUPERSEDED on Windows by **7.16** (`REQ-RC-KEY-VT-TRANSLATE`, v0.13.0 bug 2) — the agnostic key-event→xterm-VT `translate_key_event` now emits Backspace → `0x7f` and Ctrl+Backspace → `0x08` natively (carrying the relocated `[impl]`/`[unit]` evidence), so `normalize_key_byte` + its unit were removed. The live keypress is HITL operator re-capture.\n- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19; superseded-on-Windows by the bug-2 translator 2026-06-19).\n\n### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`\n- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two facets, one root: **(A) stutter** — measured fsync on `%LOCALAPPDATA%\\spt-core` median 6.5 ms / spikes 198 ms, ×2 per keystroke → choppy, worsens with volume; **(B) HARD WEDGE** — a blocking `PtyWrite` (ConPTY/forkpty input buffer not draining) held the lock indefi…\n- **Invariant:** `apply_once` RELEASES the inner lock across `effect()` — reserve the key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable, mark applied). Crash-idempotency comes from the per-key reservation + the applied-set, not from holding the lock across the effect (the brain-crash guarantee is unchanged: the broker survives the brain, so reserve→effect→finalize runs uninterrupted on the broker thread; a replay hits the applied-set and dedups). `EffectKind::PtyWrite` is **EPHEMERAL** — NO journal lines, NO fsync, in-memory dedup only (a keystroke lost to a broker crash is retyped; PTY state is never rebuilt from keystroke replay) — while durable kinds (spool/r…\n- **spt-core mapping:** impl `effect.rs` `apply_once` (reserve/release/finalize) + `EffectKind::is_durable`; unit (`effect.rs`) a barrier proving the lock is NOT held across `effect()` (a different key's `apply_once` completes while one effect blocks) + `PtyWrite` writes no journal line while a durable kind (`NetSend`) does (the no-fsync proxy); int `crates/spt-daemon/tests/inject_control_wedge.rs` (real broker+PTY, a stalled consumer + sustained blocking input effect: a concurrent `spt rc` attach stays serviceable AND actually receives PTY bytes — the assertion W1's gate lacked). Unix park-(b) folded here, gated on gravity-linux. doyle GATE-GUARD: assert structurally, NEVER on fsync wall-clock (env-dependent, flaky); the stutter is fixed as a consequence of…\n- **Source:** v0.13.0 W1b (operator dogfooding post-W1 escape; doyle /diagnose + measurement 2026-06-19).\n\n…\n### 7.18 `spt rc` paste is client-originated on Windows — read the LOCAL clipboard, inject a BRACKETED paste  `[REQ-RC-WIN-PASTE]`\n- **Failure (operator dogfooding):** in an `spt rc` session neither ctrl+V nor right-click pasted (CC explicitly supports ctrl+V). `RawGuard` did 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 translated to a 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. A multi-line paste-as-keys also became a `\\r` submit-storm.\n…\n- **spt-core mapping:** impl `rc.rs` `wrap_bracketed_paste` + `mouse_is_paste` + `clipboard_paste` + `read_clipboard` + `windows_mouse_wanted` + `RawGuard` (mouse capture/restore) + the `spawn_stdin_reader_events` right-mouse arm; unit the exact bracketed framing + content-verbatim + mouse classify (right-down ⇒ paste, all else ⇒ drop) + the injected-reader paste decision (non-empty ⇒ wrapped, empty/fail ⇒ no-op); NO int (live clipboard + console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent).\n- **Source:** v0.13.0 P1 (operator HITL; doyle design 2026-06-19). Depends on P0 (7.17). AMENDED v0.13.0 P1b: scope narrowed to right-click-only (the dead ctrl+V interception removed); scroll-forward is 7.20; the input-flood non-wedge is 7.19.\n\n<!-- [doc->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] -->\n### 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- **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 → the broker→brain return direction fills (~10 frames = the IPC pipe buffer) → `send_frame` BLOCKS → the handler stops reading → the brain's writes block → mutual full-duplex DEADLOCK. (Capture: 11 frames, `write_input` 11/11 — P0 holds; `ack send` START=11/END=10…\n- **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=true`). EXACTLY-ONCE preserved: the broker dedups by `(session, op_id)` at the applied-set regardless of the ack. **N-1 caveat:** an OLD resident broker (the self-update window) ignores `ack=false` → still acks → the deadlock persists until a broker restart (inher…\n- **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 session stays live + a concurrent attach opens (real broker+brain, no mocks).\n- **Source:** v0.13.0 P1b (operator HITL re-open; doyle /diagnose on the enhanced rc+broker capture 2026-06-19). The real ship-blocker behind the ctrl+V wedge.\n\n<!-- [doc->REQ-RC-MOUSE-FORWARD] -->\n### 7.20 `spt rc` must forward the scroll wheel to the harness (our mouse capture steals WT's native scroll)  `[REQ-RC-MOUSE-FORWARD]`\n- **Failure (operator HITL):** scroll stopped working in `spt rc`. P1's `EnableMouseCapture` (for right-click paste, 7.18) makes Windows Terminal forward ALL mouse — including the wheel — to rc instead of scrolling its own buffer, but the rc mouse handler dropped everything except right-button-down → scroll DIED (and WT's native scrollback is stolen under the capture).\n- **Invariant:** on Windows, TRACK the harness's mouse-reporting mode from its OUTPUT — scan for DECSET `ESC[?1000h/1002h/1003h` (mouse on) + `ESC[?1006h` (SGR ext) and their `…l` (off) into a shared `MouseMode{enabled,sgr}` (the pump writes from the output render path, the stdin reader reads); the scan survives a sequence SPLIT across output chunks (a bounded carry buffer). The mouse handler: right-button-DOWN → bracketed clipboard paste (7.18, unchanged); `ScrollUp/Down` → an xterm SGR mouse report (`ESC[<64;col+1;row+1M` up / `ESC[<65;…M` down; 0-based crossterm → 1-based xterm), forwarded ONLY when `enabled && sgr` (else DROP — a legacy report the harness may misread is garbage); Moved/drag/left/middle DROP (scroll is the need; click-forward risks garbag…\n- **spt-core mapping:** impl `rc.rs` `MouseMode` + `MouseModeScanner`/`parse_decset_private`/`apply_mouse_mode` (carry-buffer scan, fed from the pump's output path) + `scroll_dir` + `scroll_sgr` + the `spawn_stdin_reader_events` scroll arm (threaded via `Arc<MouseMode>`); unit `scroll_dir` classify + `scroll_sgr` exact bytes + the DECSET scan (set/reset, combined, mixed, and split-across-chunks); NO int (live console mouse = HITL).\n- **Source:** v0.13.0 P1b (operator HITL; doyle design 2026-06-19). Bundled with 7.19.\n\n<!-- [doc->REQ-HAZARD-CONTROLLER-WRITER-REORDER] -->\n### 7.21 Exactly ONE `controller_writer` per brain↔broker connection — a superseded writer must write nothing further  `[REQ-HAZARD-CONTROLLER-WRITER-REORDER]`\n- **Failure (doyle instrumented RACEDIAG repro, kitsubito):** on a brain-restart re-serve the handoff brain registered as controller on the SAME session TWICE over the SAME socket — `Brain::handoff` eagerly `subscribe(prior.next_seq=1)` → `become_controller(from_seq=1)`, spawning writer-A (writes seq 1); then `serve_attach` re-handled the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, spawning writer-B (writes 0,1). `become_controller` dropped the prior `ControllerSink` (its `tx`) but did NOT stop the prior writer — writer-A kept flushing its OWNED `initial` batch, and both writers held clones of one `SharedSend` (`Arc<Mutex<socket>>`) with no inter-thread ordering. When writer-A's seq 1 beat writer-B's seq 0, the stri…\n- **Invariant:** on a single brain↔broker connection exactly ONE `controller_writer` is ever the LIVE writer; a SUPERSEDED writer writes no further frames after the epoch bump it observes; and **every `controller_writer` emits a strictly ASCENDING seq stream** (sorted initial batch + ascending live frames). The CORRECTNESS guarantee that falls out: a snap-above consumer over any interleaving of ascending writers — where the surviving writer (`serve_attach`'s `attach_as(sid,0)`) offers the COMPLETE range `[0, end]` — delivers `[K, end]` with **no skip and no dup** (the first sighting of any seq `>M` is always preceded by a sighting of `M` on that same ascending writer, so `M` is delivered before the cursor can pass it). Enforced/relied-on three ways (NB: fix …\n- **spt-core mapping:** impl `brain.rs` `Brain::handoff` (KEEP the eager `subscribe`; seed `session_cursors`) + `Brain::subscribe_with` (resume-mode dedup-cursor reset to `from_seq`, shared by `attach`/`attach_as` — the operator-stream boundary fix) + `broker.rs` `OutputLog.controller_epoch: Arc<AtomicU64>` / `become_controller` (atomic `fetch_add`, passes the new epoch + `Arc::clone` into the writer) / `controller_writer` (epoch gate read UNDER the lock on both loops) / `mark_controller_gone` + the `ControllerJob` epoch read; unit (white-box, `src/broker.rs`) the epoch-gated writer (a superseded writer flushes nothing — only the latest writer's monotonic stream reaches the wire) + `handoff` seeds `session_cursors`/resubscribes; int (keystone, `tests/broker.…\n- **Source:** v0.13.0 P1c (operator-ruled root-fix before ship; doyle root-cause via instrumented repro 2026-06-20; design corrected across two gate rounds — fix #1 reverted, then the kitsubito RACEDIAG pinned the residual to the consumer-side operator-stream boundary, fixed by the `subscribe_with` cursor reset on `attach_as` re-subscribe). The last v0.13.0 ship-blocker.\n\n### 7.22 An idle delivery with no working translation binary must SPOOL, never raw-inject a pseudo-delivery reported as delivered  `[REQ-HAZARD-IDLE-SILENT-NONDELIVERY]`\n- **Failure (F-019 post-mortem):** the v0.11.0 path raw-injected `payload+\\r` into the spt-hosted PTY whenever no working translation binary handled an inbound message — none declared, spawn-failed, FAULTED (commit-deadline miss / binary death), or its inject-worker channel dropped — AND acked `delivered=true`. But a bare `payload+\\r` does NOT submit on a modern TUI (Claude Code): the message was TYPED into the harness but never sent, a silent pseudo-delivery reported as success. That silent degrade-to-raw-inject is precisely what MASKED F-019 (`[REQ-INSTALL-11]`, above) through a multi-hour black-box hunt — every behavioral hypothesis was moot because the \"delivery\" never delivered.\n- **Invariant:** spt-hosted idle delivery is translation-binary-ONLY (ADR-0022 amendment). `dispatch_endpoint_input` with no working binary replies `endpoint_injected_envelope(ep, delivered=false)` and writes NOTHING to the PTY; the caller (`try_broker_inject` → `cmd_send`) reads `delivered=false` and falls through to `deliver::send` = SPOOL (poll-fed, never lost), reporting an honest `QUEUED`, never a confident-but-false `SENT`. The failure is LOUD (`ENDPOINT_INJECT:<ep>: no working translation binary … -> SPOOLED, not injected`). The raw-inject fallback (`input.enqueue`) is removed from the no-binary, worker-dropped, AND post-fault paths; the operator-keystroke floor-flush on FAULT is UNCHANGED (operator input is never stranded). Out of scope (follow-up): …\n- **Boundary — what this does NOT cover (the STATE-vs-transient precision):** the guarantee is the steady FAILED STATE — once a binary IS faulted/absent/worker-gone, subsequent deliveries spool (`faulted` is MONOTONIC: set once, never respawns, so the state is reached deterministically). It does NOT cover the FAULT-TRANSIENT: a delivery that lands in the worker's commit window — BEFORE `event_rx` is dropped / `faulted` is set — can be optimistically enqueue-acked (`delivered=true` the instant `event_tx.send` succeeds) then DROPPED when the worker faults+returns. This is a SEPARATE, PRE-EXISTING hazard (raw-inject removal did not touch it — the old code dropped that queued event too; v0.14.3 makes nothing worse) and is tracked for **v0.15.0 under `[REQ-MSG-DE…\n- **spt-core mapping:** impl `broker.rs` `dispatch_endpoint_input` (no-working-binary → `delivered=false` + loud log, no `input.enqueue`; the `input` writer is no longer resolved from the sessions table) + `build_translation`/`fault_translation` (`None`/fault now MEAN spool); unit `msg.rs` `endpoint_injected_envelope_carries_delivered_both_ways` (the `delivered=false` spool signal round-trips); int (real broker+PTY) `tests/broker.rs` `endpoint_keyed_inject_without_binary_spools_not_pty` + `tests/inject_control_wedge.rs` `large_endpoint_inject_to_a_no_binary_session_spools_promptly_without_wedging` + `g2_no_commit_deadline_faults_binary_and_does_not_wedge_controller_input` (post-FAULT inbound EVENTUALLY spools via bounded-retry-until-spool, marker NEVER on PT…\n- **Source:** v0.14.3 (ADR-0022 amendment @1eaeef1, operator-ruled; doyle-scoped, the F-019 follow-up). CHANGE-5 (`cli.rs` honest report) verified a NO-OP — the spool fall-through already reported `QUEUED`, never `SENT`.\n<!-- [doc->REQ-HAZARD-IDLE-SILENT-NONDELIVERY] -->\n\n---\n\n### 7.23 A message that has REACHED a node's spool must NEVER depend on an adapter hook-poll cadence to reach an spt-hosted (relay-less) endpoint — the daemon drives delivery on the events it owns  `[REQ-HAZARD-DELIVERY-STARVATION]`\n- **Failure (F-023, RCA @ `f023-f024-wan-idle-starvation`):** a message that has already landed in a node's spool — WAN-arrived, or locally spooled-while-active — stranded INDEFINITELY on an spt-hosted, relay-LESS endpoint whenever the adapter's hooks went quiet (an idle session with no user turns). An spt-hosted perch has no `api listen` relay to wake it, and NOTHING in the daemon re-offered a spooled message once the arrival moment passed — delivery silently depended on the adapter happening to call `api poll`. Presented as \"sent but never lands\" with a HEALTHY translation binary and a visibly-idle perch (F-023: verified 7 min idle, binary alive, zero injection). TWO gaps: (1) the WAN-ingress path (`receive_wan`) tried the harness-hosted TCP relay then fel…\n…\n- **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).\n…\n\n…\n### 7.26 Concurrent first-touch of ONE fresh BranchStore must ALL succeed — a non-atomic `git init` race must never strand a first-toucher  `[REQ-HAZARD-STORE-INIT-RACE]`\n…\n<!-- [doc->REQ-HAZARD-CONTROL-STAMP-LIFETIME] -->\n### 7.27 A control/viewer stamp must NEVER outlive its session — every teardown path clears what attach stamped  `[REQ-HAZARD-CONTROL-STAMP-LIFETIME]`\n- **Failure (F-026 #2, live evidence HFENDULEAM):** an spt-hosted endpoint stayed `ONLINE+CONTROLLED` after the operator's RC `/exit` — hours later hall-a's `info.json` still read `controlled:true` (status offline, dormant) and hfenduleam still GOSSIPED `controller_node=self` cross-node (SPT_DEV.json snapshot). ROOT: `/exit` kills the CHILD, not the controller connection, so the endpoint suspends via child termination and the teardown runs the REAP path (`broker.rs` exit-waiter → `sessions.remove(&id)`), NEVER the controller-detach path (`detach_if`→`clear_controller`→`stamp_driven_by`, the ONLY stamp-clear). The exit-waiter had no stamp clear, so the OutputLog dropped with the controller slot occupied and `controlled` / `viewer_count` / (`driven_by` for a r…\n- **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-effort like the sibling stampers (empty endpoint ⇒ nowhere to record). The residual ~1 min `ONLINE` tail after child death is the roster liveness-decay window (separate, lower priority) — but `CONTROLLED` no longer lies.\n- **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(2)` past reap). Killing the child (not dropping the conn) isolates the reap leg — the detach path never runs.\n- **Source:** F-026 W1 #2 (BUILD-F026-PICKERTRUTH; doyle triage + gate, todlando build 2026-07-03).\n\n---\n\n<!-- [doc->REQ-STORE-CONTEXT-BRANCH-FILL] -->\n### 7.28 A relative manifest path resolves against the ENDPOINT, never the daemon  `[REQ-STORE-CONTEXT-BRANCH-FILL]`\n- **Class rule:** a relative manifest path naming a filesystem location the ENDPOINT reads or writes MUST resolve against the endpoint's own cwd (its `info.json.cwd`, read at USE time), never the daemon's process cwd. A single daemon hosts many endpoints with different cwds, so a relative path frozen or read against the daemon cwd points at a directory no endpoint uses. Keys that resolve via `{cwd}` template substitution (`[digest].source`, `[history].locate_template` — `{cwd}` filled from `info.cwd`) or that the daemon fills with a computed ABSOLUTE path (`[message-idle-translation-binary].path` ← install-dir/PATH program resolution) are already endpoint-anchored and out of scope; the verbatim-`PathBuf::from` keys are the risk.\n- **Failure (F-026 SI-1, box-wide, evidence-complete):** the two-tier context BranchStore (`tracked/.seed.git`) held ZERO `a-*`/`p-*` branches on HFENDULEAM despite months of live-agent use — 0 objects, unborn HEAD, no `projects/` worktree checkouts, no commune drop files anywhere (psyche context flowed via the SEPARATE spt-agent-storage sync, masking it). ROOT: `BrainLifecycle::with_config_in` used `manifest.session.commune_dir` / `signoff_dir` VERBATIM (`PathBuf::from`). claude-spt declares `commune_dir = \".claude\"` (RELATIVE, documented \"resolved per-endpoint against its cwd\"), so `drop_dirs()` yielded a bare `.claude` that `ingest_drops` joined against the DAEMON's cwd (`drop_path.exists()==false` every pulse → `continue` → zero ingest → zero commit). Co…\n- **Invariant:** drop-dir resolution + project-id derivation happen AT INGEST TIME against the endpoint's fresh `info.cwd` (`pulse_tick`, not frozen at construction): absolute → as-is; relative + a recorded cwd → joined under it; relative + NO cwd → SKIPPED with a LOUD-once `DROP_INGEST_SKIP` diagnostic (never guessed, never the daemon cwd). Project-tier routing is owlery-gated — an owlery-internal anchor (psyche-host session) resolves its drops verbatim but routes NO `p-<project>` branch (empty project_id → `route_slices` skips `commit_project`; mirrors the picker's owlery exclusion); a non-owlery anchor derives `project_id` via the SAME `project_id_for_dir` the picker uses (category-key parity). Pure back-compat: absolute manifests unchanged, no wire chang…\n- **spt-core mapping:** impl `spt-daemon/src/lifecycle.rs` `resolve_endpoint_drop_dir` + `is_owlery_internal` + `warn_no_cwd_once` + `pulse_tick` (reads `info.cwd`, resolves each drop dir, derives owlery-gated `project_id`) + `spt-live/src/ingest.rs` `route_slices` (empty-project_id skips the project tier); unit `resolve_endpoint_drop_dir_cases` + `is_owlery_internal_cases` (lifecycle.rs) + `empty_project_id_skips_project_tier` (ingest.rs); int (real pulse + real store) `relative_commune_dir_resolves_against_endpoint_cwd_and_fills_project_branch` — a RELATIVE `.claude` + a real endpoint cwd → the drop is found and `a-<id>` + `p-<project_id_for_dir(cwd)>` commit (RED-first: verbatim-relative resolution → 0 ingested, store pristine).\n- **Source:** F-026 SI-1 (BUILD-F026-PICKERTRUTH; doyle RCA-gate + design ruling, todlando build 2026-07-03). Hazard-class sweep confirmed commune_dir+signoff_dir the ONLY verbatim instances.\n\n---\n\n<!-- [doc->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE] -->\n### 7.29 Control/viewer stamps CONVERGE to broker session-table truth, not merely edge-trigger  `[REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]`\n- **Failure (F-026 stamp-gap, hall-b + the original ball-b):** a picker-created endpoint (`endpoint run` → new) read plain `ONLINE` in the list + picker while genuinely driven — `info.json` `controlled:false` throughout. ROOT (the UPWARD companion to 7.27's downward edge-clear): the broker spawn path's `become_controller` → `stamp_driven_by` → `set_controlled(true)` fires at SPAWN time, but a FRESH endpoint has NO PERCH yet (the adapter binds it after claude boots), so `mutate_info` returns `NotFound` and every stamper swallows it (`let _`). The adapter's bind then writes `InfoJson::new` with `controlled:false` DEFAULT, and since the operator never re-attaches, no later EDGE ever re-stamps → the endpoint reads uncontrolled FOREVER while driven. The 7.15/7.27…\n- **Invariant:** the broker (SINGLE writer) re-asserts each live session's control/viewer stamps to session-table TRUTH, DIVERGENCE-GATED — read the perch's recorded `(driven_by, controlled, viewer_count)`, compare to the live truth (`controller_by()` / `has_controller()` / live `viewer_count`), and write ONLY on a real difference (no per-poll fsync storm). The re-assert rides the `KIND_SESSIONS` handler (the daemon reconcile + picker poll it on cadence), so a fresh perch CONVERGES within one reconcile-poll window after bind — a BOUNDED window, no new timer. Truth is SNAPSHOTTED under each log lock; the convergence writes run OFF the lock (the lock-across-effect discipline, 7.12/5.16). Event-on-input is INSUFFICIENT (an idle controlled session — the operator…\n- **Coverage boundary (who MAY clear vs who MUST NOT — cross-ref 7.15):** the convergence's `driven_by`-clear leg fires ONLY when `has_controller()==false` — the DEFINITIVE broker signal (it reads its OWN controller slot, not the ambiguous `controller_by`). That is why the broker MAY clear a stale `driven_by` where the BRAIN reconcile (7.15) MUST NOT: the brain only sees `controller_by==None`, which a live LOCAL controller also reads, so a brain clear would false-heal a genuinely-driven session. This leg is belt-and-suspenders over the clean-disconnect `detach_if`→`clear_controller` (which already zeroes `driven_by` on a clean drop). The convergence does NOT close the HALF-OPEN wedged-remote residual — an abandoned REMOTE controller whose conn stays OPEN (no…\n- **spt-core mapping:** impl `broker.rs` `stamp_divergence` (pure gate) + `converge_perch_stamps` (off-lock, single-writer, skips a perch-less/corrupt endpoint) + `OutputLog::has_controller`/`live_viewer_count` (snapshot accessors) + the `KIND_SESSIONS` handler (snapshot-under-lock → converge-off-lock); unit `broker.rs` `stamp_divergence_gates_writes` (no-diff → no write; `controlled false→true` = the stamp-before-bind case; driven_by / viewer / both); int (real broker+PTY) `control_stamp_lifetime.rs` `converge_stamps_on_sessions_poll_after_late_bind` (spawn with NO perch → spawn stamp swallowed → LATE bind writes `controlled:false` → a `KIND_SESSIONS` poll converges `controlled=true`; RED-first: hook disabled leaves `controlled:false` forever).\n- **Source:** F-026 W1-addendum (BUILD-F026-PICKERTRUTH; doyle stamp-gap dispatch + convergence ruling + KIND_SESSIONS-hook approval, todlando build 2026-07-03).\n\n### 7.30 A Psyche failure of ANY shape must NEVER remove or alter the parent endpoint's ready/hosted state  `[REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION]`\n<!-- [doc->REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION] -->\n- **Failure (paid-for, field brick 2026-07-04, adapter v0.13.2):** the pre-F-030 model kept a **resident** Psyche process the daemon supervised, with residency machinery (`confirm_residency_or_unhost`) that **un-hosted the parent endpoint** when the resident child went missing. A bad adapter ship (v0.13.2) made the psyche shim exit on every turn; the residency machinery read that as a lost resident and **tore down the parent's hosted state — the ready marker was removed and never re-stamped**, so every `--force-native` delivery then gated `cli-gate-not-hosted` PERMANENTLY. A Psyche problem silently bricked the parent LiveAgent. (Companion churn class: 7.31.)\n- **Invariant:** a Psyche is a **bounded per-event turn**, not a resident process — its liveness is that turns succeed, so there is no \"resident is gone\" signal to react to. A psyche turn failure of ANY shape (spawn fail, non-zero exit, timeout, wedge) stamps **psyche fields only** (`psyche_host_error` on the Self perch) and **NEVER** touches the parent endpoint's `status` / ready marker / hosted state; the endpoint stays online and deliverable while its Psyche is unwell. The teardown-on-psyche-trouble path is **deleted** with the residency machinery. `REQ-HAZARD-LIVEHOST-NONRESIDENT`'s spirit (a psyche that cannot work must be visible on the perch, hosting must not churn) transfers to the per-event failure budget (7.31); its traceable entry carries a SUPERS…\n- **spt-core mapping:** the residency/reap DELETIONS in `spt_daemon::livehost` (`reconcile_once`'s stop-side no longer un-hosts on psyche trouble; `host_one` holds no resident child) + `spt_daemon::lifecycle::run_psyche_event_turn` (a turn failure stamps `psyche_host_error` only) + the `first_turn_psyche_context` non-empty guarantee (a zero-context fresh turn can't masquerade as continue → reseed churn). Conformance int = the hall-bf shape: multi-subnet home, live endpoint, a psyche turn that fails every fire → parent ready marker PRESENT + deliverable stays TRUE, endpoint NOT un-hosted, no rehost churn, `psyche_host_error` stamped (the wave's heart).\n- **Source:** F-030 (psyche-ephemeral) — paid-for by the hall-bf churn (ordinal 6491+) and the adapter v0.13.2 bad-ship brick, 2026-07-04.\n\n### 7.31 The Psyche failure budget must count REAL per-event attempts — a resident rate-guard is blind to per-event churn  `[REQ-HAZARD-THRASH-GUARD-BLIND]`\n…\n\n…\n### 7.33 NO bare `.lock().unwrap()` on a broker-resident lock reachable from serve/dispatch — a poison permanently wedges every attach  `[REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]`\n<!-- [doc->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON] -->\n- **Failure (paid-for class):** a brain-only self-update restarts the BRAIN but keeps the BROKER process — and every one of its `Mutex`es — ALIVE by design (REQ-UPD-3). So a single panic while another thread held a broker-resident lock POISONS it permanently: the next `.lock().unwrap()` panics too, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines (`spt rc` → \"brain IPC read deadline elapsed\") while non-locked ops keep working. The effect journal (bug #16, `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`) and the inject floor (`[REQ-HAZARD-INJECT-WORKER-POISON]`) were fixed one lock at a time; the SURVIVING class was the attach-path locks — the session map (`sessions` ×18 + its `sessions_exit` alias), the per-session `OutputLog` ring (×1…\n- **Invariant:** NO broker-resident lock reachable from serve/dispatch may use a bare `.lock().unwrap()`. Recover via the shared `recover` (`into_inner` — safe for the short, coherent-on-recovery map ops of `sessions`/`pair_holds`) or, for the `OutputLog` ring, `recover_log` — which adds a COHERENCE CLAMP on the recovery path: a panic mid-`append` can leave the ring torn (over-cap, a last seq not below `next_seq`, non-monotonic), and serving those bytes risks garbage, so `clamp_or_reset` cheap-checks the invariants and RESETS the ring empty (`next_seq` preserved — cursors never rewind) + loud-logs on violation. Fail-fast on the log would REINTRODUCE the wedge; blind recover would serve torn bytes; clamp-or-reset costs only scrollback that self-heals on the n…\n- **spt-core mapping:** `broker::recover` / `broker::recover_log` / `OutputLog::clamp_or_reset`. unit = a poisoned session-map recovers to a usable guard (the next attach still opens), a torn ring clamps/resets-empty only when torn (coherent untouched, `next_seq` preserved), and `recover_log` wires the clamp onto the poison branch — each RED-first against a bare `.lock().unwrap()` / an un-clamped `into_inner`. int (scripted panic-under-lock during concurrent attach → next attach opens) deferred to the two-host rig.\n- **Source:** REMOTE-TRUTH B-1 (pivoted — the triage's named floor sites were already closed by `[REQ-HAZARD-INJECT-WORKER-POISON]`; the surviving attach-path lock class is the real root), doyle B-1 ruling (recover all three via one helper + the OutputLog ring coherence clamp).\n\n### 7.34 A dead `rec.pid` on an spt-hosted perch is EXPECTED — no reader may alive-gate on `rec.pid` alone  `[REQ-HAZARD-DEAD-REC-PID]`\n<!-- [doc->REQ-HAZARD-DEAD-REC-PID] -->\n- **Failure (paid-for, field evidence — the F-026 #11 dead-pid class):** an spt-hosted endpoint's `rec.pid` records the ephemeral bind-CLI pid, which dies IMMEDIATELY after bind (the broker holds the PTY; there is no resident harness process at that pid). Readers that alive-gated on `rec.pid` alone treated the perch as stale: self-detect leg (c) could NEVER resolve an spt-hosted sender (its from-stamp degraded to `cli@NODE`, operator #7, and replies bounced `NO_PERCH` — sighted on hall-bf/ball-b, v0.24.0).\n- **Invariant:** a dead `rec.pid` on an spt-hosted perch is an EXPECTED state, not staleness. NO reader may alive-gate on `rec.pid` alone: spt-hosted LIVENESS comes from the daemon-managed `status` field (KH 2.5 — status present ⇒ authoritative, never a per-pid probe); IDENTITY comes from session/ancestry resolution, where `rec.parent_pid` (the harness pid, the stable session-binding anchor) is the ancestry candidate (ADR-0021 seed-hint discipline: pid is a bind-time SEED, not a truth anchor — re-anchoring truth in `rec.pid` is overruled by design). Any newly sighted `rec.pid`-alive-gating reader gets the same scoped fix and extends THIS requirement's evidence — no new REQ per reader.\n- **spt-core mapping:** `roster.rs` `detect_self_by_ancestry` (the first sighted reader, fixed by `[REQ-SELF-DETECT-PARENT-PID]`: `parent_pid` candidates alongside `rec.pid`). int = the E-1 red-first (`detect_self_resolves_spt_hosted_perch_via_parent_pid`: dead `rec.pid` + live-ancestor `parent_pid` resolves), dual-tagged as this class's test.\n- **Source:** REMOTE-TRUTH E-1 rider (doyle ruling 2026-07-05: scoped reader-side (b) over re-stamping `rec.pid` (a) — ADR-0021 ground truth, migration hole, blast radius).\n\n### 7.35 The cached ceremony-clock NTP offset must NOT survive an OS clock STEP — an offset measured against the pre-step clock strands every pairing for the TTL  `[REQ-HAZARD-CEREMONY-CLOCK-STEP]`\n- **Failure (paid-for, field evidence — the enlyzeam BIGNET-join RCA 2026-07-06):** enlyzeam's `w32time` was STOPPED, so its system clock drifted +3m30s; the pairing ceremony offset (REQ-PAIR-8, `ntp.rs`) correctly corrected the TOTP clock — until an operator `w32tm /resync` STEPPED the OS clock back −210s under the LIVE daemon. The cached offset had been measured against the OLD (drifted) clock and its snapshot was monotonic-only (`when: Option<Instant>`), so the step was invisible to the cache: for up to the 15-min TTL the ceremony clock stayed ~7 TOTP steps in the past, every `subnet join` returned NO_SEED_HOLDER while fresh-process probes met in <500ms, and only a daemon bounce (which forced a fresh query) healed it. Timeline-proven: refresh cadence 15:0…\n- **Invariant:** the offset cache MUST detect an OS clock step and re-query, never apply a stale offset across it. The snapshot stores an `(Instant, SystemTime)` PAIR; on every read, if the wall clock diverged from the monotonic clock since the snapshot beyond a small tolerance (`|wall_elapsed − mono_elapsed| > ~2s`) the OS clock STEPPED ⇒ force an immediate re-query (offset recomputed against the new clock) regardless of TTL. Belt-and-braces at the join edge: `meet_seed_holder`, on search-deadline exhaustion, forces ONE fresh NTP query and runs one final sweep at the corrected step before the NO_SEED_HOLDER verdict — so a stepped-clock join self-heals WITHOUT a daemon bounce. The step tolerance sits far below one TOTP step (30 s) and well above scheduling j…\n- **spt-core mapping:** `spt_net::net::pairing::ntp` — `CachedOffset{when, wall_at}` pair + `refresh_needed`/`clock_stepped`/`signed_secs` (step-detect) + `force_refresh`/`invalidate` (the meet-exhaustion re-query seam); `spt_daemon::pairhost::meet_seed_holder` via `sweep_then_final_retry` (force-refresh + one final sweep on exhaustion). unit = `clock_step_forces_refresh_via_injected_reads` (injected mono/wall reads: a wall step forward OR back with mono barely moved re-queries; agreeing clocks within TTL do not; TTL-expiry and never-queried also refresh — no 15-min sleep) + `invalidate_clears_the_snapshot` + `meet_retries_once_after_refresh_on_exhaustion`/`meet_final_retry_failing_surrenders_once` (exactly one post-refresh retry, then surrender with the ric…\n- **Source:** JOIN-TRUTH W1/D3 (doyle /diagnose enlyzeam RCA; the offset defeated by a live clock step is the D3 of four defects, D1 multihome + D2 loud-fail + D4 verbose-clock the siblings).\n\n### 7.36 The broker control plane and PTY fan-out must NEVER block on a single subscriber connection — a suspended brain conn must not wedge control  `[REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]`\n<!-- [doc->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE] -->\n- **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 controller BLOCKED (not exited) on an IDLE session was evicted by NEITHER. Within seconds: attached output froze, `detach` did not release the control stamp (release routed through the wedged conn), reattach was REFUSED (`controlled-by` latched), `rc --take` hung, and `d…\n- **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 stall-evicts it. Eviction is triggered TIME-based, not output-driven: at the take/reattach path (`resolve_subscribe`, so control self-heals against broker truth even on an idle session) AND at the `KIND_SESSIONS` reap. (2) The stamp release is BROKER-side — `clear_c…\n- **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` (`KIND_STALL_EVICTS` IPC); `brain.rs` `Brain::stall_evicts`; `cli.rs` `render_stall_evict_line` (daemon-status surface, public wording). unit = `controller_write_stall_predicate_distinguishes_wedged_from_idle` + `stalled_incumbent_is_evicted_on_reattach_and_cont…\n- **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\n### 7.37 The Layer-1 settle-gate must RE-ARM per delivery on an observable PTY — a mid-session reader reattach (`/clear`) re-creates the head-swallow race  `[REQ-HAZARD-INJECT-SETTLE-REARM]`\n<!-- [doc->REQ-HAZARD-INJECT-SETTLE-REARM] -->\n- **Failure (paid-for, field-CONFIRMED on 0.29.0 — doyle diagnosis + perri screenshot `WindowsTerminal_6iSjya8pMt.png`):** the shipped W5-A settle-gate (7-of-W5, `REQ-INJECT-MULTILINE-INTEGRITY`) gated Layer 1 behind a worker-local ONE-SHOT (`settled_once`) on the premise that the head-swallow race is a STARTUP condition (the input reader not yet attached after spawn). That premise is FALSE: a mid-session `/clear` re-enters the harness's raw-mode input reader, re-attaching it and RE-CREATING the pre-settle window — but the one-shot had already fired at spawn, so `settle_before_inject` was SKIPPED and the head was eaten again. A checkpoint-wake payload injected right after `/clear` arrived head-truncated (mid-path `spt/Cargo.toml)`); echo-verify (Layer 2) is …\n- **Invariant:** the settle-gate re-arms before EVERY delivery on an OBSERVABLE (echoing/interactive) PTY — the bug-prone class, where re-settling is cheap (a tick or two once the reader answers) and necessary (a mid-session reader reattach must be re-confirmed). The steady-state settle is latch-skipped ONLY where the probe is known UNOBSERVABLE on this PTY (a non-echoing ConPTY whose DSR answer never surfaces): there is no reader-reattach race to guard, and each settle would burn the full `INJECT_SETTLE` deadline. The class is discriminated by the settle's OWN return value — `settle_before_inject` returns `true` iff the session ring advanced (probe observed) — latched on the FIRST attempt only; a RE-DRIVE (`attempt > 1`, reached only on a swallowed head) AL…\n- **spt-core mapping:** `broker.rs` `run_inject_worker` — the `settled_once: bool` one-shot is replaced by a `probe_unobservable: bool` latch (init `false` = assume observable → settle first delivery); the gate is the pure `should_settle(attempt, probe_unobservable) = attempt > 1 || !probe_unobservable`, and the first-attempt settle's `observed` return sets `probe_unobservable = !observed`. `settle_before_inject` gains a `bool` return (observed vs timed-out). unit = `should_settle_rearms_on_observable_pty` (observable PTY re-settles each delivery; unobservable-probe PTY skips the steady-state settle; a re-drive settles in both classes — RED-first: restore the one-shot and the observable-PTY assert flips). No int edit: the `inject_control_wedge.rs` mocks (`fi…\n- **Source:** post-0.29.0 field fix (doyle diagnosis + design ruling `BUILD-SPEC-inject-settle-rearm`; todlando build 2026-07-08). An impl bug in the shipped W5-A Layer-1 fix, not a new invariant class — the settle-gate was correct but under-armed.\n\n### 7.38 EVERY write on a physical broker connection is bounded + cancelable + poison-on-failure — no writer holds the send gate across an UNBOUNDED OS write  `[REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]`\n<!-- [doc->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK] -->\n- **Failure (paid-for, field capture + deterministic Windows repro 2026-07-09 — hertz RCA, UPDATE-WEDGE round-4):** `controller_writer` held the `SharedSend = Arc<Mutex<SendHalf>>` guard ACROSS a blocking `write_frame`. On Windows, `interprocess 2.4.2` routes the send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — this local-socket send path exposes **no supported write timeout** (`set_timeout` → `Unsupported`). An `rc --take` controller consumer that stopped reading blocked the write **indefinitely** (~127.95 s in the capture, released only when a brain restart tore the connections down). The 7.36 logical `stall_evict_controller` removed the controller ROLE but neither canceled the in-flight pipe write, closed the physical connection, nor called `Cance…\n- **Invariant:** every write on a physical broker connection rides ONE broker-owned bounded/cancelable framed-write primitive with an **independent out-of-band aborter**. The `SendHalf` never leaves the conn object (a bypass cannot compile). An ABSOLUTE deadline stamped at write entry covers BOTH the serialized gate-wait AND OS write completion; a per-conn watchdog fires at that deadline and aborts the in-flight op — never relying on the write returning or on opportunistic stall-evict sampling. On deadline / partial write / cancellation / unknown completion: (1) poison the whole physical conn idempotently, (2) abort read+write so `handle_conn` reaches existing EOF cleanup, (3) wait for the canceled op to report completion before releasing its buffer, (4) NEV…\n- **spt-core mapping:** `spt-daemon/src/conn.rs` `BrokerConn` (Condvar-gated serialized write gate + poisoned state + per-in-flight op id + watchdog + `abort_physical()`: cfg(windows) `CancelIoEx` → completion handshake → `DisconnectNamedPipe` raw kernel32 externs; cfg(unix) `UnixStream::shutdown(Both)`); `broker.rs` `SharedSend = Arc<BrokerConn>` + `controller_writer`/`viewer_writer`/`send_frame`/`send_error` routed; `nethost.rs` stream-log/presence sends routed. int = `brain_decouple.rs` `non_draining_controller_stall_evict_releases_writer_and_connection` (`#[cfg(windows)]`, the hertz 9-step: real PTY burst, black-holed remote controller, exactly one logical stall-evict via an independent draining conn, the black-holed conn's `handle_conn` finishes ≤2 s of…\n- **Source:** UPDATE-WEDGE round-4 (hertz root doc `docs/UPDATE-WEDGE-ROOT-CAUSE.md` @f8596ca; doyle gate ruling `docs/UPDATE-WEDGE-RCA-DISPATCH-todlando.md`; todlando build 2026-07-09). The residual 7.36 left open: logical eviction without physical retirement.\n\n---\n\n### 7.39 Per-session identity env (`SPT_ENDPOINT_ID`/`OWL_SESSION_ID`/`SPT_AGENT_ID`) is NEVER inherited — the daemon scrubs it at startup AND on every role spawn, regardless of any role's declared `env_remove`  `[REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]`\n<!-- [doc->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE] -->\n- **Failure (paid-for, perri field RCA 2026-07-09/10 — F-036 psyche seat-theft):** a daemon restarted from inside an agent session (routine during core dev / `spt update apply`) carried the session's `SPT_ENDPOINT_ID=doyle` and passed it verbatim into every `[session.psyche_resume]` spawn — core stripped only each role's DECLARED `env_remove` list (runtime.rs `command_for`), so ONE adapter `env_remove` miss infected the whole node. Every psyche claude turn fired SessionStart, the adapter hook saw the leaked endpoint id, took the bind path, and ROTATED the victim's perch onto the psyche's own custody sid with a valid prior-sid proof — every pulse. Field blast radius: lia/deployah/doyle psyches ALL briefed as `<sptc-active-perch id=doyle>`; 37 peer msgs draine…\n- **Invariant:** the identity set (`spt_runtime::IDENTITY_ENV_VARS`) is per-session state and NEVER correct inherited state for a daemon or any child it spawns. The daemon (broker AND brain entries) scrubs its own process env first thing (`scrub_identity_env`); the shared role-spawn command builder scrubs the set unconditionally and LAST (after `env_remove`, read-env stamps, and the recursion guard), so no adapter declaration — missing or pathological — can leak identity into a role child. A spawn that NEEDS an identity var (a hosted harness) receives it by explicit per-spawn injection (manifest `[env] direction = \"inject\"`), never by inheritance. The same startup scrub also clears the inject-echo knobs (`spt_runtime::INJECT_ECHO_ENV_VARS`: `SPT_INJECT_VERIF…\n- **spt-core mapping:** `spt-runtime/src/runtime.rs` `IDENTITY_ENV_VARS` + `scrub_identity_env` + the `command_for` unconditional scrub; `spt-daemon/src/daemon.rs` `Daemon::run` / `Daemon::run_brain` startup scrub; `spt/src/api/startup.rs` `establish_perch` custody-squat refusal over `spt-store/src/psyche_custody.rs` `custody_squatter`.\n- **Source:** MSG-IDENTITY W1 (perri F-036 RCA; doyle dispatch `docs/MSG-IDENTITY-DISPATCH.md`; adapter half shipped claude-spt v0.18.8 — this is the core-layer defense so no adapter miss can ever leak identity again).\n\n---\n\n### 7.40 A commune/signoff drop is deleted ONLY after every applicable tier is durably committed — an un-committable slice preserves the drop, never delete-then-lose  `[REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]`\n<!-- [doc->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE] -->\n- **Failure (paid-for, perri field finding 2026-07-08 — F-032, data-loss):** `ingest_drops` unconditionally deleted the drop after `route_slices`, but the project tier is GATED on a non-empty `project_id` — when the endpoint's anchor cwd was unresolved/owlery-internal at ingest time, the `<project-context>` slice was parsed but never committed, yet the source drop was still deleted → the project-context content permanently lost. perri's two-sliced echo-commune INGESTED (file deleted) yet never surfaced at her next SessionStart resume-pull; adapter exonerated. Legacy spt held commit-first-then-delete parity; the modern two-slice ingest broke it.\n- **Invariant:** delete-after-commit, per slice. Distinguish a write **SUPPRESSED by precedence** (incoming older than durable → already superseded → safe to delete) from a slice **un-committable now** (empty `project_id`, or any write error → the `?` retry path). An un-committable non-empty project slice is preserved as a **project-only pending slice under the COMMUNE suffix, whatever the source drop's kind** — a signoff-suffix pending would be reaped by `sweep_stale_signoff` on the next listener start (the signoff sentinel is write-once-per-generation), recreating the black-hole on the signoff path; the commune-named pending is a pure project-slice carrier both suffixes ingest identically and the sweep never sees. The already-committed live slice is droppe…\n- **spt-core mapping:** `spt-live/src/ingest.rs` `ingest_drops` (deferred-project preserve + `Ingested.preserved`); `route_slices` project gate unchanged (`REQ-STORE-CONTEXT-BRANCH-FILL`); kin `REQ-HAZARD-DROP-FILE-SINGLE-WRITER` (core stays the single writer — the preserve rewrite is core's own write).\n…\n\n…\n","startLine":408,"lineNumbers":[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,null,477,478,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,518,null,522,null,547,null,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,null,596,null,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,null,662,null,664]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\KNOWN-HAZARDS.md"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784195789175}}
{"type":"message","id":"9c722c9d","parentId":"76e742b5","timestamp":"2026-07-16T09:56:29.179Z","message":{"role":"toolResult","toolCallId":"call_0v4wgXCf3bUcx09HbxN656Dy|fc_008390f508d1d09b016a58aacd12d8819ab08b1b7689188663","toolName":"read","content":[{"type":"text","text":"//! UPDATE-WEDGE round 2 (REQ-BRAIN-RESUME-NO-CONTROL-STEAL) — the multi-broker-PTY-\n//! session RESPAWN rig (doyle-ruled 2026-07-09). This is the coverage the counter-54\n//! single-black-holed-session A'-rig (`trial_drain_drive_e2e.rs`) LACKED: N REAL broker\n//! PTY sessions, each with its OWN local controller producing output, PRESENT at a real\n//! brain respawn — the exact field condition (7 spt-hosted PTYs at a `fetch --apply`) the\n//! resume-steal bug needed and the single-session rig never exercised.\n//!\n//! ## The field incident this locks down (root, log-anchored — docs/UPDATE-WEDGE-2-RCA.md)\n//!\n//! `spt update fetch --apply` (v0.30.2 → v0.30.3) PROMOTED CLEANLY (the counter-54 fix\n//! worked) but under 7 live spt-hosted broker PTYs it FROZE all 7 + failed every `spt rc`\n//! retake. ROOT (SME docs/UPDATE-WEDGE-2-SME-todlando.md): `run_brain` calls\n//! `resume_sessions()` at boot, which re-attached EVERY session `KIND_SESSIONS` returns as\n//! `subscribe_with(AttachIntent::Control, by:None)`. The docstring's \"a None identity never\n//! displaces (falls back to viewer)\" is a MYTH for FREE / SAME-LOCAL-IDENTITY slots:\n//! `resolve_subscribe` (broker.rs) stall-evicts first, then `become_controller` when the\n//! slot is free OR the incumbent is also local (`None == None`) — viewer-fallback fires\n//! ONLY against a DIFFERENT *remote* controller. So the respawned brain STOLE the by:None\n//! controller of every free/local-controlled session (incl. the operator's local `spt rc`\n//! and each spt-hosted PTY's spawn-time controller), which the daemon brain never drains\n//! (it hosts no PTY sessions — brainproc.rs) → the session FREEZES the instant the brain takes\n//! over + head-of-line-blocked the shared brain↔broker conn on the N-session replay → every\n//! journaled `rc` retake deadlined (Failure B).\n//!\n//! All N controllers on the field box were LOCAL (by:None — the operator's local `spt rc` +\n//! each PTY's spawn-time controller). The respawn's by:None Control re-attach →\n//! `become_controller` SILENTLY re-takes (same-local-identity `None==None`, the gate-#7 path\n//! that fires NO `Displaced` notice) → the incumbent local controller is ORPHANED: the broker\n//! stops writing its output but does NOT close the conn → no EOF, no `PumpEnd`, no 30s\n//! reconnect/detach splash (that fires only on `BrokerGone`/`Severed`). So the session FREEZES\n//! (output stops entirely, not just control) the instant the brain takes over. The stall-evict\n//! (>15s) is a SEPARATE, LATER, output-dependent symptom: sessions actively producing output\n//! make the stolen writer block >15s → evict (the field's 5); idle sessions never block →\n//! never evict (the field's 2) — but ALL are silently stolen+frozen. The 5-vs-2 split is\n//! ACTIVE-vs-IDLE, not remote-vs-local. This rig asserts the IMMEDIATE, deterministic signal —\n//! a held controller's output-tick freezes on the steal (RED) vs keeps ticking under the Viewer\n//! fix (GREEN) — NOT the flaky, timing-dependent stall-evict.\n//!\n//! ## The FIX and what this rig discriminates\n//!\n//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` no longer\n//! calls `resume_sessions` at all — it uses the cursor-only `resume_session_cursors` (NO\n//! subscribe), which supersedes the Viewer re-attach as the no-steal cure (no attach ⇒ no\n//! steal, a fortiori) AND fixes the SharedSend self-deadlock. This rig's no-steal\n//! discriminator (the operator's `A_i` controllers keep ticking across the swap) therefore\n//! passes for the stronger reason — the daemon brain never attaches. The Viewer text below\n//! describes the round-2 shape this superseded; the assertions are unchanged and still hold.\n//!\n//! `resume_sessions` (still used by DRAINING callers) re-attaches as **`AttachIntent::Viewer`**\n//! (a viewer never owns `driven_by` and is never stall-evicted). The RED→GREEN discriminator\n//! this rig asserts is\n//! the STEAL itself — the deterministic ROOT that CAUSES both A and B — observed as OUTPUT\n//! ROUTING:\n//!\n//! - Each session is spawned by a controller conn (`A_i`) the test HOLDS; `dispatch_spawn`\n//!   pre-attaches `A_i` as the session's LOCAL (by:None) controller. A reader thread on\n//!   `A_i` counts `KIND_OUTPUT` frames from a child that ticks forever (~150ms).\n//! - The real `spt daemon brain` candidate boots and `resume_sessions()` re-attaches all N.\n//! - **RED (Control re-attach):** `resolve_subscribe` `same_identity(None==None)` →\n//!   `become_controller` DROPS `A_i`'s sink → `A_i` is displaced → its tick counter FREEZES.\n//! - **GREEN (Viewer re-attach):** the brain joins as a viewer → `A_i` stays controller →\n//!   its tick counter KEEPS climbing across the swap.\n//!\n//! So the load-bearing assertion is DETERMINISTIC (output-routing displacement), not the\n//! flaky 15s wedge-maturity timing: RED starves every `A_i` the instant resume runs; GREEN\n//! never does. We also assert the trial still PROMOTES (no regression of\n//! REQ-UPDATE-TRIAL-DRAIN-DRIVE) — with no controller wedged, `old_gen_drained()` is true\n//! and the latched-ready candidate promotes.\n//!\n//! ## Scope note — Failure B (rc-retake deadline) is NOT asserted here (todlando, reported\n//! to doyle 2026-07-09). Failure B is the daemon-brain-conn SATURATION symptom; in an\n//! in-proc broker a local `rc` (by:None) `same_identity`-TAKES at the broker even in the RED\n//! state, so it does not discriminate here, and forcing real subprocess-brain-conn\n//! saturation is brittle (the same nondeterminism the counter-54 rig engineered around). We\n//! assert the ROOT (the steal) that CAUSES B; a full-daemon B repro is left to PR CI /\n//! field verification per doyle's ruling (heavy E2Es off the local gate).\n//!\n//! ## RUNNER CONTRACT — single `#[test]`, process-per-test (nextest) or -j1\n//!\n//! The SOLE `#[test]` here by design: it owns the process-global `SPT_HOME` (the subprocess\n…\n    let sup = {\n…\n        thread::spawn(move || {\n…\n        })\n    };\n\n    // ── The candidate boots, RUNS resume_sessions against the N present sessions, then\n    //    stamps ready. A present ready ⇒ resume (and the RED steal, if any) has completed. ──\n    let _cand_pid = wait_ready_pid(&ready_path, Duration::from_secs(45))\n        .unwrap_or_else(|| teardown_panic(&stop, \"the trial candidate never stamped brain.ready\"));\n\n    // ── DISCRIMINATOR (load-bearing, RED-first). Snapshot each A_i tick tally AT the\n    //    post-resume instant, then measure a window. GREEN (Viewer re-attach): A_i kept\n    //    control → its tally CLIMBS. RED (Control re-attach): resume become_controller'd the\n    //    slot → A_i's sink was dropped → its tally FREEZES. The child keeps ticking either\n    //    way (broker-hosted, independent of the brain), so a frozen tally is a displaced\n    //    controller, not a dead child. ──\n    let t0: Vec<u64> = sessions.iter().map(|(_, t)| t.load(Ordering::Relaxed)).collect();\n    // ~2s ≫ the 150ms tick, so GREEN accrues ~10+ ticks; comfortably clears CI jitter.\n    thread::sleep(Duration::from_secs(2));\n    let t1: Vec<u64> = sessions.iter().map(|(_, t)| t.load(Ordering::Relaxed)).collect();\n\n    // ── Wait out the trial verdict (assertion 3). With no wedged controller, GREEN drains\n    //    true and promotes within a heartbeat of ready; a RED that also wedged its stolen\n    //    controller may instead roll back — either way the discriminator above already fired.\n    let deadline = Instant::now() + Duration::from_secs(30);\n    let promoted = loop {\n        if !env.promotions.lock().unwrap().is_empty() {\n            break true;\n        }\n        if !env.rollbacks.lock().unwrap().is_empty() || Instant::now() >= deadline {\n            break false;\n        }\n        thread::sleep(Duration::from_millis(50));\n    };\n\n    // ── Teardown BEFORE asserting (so a failing assert still reaps the subprocess brain +\n    //    the N ticker children). ──\n    stop.store(true, Ordering::Relaxed);\n    let child_pids: Vec<Option<u32>> = sessions.iter().map(|(sid, _)| broker.session_pid(*sid)).collect();\n    for pid in child_pids.into_iter().flatten() {\n        kill_pid(pid);\n    }\n    let _ = sup.join();\n\n    let promotions = env.promotions.lock().unwrap().clone();\n    let rollbacks = env.rollbacks.lock().unwrap().clone();\n    let gained: Vec<u64> = t0.iter().zip(&t1).map(|(a, b)| b.saturating_sub(*a)).collect();\n    eprintln!(\n        \"=== UPDATE-WEDGE-2 RESUME-STEAL GATE: n={N_SESSIONS} ticks_before={t0:?} \\\n         ticks_after={t1:?} gained_in_window={gained:?} promoted={promoted} \\\n         promotions={promotions:?} rollbacks={rollbacks:?} \\\n         (GREEN = every gained>0 + promoted; RED = gained==0 on the stolen sessions) ===\"\n    );\n\n    // Assertion 1 (load-bearing, RED-first): EVERY session kept its controller across the\n    // swap — each A_i went on receiving output after the respawn. RED (resume re-attaches\n    // AttachIntent::Control): resolve_subscribe same_identity(None==None) become_controller'd\n    // each slot, dropping A_i → its tally froze → gained==0. GREEN (Viewer): A_i untouched.\n    // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    for (i, g) in gained.iter().enumerate() {\n        assert!(\n            *g > 0,\n            \"session {i} (sid {}) LOST its controller across the brain respawn: its \\\n             controller conn received {g} new output frames in the post-resume window (froze \\\n             at {}). The respawn's resume_sessions STOLE the controller slot (Control \\\n             re-attach → become_controller displaced the local controller) then never drained \\\n             it — Failure A. GREEN (resume re-attaches Viewer) leaves every controller intact. \\\n             ticks_before={t0:?} ticks_after={t1:?}\",\n            sessions[i].0,\n            t1[i],\n        );\n    }\n"}],"details":{"displayContent":{"text":"//! UPDATE-WEDGE round 2 (REQ-BRAIN-RESUME-NO-CONTROL-STEAL) — the multi-broker-PTY-\n//! session RESPAWN rig (doyle-ruled 2026-07-09). This is the coverage the counter-54\n//! single-black-holed-session A'-rig (`trial_drain_drive_e2e.rs`) LACKED: N REAL broker\n//! PTY sessions, each with its OWN local controller producing output, PRESENT at a real\n//! brain respawn — the exact field condition (7 spt-hosted PTYs at a `fetch --apply`) the\n//! resume-steal bug needed and the single-session rig never exercised.\n//!\n//! ## The field incident this locks down (root, log-anchored — docs/UPDATE-WEDGE-2-RCA.md)\n//!\n//! `spt update fetch --apply` (v0.30.2 → v0.30.3) PROMOTED CLEANLY (the counter-54 fix\n//! worked) but under 7 live spt-hosted broker PTYs it FROZE all 7 + failed every `spt rc`\n//! retake. ROOT (SME docs/UPDATE-WEDGE-2-SME-todlando.md): `run_brain` calls\n//! `resume_sessions()` at boot, which re-attached EVERY session `KIND_SESSIONS` returns as\n//! `subscribe_with(AttachIntent::Control, by:None)`. The docstring's \"a None identity never\n//! displaces (falls back to viewer)\" is a MYTH for FREE / SAME-LOCAL-IDENTITY slots:\n//! `resolve_subscribe` (broker.rs) stall-evicts first, then `become_controller` when the\n//! slot is free OR the incumbent is also local (`None == None`) — viewer-fallback fires\n//! ONLY against a DIFFERENT *remote* controller. So the respawned brain STOLE the by:None\n//! controller of every free/local-controlled session (incl. the operator's local `spt rc`\n//! and each spt-hosted PTY's spawn-time controller), which the daemon brain never drains\n//! (it hosts no PTY sessions — brainproc.rs) → the session FREEZES the instant the brain takes\n//! over + head-of-line-blocked the shared brain↔broker conn on the N-session replay → every\n//! journaled `rc` retake deadlined (Failure B).\n//!\n//! All N controllers on the field box were LOCAL (by:None — the operator's local `spt rc` +\n//! each PTY's spawn-time controller). The respawn's by:None Control re-attach →\n//! `become_controller` SILENTLY re-takes (same-local-identity `None==None`, the gate-#7 path\n//! that fires NO `Displaced` notice) → the incumbent local controller is ORPHANED: the broker\n//! stops writing its output but does NOT close the conn → no EOF, no `PumpEnd`, no 30s\n//! reconnect/detach splash (that fires only on `BrokerGone`/`Severed`). So the session FREEZES\n//! (output stops entirely, not just control) the instant the brain takes over. The stall-evict\n//! (>15s) is a SEPARATE, LATER, output-dependent symptom: sessions actively producing output\n//! make the stolen writer block >15s → evict (the field's 5); idle sessions never block →\n//! never evict (the field's 2) — but ALL are silently stolen+frozen. The 5-vs-2 split is\n//! ACTIVE-vs-IDLE, not remote-vs-local. This rig asserts the IMMEDIATE, deterministic signal —\n//! a held controller's output-tick freezes on the steal (RED) vs keeps ticking under the Viewer\n//! fix (GREEN) — NOT the flaky, timing-dependent stall-evict.\n//!\n//! ## The FIX and what this rig discriminates\n//!\n//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` no longer\n//! calls `resume_sessions` at all — it uses the cursor-only `resume_session_cursors` (NO\n//! subscribe), which supersedes the Viewer re-attach as the no-steal cure (no attach ⇒ no\n//! steal, a fortiori) AND fixes the SharedSend self-deadlock. This rig's no-steal\n//! discriminator (the operator's `A_i` controllers keep ticking across the swap) therefore\n//! passes for the stronger reason — the daemon brain never attaches. The Viewer text below\n//! describes the round-2 shape this superseded; the assertions are unchanged and still hold.\n//!\n//! `resume_sessions` (still used by DRAINING callers) re-attaches as **`AttachIntent::Viewer`**\n//! (a viewer never owns `driven_by` and is never stall-evicted). The RED→GREEN discriminator\n//! this rig asserts is\n//! the STEAL itself — the deterministic ROOT that CAUSES both A and B — observed as OUTPUT\n//! ROUTING:\n//!\n//! - Each session is spawned by a controller conn (`A_i`) the test HOLDS; `dispatch_spawn`\n//!   pre-attaches `A_i` as the session's LOCAL (by:None) controller. A reader thread on\n//!   `A_i` counts `KIND_OUTPUT` frames from a child that ticks forever (~150ms).\n//! - The real `spt daemon brain` candidate boots and `resume_sessions()` re-attaches all N.\n//! - **RED (Control re-attach):** `resolve_subscribe` `same_identity(None==None)` →\n//!   `become_controller` DROPS `A_i`'s sink → `A_i` is displaced → its tick counter FREEZES.\n//! - **GREEN (Viewer re-attach):** the brain joins as a viewer → `A_i` stays controller →\n//!   its tick counter KEEPS climbing across the swap.\n//!\n//! So the load-bearing assertion is DETERMINISTIC (output-routing displacement), not the\n//! flaky 15s wedge-maturity timing: RED starves every `A_i` the instant resume runs; GREEN\n//! never does. We also assert the trial still PROMOTES (no regression of\n//! REQ-UPDATE-TRIAL-DRAIN-DRIVE) — with no controller wedged, `old_gen_drained()` is true\n//! and the latched-ready candidate promotes.\n//!\n//! ## Scope note — Failure B (rc-retake deadline) is NOT asserted here (todlando, reported\n//! to doyle 2026-07-09). Failure B is the daemon-brain-conn SATURATION symptom; in an\n//! in-proc broker a local `rc` (by:None) `same_identity`-TAKES at the broker even in the RED\n//! state, so it does not discriminate here, and forcing real subprocess-brain-conn\n//! saturation is brittle (the same nondeterminism the counter-54 rig engineered around). We\n//! assert the ROOT (the steal) that CAUSES B; a full-daemon B repro is left to PR CI /\n//! field verification per doyle's ruling (heavy E2Es off the local gate).\n//!\n//! ## RUNNER CONTRACT — single `#[test]`, process-per-test (nextest) or -j1\n//!\n//! The SOLE `#[test]` here by design: it owns the process-global `SPT_HOME` (the subprocess\n…\n    let sup = {\n…\n        thread::spawn(move || {\n…\n        })\n    };\n\n    // ── The candidate boots, RUNS resume_sessions against the N present sessions, then\n    //    stamps ready. A present ready ⇒ resume (and the RED steal, if any) has completed. ──\n    let _cand_pid = wait_ready_pid(&ready_path, Duration::from_secs(45))\n        .unwrap_or_else(|| teardown_panic(&stop, \"the trial candidate never stamped brain.ready\"));\n\n    // ── DISCRIMINATOR (load-bearing, RED-first). Snapshot each A_i tick tally AT the\n    //    post-resume instant, then measure a window. GREEN (Viewer re-attach): A_i kept\n    //    control → its tally CLIMBS. RED (Control re-attach): resume become_controller'd the\n    //    slot → A_i's sink was dropped → its tally FREEZES. The child keeps ticking either\n    //    way (broker-hosted, independent of the brain), so a frozen tally is a displaced\n    //    controller, not a dead child. ──\n    let t0: Vec<u64> = sessions.iter().map(|(_, t)| t.load(Ordering::Relaxed)).collect();\n    // ~2s ≫ the 150ms tick, so GREEN accrues ~10+ ticks; comfortably clears CI jitter.\n    thread::sleep(Duration::from_secs(2));\n    let t1: Vec<u64> = sessions.iter().map(|(_, t)| t.load(Ordering::Relaxed)).collect();\n\n    // ── Wait out the trial verdict (assertion 3). With no wedged controller, GREEN drains\n    //    true and promotes within a heartbeat of ready; a RED that also wedged its stolen\n    //    controller may instead roll back — either way the discriminator above already fired.\n    let deadline = Instant::now() + Duration::from_secs(30);\n    let promoted = loop {\n        if !env.promotions.lock().unwrap().is_empty() {\n            break true;\n        }\n        if !env.rollbacks.lock().unwrap().is_empty() || Instant::now() >= deadline {\n            break false;\n        }\n        thread::sleep(Duration::from_millis(50));\n    };\n\n    // ── Teardown BEFORE asserting (so a failing assert still reaps the subprocess brain +\n    //    the N ticker children). ──\n    stop.store(true, Ordering::Relaxed);\n    let child_pids: Vec<Option<u32>> = sessions.iter().map(|(sid, _)| broker.session_pid(*sid)).collect();\n    for pid in child_pids.into_iter().flatten() {\n        kill_pid(pid);\n    }\n    let _ = sup.join();\n\n    let promotions = env.promotions.lock().unwrap().clone();\n    let rollbacks = env.rollbacks.lock().unwrap().clone();\n    let gained: Vec<u64> = t0.iter().zip(&t1).map(|(a, b)| b.saturating_sub(*a)).collect();\n    eprintln!(\n        \"=== UPDATE-WEDGE-2 RESUME-STEAL GATE: n={N_SESSIONS} ticks_before={t0:?} \\\n         ticks_after={t1:?} gained_in_window={gained:?} promoted={promoted} \\\n         promotions={promotions:?} rollbacks={rollbacks:?} \\\n         (GREEN = every gained>0 + promoted; RED = gained==0 on the stolen sessions) ===\"\n    );\n\n    // Assertion 1 (load-bearing, RED-first): EVERY session kept its controller across the\n    // swap — each A_i went on receiving output after the respawn. RED (resume re-attaches\n    // AttachIntent::Control): resolve_subscribe same_identity(None==None) become_controller'd\n    // each slot, dropping A_i → its tally froze → gained==0. GREEN (Viewer): A_i untouched.\n    // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    for (i, g) in gained.iter().enumerate() {\n        assert!(\n            *g > 0,\n            \"session {i} (sid {}) LOST its controller across the brain respawn: its \\\n             controller conn received {g} new output frames in the post-resume window (froze \\\n             at {}). The respawn's resume_sessions STOLE the controller slot (Control \\\n             re-attach → become_controller displaced the local controller) then never drained \\\n             it — Failure A. GREEN (resume re-attaches Viewer) leaves every controller intact. \\\n             ticks_before={t0:?} ticks_after={t1:?}\",\n            sessions[i].0,\n            t1[i],\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,null,396,null,399,null,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,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\resume_no_control_steal_e2e.rs"}}},"isError":false,"timestamp":1784195789179}}
{"type":"message","id":"da375c0b","parentId":"9c722c9d","timestamp":"2026-07-16T09:56:29.204Z","message":{"role":"toolResult","toolCallId":"call_9fozOaCtfEa0DzFMGad6KoUg|fc_008390f508d1d09b016a58aacd12dc819a89f894d5d984cfbc","toolName":"read","content":[{"type":"text","text":"mod tests {\n…\n    fn translate_key_event_emits_exact_xterm_vt() {\n…\n        assert_eq!(ev(KeyCode::Char('z'), ctrl), vec![0x1a]);\n\n        // --- Alt+<char> → ESC-prefixed (meta) ---\n        assert_eq!(ev(KeyCode::Char('x'), alt), vec![0x1b, b'x']);\n\n        // --- named editing/control keys ---\n        assert_eq!(ev(KeyCode::Enter, N), vec![b'\\r']); // 0x0d\n        assert_eq!(ev(KeyCode::Tab, N), vec![b'\\t']); // 0x09\n        assert_eq!(ev(KeyCode::BackTab, N), vec![0x1b, b'[', b'Z']);\n        assert_eq!(ev(KeyCode::Esc, N), vec![0x1b]);\n\n        // Backspace / Ctrl+Backspace — relocated W7 evidence:\n        // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]\n        assert_eq!(ev(KeyCode::Backspace, N), vec![0x7f]); // char-delete DEL\n        // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]\n        assert_eq!(ev(KeyCode::Backspace, ctrl), vec![0x08]); // word-delete ^H\n\n        // --- CSI tilde keys (unmodified) ---\n        assert_eq!(ev(KeyCode::Delete, N), vec![0x1b, b'[', b'3', b'~']);\n        assert_eq!(ev(KeyCode::Insert, N), vec![0x1b, b'[', b'2', b'~']);\n        assert_eq!(ev(KeyCode::PageUp, N), vec![0x1b, b'[', b'5', b'~']);\n        assert_eq!(ev(KeyCode::PageDown, N), vec![0x1b, b'[', b'6', b'~']);\n\n        // --- CSI final-byte keys (arrows / Home / End, unmodified) ---\n        assert_eq!(ev(KeyCode::Up, N), vec![0x1b, b'[', b'A']);\n        assert_eq!(ev(KeyCode::Down, N), vec![0x1b, b'[', b'B']);\n        assert_eq!(ev(KeyCode::Right, N), vec![0x1b, b'[', b'C']);\n        assert_eq!(ev(KeyCode::Left, N), vec![0x1b, b'[', b'D']);\n        assert_eq!(ev(KeyCode::Home, N), vec![0x1b, b'[', b'H']);\n        assert_eq!(ev(KeyCode::End, N), vec![0x1b, b'[', b'F']);\n\n        // --- MODIFIED specials: m = 1 + Shift + 2*Alt + 4*Ctrl ---\n        // Ctrl+Right (m=5) → ESC[1;5C\n        assert_eq!(\n            ev(KeyCode::Right, ctrl),\n            vec![0x1b, b'[', b'1', b';', b'5', b'C']\n        );\n        // Shift+Up (m=2) → ESC[1;2A\n        assert_eq!(\n            ev(KeyCode::Up, shift),\n            vec![0x1b, b'[', b'1', b';', b'2', b'A']\n        );\n        // Alt+Left (m=3) → ESC[1;3D\n        assert_eq!(\n            ev(KeyCode::Left, alt),\n            vec![0x1b, b'[', b'1', b';', b'3', b'D']\n        );\n        // Ctrl+Shift+Home (m=6) → ESC[1;6H\n        assert_eq!(\n            ev(KeyCode::Home, ctrl | shift),\n            vec![0x1b, b'[', b'1', b';', b'6', b'H']\n        );\n        // Ctrl+Delete (m=5) → ESC[3;5~\n        assert_eq!(\n            ev(KeyCode::Delete, ctrl),\n            vec![0x1b, b'[', b'3', b';', b'5', b'~']\n        );\n        // Shift+PageUp (m=2) → ESC[5;2~\n        assert_eq!(\n            ev(KeyCode::PageUp, shift),\n            vec![0x1b, b'[', b'5', b';', b'2', b'~']\n        );\n\n        // --- F-keys: F1–F4 = SS3 (ESC O P/Q/R/S); F5–F12 = CSI tilde codes ---\n        assert_eq!(ev(KeyCode::F(1), N), vec![0x1b, b'O', b'P']);\n        assert_eq!(ev(KeyCode::F(2), N), vec![0x1b, b'O', b'Q']);\n        assert_eq!(ev(KeyCode::F(3), N), vec![0x1b, b'O', b'R']);\n        assert_eq!(ev(KeyCode::F(4), N), vec![0x1b, b'O', b'S']);\n        assert_eq!(ev(KeyCode::F(5), N), vec![0x1b, b'[', b'1', b'5', b'~']);\n        assert_eq!(ev(KeyCode::F(6), N), vec![0x1b, b'[', b'1', b'7', b'~']);\n        assert_eq!(ev(KeyCode::F(7), N), vec![0x1b, b'[', b'1', b'8', b'~']);\n        assert_eq!(ev(KeyCode::F(8), N), vec![0x1b, b'[', b'1', b'9', b'~']);\n        assert_eq!(ev(KeyCode::F(9), N), vec![0x1b, b'[', b'2', b'0', b'~']);\n        assert_eq!(ev(KeyCode::F(10), N), vec![0x1b, b'[', b'2', b'1', b'~']);\n        assert_eq!(ev(KeyCode::F(11), N), vec![0x1b, b'[', b'2', b'3', b'~']);\n        assert_eq!(ev(KeyCode::F(12), N), vec![0x1b, b'[', b'2', b'4', b'~']);\n        // MODIFIED F-keys: Ctrl+F1 (m=5) → ESC[1;5P (SS3 promotes to CSI when modified);\n        // Ctrl+F5 (m=5) → ESC[15;5~\n        assert_eq!(\n            ev(KeyCode::F(1), ctrl),\n            vec![0x1b, b'[', b'1', b';', b'5', b'P']\n        );\n        assert_eq!(\n            ev(KeyCode::F(5), ctrl),\n            vec![0x1b, b'[', b'1', b'5', b';', b'5', b'~']\n        );\n\n        // --- unhandled codes → empty (the catch-all Vec::new()) ---\n        assert_eq!(ev(KeyCode::Null, N), Vec::<u8>::new());\n        assert_eq!(ev(KeyCode::CapsLock, N), Vec::<u8>::new());\n    }\n\n    // [unit->REQ-RC-KEY-VT-TRANSLATE] The event-sourced Option-B `ctrl-b d` detach\n    // SM (key-event path), mirroring the byte-path `detach_keybind_semantics`:\n    // Ctrl+B arms (swallowed); armed + plain 'd' detaches; armed + Ctrl+B forwards a\n    // literal 0x02; armed + other forwards 0x02 then the translated event; and\n    // critically the detach key is PLAIN 'd' only — Ctrl+D does NOT detach (it\n    // forwards 0x02 then its 0x04 translation). `armed` is mutated in place.\n    #[cfg(windows)]\n    #[test]\n    fn key_event_step_detach_sm() {\n        use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};\n        let ctrl = KeyModifiers::CONTROL;\n        let none = KeyModifiers::NONE;\n        let ke = |code, mods| KeyEvent::new(code, mods);\n\n        // unarmed Ctrl+B → Swallow, arms.\n        let mut armed = false;\n        match key_event_step(&mut armed, ke(KeyCode::Char('b'), ctrl)) {\n            KeyAction::Swallow => {}\n            other => panic!(\"unarmed Ctrl+B should Swallow, got {:?}\", action_name(&other)),\n        }\n        assert!(armed, \"Ctrl+B must arm the SM\");\n\n        // armed + plain 'd' → Detach.\n        match key_event_step(&mut armed, ke(KeyCode::Char('d'), none)) {\n            KeyAction::Detach => {}\n            other => panic!(\"armed + 'd' should Detach, got {:?}\", action_name(&other)),\n        }\n        assert!(!armed, \"Detach disarms\");\n\n        // armed + Ctrl+B → Forward(literal 0x02), disarms.\n        armed = true;\n        match key_event_step(&mut armed, ke(KeyCode::Char('b'), ctrl)) {\n            KeyAction::Forward(v) => assert_eq!(v, vec![0x02]),\n            other => panic!(\"armed + Ctrl+B should Forward [0x02], got {:?}\", action_name(&other)),\n        }\n        assert!(!armed);\n\n        // armed + plain 'x' → Forward(0x02 then translated 'x'), disarms.\n        armed = true;\n        match key_event_step(&mut armed, ke(KeyCode::Char('x'), none)) {\n            KeyAction::Forward(v) => assert_eq!(v, vec![0x02, b'x']),\n            other => panic!(\"armed + 'x' should Forward [0x02,'x'], got {:?}\", action_name(&other)),\n        }\n        assert!(!armed);\n\n        // armed + Ctrl+D → NOT Detach: Forward(0x02 then 0x04). Detach key is PLAIN 'd'.\n        armed = true;\n        match key_event_step(&mut armed, ke(KeyCode::Char('d'), ctrl)) {\n            KeyAction::Forward(v) => assert_eq!(\n                v,\n                vec![0x02, 0x04],\n                \"Ctrl+D must forward prefix + ^D, never detach\"\n            ),\n            other => panic!(\"armed + Ctrl+D must NOT Detach, got {:?}\", action_name(&other)),\n        }\n        assert!(!armed);\n\n        // unarmed plain 'a' → Forward(translated 'a'), stays disarmed.\n        armed = false;\n        match key_event_step(&mut armed, ke(KeyCode::Char('a'), none)) {\n            KeyAction::Forward(v) => assert_eq!(v, vec![b'a']),\n            other => panic!(\"unarmed 'a' should Forward ['a'], got {:?}\", action_name(&other)),\n        }\n        assert!(!armed);\n    }\n\n    /// Name a KeyAction for panic messages without requiring Debug on the enum.\n    #[cfg(windows)]\n    fn action_name(a: &KeyAction) -> &'static str {\n        match a {\n            KeyAction::Detach => \"Detach\",\n            KeyAction::Forward(_) => \"Forward\",\n            KeyAction::Swallow => \"Swallow\",\n        }\n    }\n\n    // [unit->REQ-RC-WIN-PASTE] the bracketed-paste wrap is EXACT (`ESC[200~` …\n    // `ESC[201~`) and the content is forwarded VERBATIM — a multi-line paste keeps\n    // its embedded newlines untranslated (the no-submit-storm property: CC sees the\n    // markers + treats the whole block as one paste, not a Enter-per-line storm).\n    #[cfg(windows)]\n    #[test]\n    fn bracketed_paste_framing_is_exact_and_content_verbatim() {\n        // Single line.\n        assert_eq!(wrap_bracketed_paste(b\"hi\"), b\"\\x1b[200~hi\\x1b[201~\".to_vec());\n        // Multi-line: newlines stay literal INSIDE the markers (no \\r submit-storm,\n        // no per-char translation), markers exactly once around the whole block.\n        let content = b\"line1\\nline2\\nline3\";\n        let mut expect = b\"\\x1b[200~\".to_vec();\n        expect.extend_from_slice(content);\n        expect.extend_from_slice(b\"\\x1b[201~\");\n        assert_eq!(wrap_bracketed_paste(content), expect);\n        // Empty content still frames (the caller — clipboard_paste — is what no-ops\n        // on empty; the wrap itself is pure framing).\n        assert_eq!(wrap_bracketed_paste(b\"\"), b\"\\x1b[200~\\x1b[201~\".to_vec());\n    }\n\n    // [unit->REQ-RC-WIN-PASTE] the right-click paste gesture is ONLY right-button\n    // DOWN; every other mouse event (left/middle, up/move/drag/scroll) is dropped.\n    #[cfg(windows)]\n    #[test]\n    fn mouse_is_paste_only_right_button_down() {\n        use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};\n        let me = |kind| MouseEvent {\n            kind,\n            column: 0,\n            row: 0,\n            modifiers: KeyModifiers::NONE,\n        };\n        assert!(mouse_is_paste(&me(MouseEventKind::Down(MouseButton::Right))));\n        assert!(!mouse_is_paste(&me(MouseEventKind::Up(MouseButton::Right))));\n        assert!(!mouse_is_paste(&me(MouseEventKind::Down(MouseButton::Left))));\n        assert!(!mouse_is_paste(&me(MouseEventKind::Down(MouseButton::Middle))));\n        assert!(!mouse_is_paste(&me(MouseEventKind::Moved)));\n        assert!(!mouse_is_paste(&me(MouseEventKind::Drag(MouseButton::Right))));\n        assert!(!mouse_is_paste(&me(MouseEventKind::ScrollDown)));\n    }\n\n    // [unit->REQ-RC-WIN-PASTE] the gesture→paste decision with an INJECTED clipboard\n    // reader (no real clipboard): a non-empty read ⇒ the bracketed paste; an empty\n    // read OR a read failure (`None`) ⇒ a clean no-op (never inject garbage).\n    #[cfg(windows)]\n    #[test]\n    fn clipboard_paste_wraps_nonempty_and_noops_on_empty_or_failure() {\n        // Non-empty clipboard → bracketed paste of the verbatim content.\n        assert_eq!(\n            clipboard_paste(|| Some(b\"pasted\".to_vec())),\n            Some(b\"\\x1b[200~pasted\\x1b[201~\".to_vec())\n        );\n        // Empty clipboard → no-op (not an empty bracketed frame — nothing to paste).\n        assert_eq!(clipboard_paste(|| Some(Vec::new())), None);\n        // Failed read → no-op.\n        assert_eq!(clipboard_paste(|| None), None);\n    }\n\n    // [unit->REQ-RC-MOUSE-FORWARD] the scroll-wheel classifier: ScrollUp/Down only,\n    // every other mouse event is a non-scroll (None → dropped or paste, not forwarded).\n    #[cfg(windows)]\n    #[test]\n    fn scroll_dir_classifies_wheel_only() {\n        use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};\n        let me = |kind| MouseEvent {\n            kind,\n            column: 0,\n            row: 0,\n            modifiers: KeyModifiers::NONE,\n        };\n        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollUp)), Some(true));\n        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollDown)), Some(false));\n        assert_eq!(scroll_dir(&me(MouseEventKind::Down(MouseButton::Right))), None);\n        assert_eq!(scroll_dir(&me(MouseEventKind::Moved)), None);\n        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollLeft)), None);\n    }\n\n    // [unit->REQ-RC-MOUSE-FORWARD] the scroll→SGR encoder emits EXACT xterm bytes:\n    // ESC[<btn;col+1;row+1M, btn 64=up/65=down, 0-based crossterm → 1-based xterm.\n    #[cfg(windows)]\n    #[test]\n    fn scroll_sgr_emits_exact_xterm_bytes() {\n        assert_eq!(scroll_sgr(true, 0, 0), b\"\\x1b[<64;1;1M\".to_vec());\n        assert_eq!(scroll_sgr(false, 0, 0), b\"\\x1b[<65;1;1M\".to_vec());\n        // 0-based (col=4,row=9) → 1-based (5,10).\n        assert_eq!(scroll_sgr(true, 4, 9), b\"\\x1b[<64;5;10M\".to_vec());\n        assert_eq!(scroll_sgr(false, 79, 23), b\"\\x1b[<65;80;24M\".to_vec());\n    }\n\n    // [unit->REQ-RC-MOUSE-FORWARD] the DECSET scan tracks the harness's mouse-report\n    // mode from its output: 1000/1002/1003 toggle `enabled`, 1006 toggles `sgr`,\n    // a combined `ESC[?1002;1006h` sets both, and `l` resets — over a mixed stream.\n    #[test]\n    fn mouse_mode_decset_scan_tracks_enable_and_sgr() {\n        use std::sync::atomic::Ordering::Acquire;\n        let mode = MouseMode::default();\n        let mut sc = MouseModeScanner::default();\n        // Plain output, no DECSET → unchanged.\n        sc.feed(&mode, b\"hello world\\x1b[0m\");\n        assert!(!mode.enabled.load(Acquire));\n        assert!(!mode.sgr.load(Acquire));\n        // Combined enable (button-event tracking + SGR), amid other output.\n        sc.feed(&mode, b\"before\\x1b[?1002;1006hafter\");\n        assert!(mode.enabled.load(Acquire));\n        assert!(mode.sgr.load(Acquire));\n        // 1003 (any-event) also enables; a non-mouse private mode (25 cursor) is ignored.\n        sc.feed(&mode, b\"\\x1b[?25h\\x1b[?1003h\");\n        assert!(mode.enabled.load(Acquire));\n        // Reset mouse but keep SGR.\n        sc.feed(&mode, b\"\\x1b[?1003l\");\n        assert!(!mode.enabled.load(Acquire));\n        assert!(mode.sgr.load(Acquire));\n        // Reset SGR.\n        sc.feed(&mode, b\"\\x1b[?1006l\");\n        assert!(!mode.sgr.load(Acquire));\n    }\n\n    // [unit->REQ-RC-MOUSE-FORWARD] a DECSET sequence SPLIT across two output chunks\n    // (the harness's enable can land across a decoder boundary) is still tracked —\n    // the scanner carries the partial `ESC[?…` to the next feed.\n    #[test]\n    fn mouse_mode_decset_scan_survives_split_across_chunks() {\n        use std::sync::atomic::Ordering::Acquire;\n        let mode = MouseMode::default();\n        let mut sc = MouseModeScanner::default();\n        // `ESC[?1006h` split right in the middle of the digits.\n        sc.feed(&mode, b\"tail\\x1b[?100\");\n        assert!(!mode.sgr.load(Acquire), \"incomplete half must not toggle yet\");\n        sc.feed(&mode, b\"6hmore\");\n        assert!(mode.sgr.load(Acquire), \"the completed sequence toggles sgr\");\n        // Split at the very ESC/[/? boundary too.\n        sc.feed(&mode, b\"x\\x1b\");\n        sc.feed(&mode, b\"[?1002h\");\n        assert!(mode.enabled.load(Acquire));\n    }\n\n    // ── REQ-RC-IDENTITY: the controller reserved-row status banner ──────────────\n\n    // [unit->REQ-RC-IDMARKER-DISABLE] bug #14: the top-right id marker is shipped\n    // DISABLED — the controller path yields NO reserved status row (and a viewer\n    // never had one). If someone flips STATUS_ROW_ENABLED back on, the controller\n    // assert here trips as a reminder to re-verify the sticky-overlay behavior.\n    #[test]\n    fn status_row_marker_is_disabled_by_flag() {\n        assert!(!status_row_active(false), \"controller: id marker is OFF (#14)\");\n        assert!(!status_row_active(true), \"a viewer never owns a status row\");\n    }\n\n    // [unit->REQ-RC-IDENTITY] the identity line format: `{subnet} : {id} @ {node}`,\n    // with the ` @ {node}` tail OMITTED for an absent / empty / whitespace node\n    // label (never render a lie) — and otherwise present verbatim.\n    #[test]\n    fn identity_line_format() {\n        assert_eq!(\n            identity_line(\"ops\", \"doyle\", Some(\"HFENDULEAM\")),\n            \"ops : doyle @ HFENDULEAM\"\n        );\n        // local (unset home subnet) still renders the subnet slot.\n        assert_eq!(identity_line(\"local\", \"perri\", Some(\"box\")), \"local : perri @ box\");\n        // No node → tail omitted.\n        assert_eq!(identity_line(\"local\", \"perri\", None), \"local : perri\");\n        // Empty / whitespace node → tail omitted (trimmed, never a bare ` @ `).\n        assert_eq!(identity_line(\"local\", \"perri\", Some(\"\")), \"local : perri\");\n        assert_eq!(identity_line(\"local\", \"perri\", Some(\"   \")), \"local : perri\");\n    }\n\n    // [unit->REQ-RC-IDENTITY] the DECSTBM margin assert reserves row 1 EXACTLY:\n    // `ESC[2;{rows}r` (top margin = line 2) followed by the repaint. Asserting the\n    // exact byte prefix is what makes a one-char drift (wrong margin, off-by-one\n    // bottom) fail.\n    #[test]\n    fn status_assert_reserves_row1_then_repaints() {\n        let s = StatusRow { text: \"x\".to_string(), rows: 24, cols: 80 };\n        let bytes = s.assert_bytes();\n        // The margin set comes first.\n        assert!(\n            bytes.starts_with(b\"\\x1b[2;24r\"),\n            \"assert must set the reserved-row scroll region first: {:?}\",\n            String::from_utf8_lossy(&bytes)\n        );\n        // The rest is exactly the repaint.\n        assert_eq!(&bytes[b\"\\x1b[2;24r\".len()..], &s.repaint_bytes()[..]);\n    }\n\n    // [unit->REQ-RC-IDENTITY] the repaint is the EXACT save→home→clear→right-align\n    // →cyan→text→reset→restore sequence, and the right-align column math:\n    // col = cols - width + 1 (1-based), clamped to col 1 when the text is ≥ cols.\n    #[test]\n    fn status_repaint_exact_bytes_and_right_align() {\n        // Short text, right-aligned: width 5 in 80 cols → start col 76.\n        let s = StatusRow { text: \"abcde\".to_string(), rows: 24, cols: 80 };\n        let mut expect = Vec::new();\n        expect.extend_from_slice(b\"\\x1b7\"); // DECSC\n        expect.extend_from_slice(b\"\\x1b[1;1H\"); // home\n        expect.extend_from_slice(b\"\\x1b[2K\"); // clear line\n        expect.extend_from_slice(b\"\\x1b[1;76H\"); // right-align start (80-5+1)\n        expect.extend_from_slice(b\"\\x1b[36m\"); // cyan\n        expect.extend_from_slice(b\"abcde\");\n        expect.extend_from_slice(b\"\\x1b[0m\"); // reset\n        expect.extend_from_slice(b\"\\x1b8\"); // DECRC\n        assert_eq!(s.repaint_bytes(), expect);\n\n        // Exact fit: width == cols → col 1.\n        let s = StatusRow { text: \"abcd\".to_string(), rows: 10, cols: 4 };\n        assert!(s.repaint_bytes().windows(6).any(|w| w == b\"\\x1b[1;1H\"));\n\n        // Text WIDER than cols → clamp to col 1 (never col 0 / negative).\n        let s = StatusRow { text: \"wide banner text\".to_string(), rows: 10, cols: 4 };\n        let bytes = s.repaint_bytes();\n        assert!(\n            bytes.windows(6).any(|w| w == b\"\\x1b[1;1H\"),\n            \"over-wide text clamps the right-align start to col 1: {:?}\",\n            String::from_utf8_lossy(&bytes)\n        );\n        // The text still rides verbatim.\n        assert!(\n            bytes.windows(b\"wide banner text\".len()).any(|w| w == b\"wide banner text\")\n        );\n    }\n\n    // [unit->REQ-RC-IDENTITY] restore tears the reserved row down: reset the scroll\n    // region to full screen (`ESC[r`) then clear the reclaimed row 1 — EXACT bytes.\n    #[test]\n    fn status_restore_exact_bytes() {\n        assert_eq!(StatusRow::restore_bytes(), b\"\\x1b[r\\x1b[1;1H\\x1b[2K\".to_vec());\n    }\n\n    // [unit->REQ-RC-IDENTITY] update_size re-points the tracked terminal size (the\n    // banner repaints on the NEW geometry after a window change).\n    #[test]\n    fn status_update_size_repoints_geometry() {\n        let mut s = StatusRow { text: \"id\".to_string(), rows: 24, cols: 80 };\n        s.update_size(40, 120);\n        assert_eq!((s.rows, s.cols), (40, 120));\n        // The repaint now right-aligns against 120 cols: 120 - 2 + 1 = 119.\n        assert!(s.repaint_bytes().windows(8).any(|w| w == b\"\\x1b[1;119H\"));\n        // And the assert reserves 40 rows.\n        assert!(s.assert_bytes().starts_with(b\"\\x1b[2;40r\"));\n    }\n\n    // [unit->REQ-RC-IDENTITY] the re-assert scanner fires on the region-destroying\n    // triggers and ONLY those: alt-screen enter (`?1049h`, legacy `?47h`/`?1047h`)\n    // and a bare DECSTBM reset (`ESC[r`/`ESC[0r`/`ESC[;r`, no leading `?`); it\n    // ignores an alt-screen EXIT (`?1049l`), an unrelated private mode, a real\n    // DECSTBM SET (`ESC[2;24r`, has non-zero params), and other CSIs.\n    #[test]\n    fn reassert_scanner_triggers_only_on_region_destroyers() {\n        // Alt-screen enter (modern + both legacy spellings) → fire.\n        assert!(ReassertScanner::default().feed(b\"out\\x1b[?1049hmore\"));\n        assert!(ReassertScanner::default().feed(b\"\\x1b[?47h\"));\n        assert!(ReassertScanner::default().feed(b\"\\x1b[?1047h\"));\n        // Bare DECSTBM reset, all three no/zero-param spellings → fire.\n        assert!(ReassertScanner::default().feed(b\"\\x1b[r\"));\n        assert!(ReassertScanner::default().feed(b\"\\x1b[0r\"));\n        assert!(ReassertScanner::default().feed(b\"\\x1b[;r\"));\n        // Alt-screen EXIT (reset) is NOT a destroyer (we re-assert on RE-ENTRY).\n        assert!(!ReassertScanner::default().feed(b\"\\x1b[?1049l\"));\n        // An unrelated private mode (cursor visibility) → no fire.\n        assert!(!ReassertScanner::default().feed(b\"\\x1b[?25h\"));\n        // A real DECSTBM SET (our own / the harness's margin) has non-zero params →\n        // not a reset, no fire.\n        assert!(!ReassertScanner::default().feed(b\"\\x1b[2;24r\"));\n        // Other CSIs (cursor move, SGR) → no fire.\n        assert!(!ReassertScanner::default().feed(b\"\\x1b[10;5H\\x1b[0m\"));\n        // Plain text → no fire (fast path).\n        assert!(!ReassertScanner::default().feed(b\"hello world\"));\n    }\n\n    // [unit->REQ-RC-IDENTITY] a trigger SPLIT across two output chunks is still\n    // detected — the scanner carries the partial `ESC[…` to the next feed (mirrors\n    // the mouse scanner's carry discipline).\n    #[test]\n    fn reassert_scanner_survives_split_across_chunks() {\n        // `ESC[?1049h` split mid-digits.\n        let mut sc = ReassertScanner::default();\n        assert!(!sc.feed(b\"tail\\x1b[?10\"), \"incomplete half must not fire yet\");\n        assert!(sc.feed(b\"49hmore\"), \"the completed alt-screen enter fires\");\n        // `ESC[r` split at the ESC/[ boundary.\n        let mut sc = ReassertScanner::default();\n        assert!(!sc.feed(b\"x\\x1b\"));\n        assert!(sc.feed(b\"[r\"));\n    }\n\n    // [int->REQ-RC-IDENTITY] the WIRED controller status sequence the pump drives,\n    // exercised end-to-end over a real `impl Write` sink (a `Vec<u8>`), using the\n    // exact operations the pump performs in order: (a) on attach START it writes\n    // `assert_bytes()` (reserve row 1 + paint); (b) when harness output trips the\n    // re-assert scanner (an alt-screen enter) it writes `assert_bytes()` (margin +\n    // paint — the region was destroyed); (c) on exit it writes `restore_bytes()`.\n    // Asserting the captured byte stream proves\n    // the reserved-row contract holds through the pump's status hooks WITHOUT a live\n    // broker (rc's pump needs a Brain + PTY peer that an in-process unit can't\n    // stand up; this drives the same status state-machine the pump invokes, against\n    // the Write seam the pump was genericized to accept).\n    #[test]\n    fn pump_status_emit_sequence_over_write_sink() {\n        use std::io::Write as _;\n        let status = StatusRow {\n            text: identity_line(\"local\", \"doyle\", Some(\"BOX\")),\n            rows: 24,\n            cols: 80,\n        };\n        let mut scanner = ReassertScanner::default();\n        let mut sink: Vec<u8> = Vec::new();\n\n        // (a) pump START: reserve + paint.\n        sink.write_all(&status.assert_bytes()).unwrap();\n        let assert_len = sink.len();\n        assert!(sink.starts_with(b\"\\x1b[2;24r\"), \"start reserves the row\");\n        assert!(\n            sink.windows(b\"local : doyle @ BOX\".len()).any(|w| w == b\"local : doyle @ BOX\"),\n            \"the identity banner is painted at start\"\n        );\n\n        // (b) harness output WITHOUT a trigger → no re-assert emitted.\n        let plain = b\"hello from the harness\\r\\n\";\n        if scanner.feed(plain) {\n            sink.write_all(&status.assert_bytes()).unwrap();\n        }\n        assert_eq!(sink.len(), assert_len, \"plain output emits no status re-assert\");\n\n        // (b') harness output WITH an alt-screen enter → re-assert (MARGIN + paint).\n        // The trigger destroyed the scroll region, so the pump re-sets the margin,\n        // not just the text — assert the margin sequence reappears.\n        let alt = b\"\\x1b[?1049hfull-screen app\";\n        let mut fired = false;\n        let before = sink.len();\n        if scanner.feed(alt) {\n            fired = true;\n            sink.write_all(&status.assert_bytes()).unwrap();\n        }\n        assert!(fired, \"the alt-screen enter trips the re-assert scanner\");\n        assert!(sink.len() > before, \"a re-assert was appended after the trigger\");\n        assert!(\n            sink[before..].starts_with(b\"\\x1b[2;24r\"),\n            \"the re-assert re-sets the DECSTBM margin (not just a repaint)\"\n        );\n\n        // (c) pump EXIT: restore (reset region + clear row 1).\n        sink.write_all(&StatusRow::restore_bytes()).unwrap();\n        assert!(\n            sink.ends_with(b\"\\x1b[r\\x1b[1;1H\\x1b[2K\"),\n            \"exit restores the full scroll region and clears the reserved row\"\n        );\n    }\n}\n"}],"details":{"displayContent":{"text":"mod tests {\n…\n    fn translate_key_event_emits_exact_xterm_vt() {\n…\n        assert_eq!(ev(KeyCode::Char('z'), ctrl), vec![0x1a]);\r\n\r\n        // --- Alt+<char> → ESC-prefixed (meta) ---\r\n        assert_eq!(ev(KeyCode::Char('x'), alt), vec![0x1b, b'x']);\r\n\r\n        // --- named editing/control keys ---\r\n        assert_eq!(ev(KeyCode::Enter, N), vec![b'\\r']); // 0x0d\r\n        assert_eq!(ev(KeyCode::Tab, N), vec![b'\\t']); // 0x09\r\n        assert_eq!(ev(KeyCode::BackTab, N), vec![0x1b, b'[', b'Z']);\r\n        assert_eq!(ev(KeyCode::Esc, N), vec![0x1b]);\r\n\r\n        // Backspace / Ctrl+Backspace — relocated W7 evidence:\r\n        // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]\r\n        assert_eq!(ev(KeyCode::Backspace, N), vec![0x7f]); // char-delete DEL\r\n        // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]\r\n        assert_eq!(ev(KeyCode::Backspace, ctrl), vec![0x08]); // word-delete ^H\r\n\r\n        // --- CSI tilde keys (unmodified) ---\r\n        assert_eq!(ev(KeyCode::Delete, N), vec![0x1b, b'[', b'3', b'~']);\r\n        assert_eq!(ev(KeyCode::Insert, N), vec![0x1b, b'[', b'2', b'~']);\r\n        assert_eq!(ev(KeyCode::PageUp, N), vec![0x1b, b'[', b'5', b'~']);\r\n        assert_eq!(ev(KeyCode::PageDown, N), vec![0x1b, b'[', b'6', b'~']);\r\n\r\n        // --- CSI final-byte keys (arrows / Home / End, unmodified) ---\r\n        assert_eq!(ev(KeyCode::Up, N), vec![0x1b, b'[', b'A']);\r\n        assert_eq!(ev(KeyCode::Down, N), vec![0x1b, b'[', b'B']);\r\n        assert_eq!(ev(KeyCode::Right, N), vec![0x1b, b'[', b'C']);\r\n        assert_eq!(ev(KeyCode::Left, N), vec![0x1b, b'[', b'D']);\r\n        assert_eq!(ev(KeyCode::Home, N), vec![0x1b, b'[', b'H']);\r\n        assert_eq!(ev(KeyCode::End, N), vec![0x1b, b'[', b'F']);\r\n\r\n        // --- MODIFIED specials: m = 1 + Shift + 2*Alt + 4*Ctrl ---\r\n        // Ctrl+Right (m=5) → ESC[1;5C\r\n        assert_eq!(\r\n            ev(KeyCode::Right, ctrl),\r\n            vec![0x1b, b'[', b'1', b';', b'5', b'C']\r\n        );\r\n        // Shift+Up (m=2) → ESC[1;2A\r\n        assert_eq!(\r\n            ev(KeyCode::Up, shift),\r\n            vec![0x1b, b'[', b'1', b';', b'2', b'A']\r\n        );\r\n        // Alt+Left (m=3) → ESC[1;3D\r\n        assert_eq!(\r\n            ev(KeyCode::Left, alt),\r\n            vec![0x1b, b'[', b'1', b';', b'3', b'D']\r\n        );\r\n        // Ctrl+Shift+Home (m=6) → ESC[1;6H\r\n        assert_eq!(\r\n            ev(KeyCode::Home, ctrl | shift),\r\n            vec![0x1b, b'[', b'1', b';', b'6', b'H']\r\n        );\r\n        // Ctrl+Delete (m=5) → ESC[3;5~\r\n        assert_eq!(\r\n            ev(KeyCode::Delete, ctrl),\r\n            vec![0x1b, b'[', b'3', b';', b'5', b'~']\r\n        );\r\n        // Shift+PageUp (m=2) → ESC[5;2~\r\n        assert_eq!(\r\n            ev(KeyCode::PageUp, shift),\r\n            vec![0x1b, b'[', b'5', b';', b'2', b'~']\r\n        );\r\n\r\n        // --- F-keys: F1–F4 = SS3 (ESC O P/Q/R/S); F5–F12 = CSI tilde codes ---\r\n        assert_eq!(ev(KeyCode::F(1), N), vec![0x1b, b'O', b'P']);\r\n        assert_eq!(ev(KeyCode::F(2), N), vec![0x1b, b'O', b'Q']);\r\n        assert_eq!(ev(KeyCode::F(3), N), vec![0x1b, b'O', b'R']);\r\n        assert_eq!(ev(KeyCode::F(4), N), vec![0x1b, b'O', b'S']);\r\n        assert_eq!(ev(KeyCode::F(5), N), vec![0x1b, b'[', b'1', b'5', b'~']);\r\n        assert_eq!(ev(KeyCode::F(6), N), vec![0x1b, b'[', b'1', b'7', b'~']);\r\n        assert_eq!(ev(KeyCode::F(7), N), vec![0x1b, b'[', b'1', b'8', b'~']);\r\n        assert_eq!(ev(KeyCode::F(8), N), vec![0x1b, b'[', b'1', b'9', b'~']);\r\n        assert_eq!(ev(KeyCode::F(9), N), vec![0x1b, b'[', b'2', b'0', b'~']);\r\n        assert_eq!(ev(KeyCode::F(10), N), vec![0x1b, b'[', b'2', b'1', b'~']);\r\n        assert_eq!(ev(KeyCode::F(11), N), vec![0x1b, b'[', b'2', b'3', b'~']);\r\n        assert_eq!(ev(KeyCode::F(12), N), vec![0x1b, b'[', b'2', b'4', b'~']);\r\n        // MODIFIED F-keys: Ctrl+F1 (m=5) → ESC[1;5P (SS3 promotes to CSI when modified);\r\n        // Ctrl+F5 (m=5) → ESC[15;5~\r\n        assert_eq!(\r\n            ev(KeyCode::F(1), ctrl),\r\n            vec![0x1b, b'[', b'1', b';', b'5', b'P']\r\n        );\r\n        assert_eq!(\r\n            ev(KeyCode::F(5), ctrl),\r\n            vec![0x1b, b'[', b'1', b'5', b';', b'5', b'~']\r\n        );\r\n\r\n        // --- unhandled codes → empty (the catch-all Vec::new()) ---\r\n        assert_eq!(ev(KeyCode::Null, N), Vec::<u8>::new());\r\n        assert_eq!(ev(KeyCode::CapsLock, N), Vec::<u8>::new());\r\n    }\r\n\r\n    // [unit->REQ-RC-KEY-VT-TRANSLATE] The event-sourced Option-B `ctrl-b d` detach\r\n    // SM (key-event path), mirroring the byte-path `detach_keybind_semantics`:\r\n    // Ctrl+B arms (swallowed); armed + plain 'd' detaches; armed + Ctrl+B forwards a\r\n    // literal 0x02; armed + other forwards 0x02 then the translated event; and\r\n    // critically the detach key is PLAIN 'd' only — Ctrl+D does NOT detach (it\r\n    // forwards 0x02 then its 0x04 translation). `armed` is mutated in place.\r\n    #[cfg(windows)]\r\n    #[test]\r\n    fn key_event_step_detach_sm() {\r\n        use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};\r\n        let ctrl = KeyModifiers::CONTROL;\r\n        let none = KeyModifiers::NONE;\r\n        let ke = |code, mods| KeyEvent::new(code, mods);\r\n\r\n        // unarmed Ctrl+B → Swallow, arms.\r\n        let mut armed = false;\r\n        match key_event_step(&mut armed, ke(KeyCode::Char('b'), ctrl)) {\r\n            KeyAction::Swallow => {}\r\n            other => panic!(\"unarmed Ctrl+B should Swallow, got {:?}\", action_name(&other)),\r\n        }\r\n        assert!(armed, \"Ctrl+B must arm the SM\");\r\n\r\n        // armed + plain 'd' → Detach.\r\n        match key_event_step(&mut armed, ke(KeyCode::Char('d'), none)) {\r\n            KeyAction::Detach => {}\r\n            other => panic!(\"armed + 'd' should Detach, got {:?}\", action_name(&other)),\r\n        }\r\n        assert!(!armed, \"Detach disarms\");\r\n\r\n        // armed + Ctrl+B → Forward(literal 0x02), disarms.\r\n        armed = true;\r\n        match key_event_step(&mut armed, ke(KeyCode::Char('b'), ctrl)) {\r\n            KeyAction::Forward(v) => assert_eq!(v, vec![0x02]),\r\n            other => panic!(\"armed + Ctrl+B should Forward [0x02], got {:?}\", action_name(&other)),\r\n        }\r\n        assert!(!armed);\r\n\r\n        // armed + plain 'x' → Forward(0x02 then translated 'x'), disarms.\r\n        armed = true;\r\n        match key_event_step(&mut armed, ke(KeyCode::Char('x'), none)) {\r\n            KeyAction::Forward(v) => assert_eq!(v, vec![0x02, b'x']),\r\n            other => panic!(\"armed + 'x' should Forward [0x02,'x'], got {:?}\", action_name(&other)),\r\n        }\r\n        assert!(!armed);\r\n\r\n        // armed + Ctrl+D → NOT Detach: Forward(0x02 then 0x04). Detach key is PLAIN 'd'.\r\n        armed = true;\r\n        match key_event_step(&mut armed, ke(KeyCode::Char('d'), ctrl)) {\r\n            KeyAction::Forward(v) => assert_eq!(\r\n                v,\r\n                vec![0x02, 0x04],\r\n                \"Ctrl+D must forward prefix + ^D, never detach\"\r\n            ),\r\n            other => panic!(\"armed + Ctrl+D must NOT Detach, got {:?}\", action_name(&other)),\r\n        }\r\n        assert!(!armed);\r\n\r\n        // unarmed plain 'a' → Forward(translated 'a'), stays disarmed.\r\n        armed = false;\r\n        match key_event_step(&mut armed, ke(KeyCode::Char('a'), none)) {\r\n            KeyAction::Forward(v) => assert_eq!(v, vec![b'a']),\r\n            other => panic!(\"unarmed 'a' should Forward ['a'], got {:?}\", action_name(&other)),\r\n        }\r\n        assert!(!armed);\r\n    }\r\n\r\n    /// Name a KeyAction for panic messages without requiring Debug on the enum.\r\n    #[cfg(windows)]\r\n    fn action_name(a: &KeyAction) -> &'static str {\r\n        match a {\r\n            KeyAction::Detach => \"Detach\",\r\n            KeyAction::Forward(_) => \"Forward\",\r\n            KeyAction::Swallow => \"Swallow\",\r\n        }\r\n    }\r\n\r\n    // [unit->REQ-RC-WIN-PASTE] the bracketed-paste wrap is EXACT (`ESC[200~` …\r\n    // `ESC[201~`) and the content is forwarded VERBATIM — a multi-line paste keeps\r\n    // its embedded newlines untranslated (the no-submit-storm property: CC sees the\r\n    // markers + treats the whole block as one paste, not a Enter-per-line storm).\r\n    #[cfg(windows)]\r\n    #[test]\r\n    fn bracketed_paste_framing_is_exact_and_content_verbatim() {\r\n        // Single line.\r\n        assert_eq!(wrap_bracketed_paste(b\"hi\"), b\"\\x1b[200~hi\\x1b[201~\".to_vec());\r\n        // Multi-line: newlines stay literal INSIDE the markers (no \\r submit-storm,\r\n        // no per-char translation), markers exactly once around the whole block.\r\n        let content = b\"line1\\nline2\\nline3\";\r\n        let mut expect = b\"\\x1b[200~\".to_vec();\r\n        expect.extend_from_slice(content);\r\n        expect.extend_from_slice(b\"\\x1b[201~\");\r\n        assert_eq!(wrap_bracketed_paste(content), expect);\r\n        // Empty content still frames (the caller — clipboard_paste — is what no-ops\r\n        // on empty; the wrap itself is pure framing).\r\n        assert_eq!(wrap_bracketed_paste(b\"\"), b\"\\x1b[200~\\x1b[201~\".to_vec());\r\n    }\r\n\r\n    // [unit->REQ-RC-WIN-PASTE] the right-click paste gesture is ONLY right-button\r\n    // DOWN; every other mouse event (left/middle, up/move/drag/scroll) is dropped.\r\n    #[cfg(windows)]\r\n    #[test]\r\n    fn mouse_is_paste_only_right_button_down() {\r\n        use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};\r\n        let me = |kind| MouseEvent {\r\n            kind,\r\n            column: 0,\r\n            row: 0,\r\n            modifiers: KeyModifiers::NONE,\r\n        };\r\n        assert!(mouse_is_paste(&me(MouseEventKind::Down(MouseButton::Right))));\r\n        assert!(!mouse_is_paste(&me(MouseEventKind::Up(MouseButton::Right))));\r\n        assert!(!mouse_is_paste(&me(MouseEventKind::Down(MouseButton::Left))));\r\n        assert!(!mouse_is_paste(&me(MouseEventKind::Down(MouseButton::Middle))));\r\n        assert!(!mouse_is_paste(&me(MouseEventKind::Moved)));\r\n        assert!(!mouse_is_paste(&me(MouseEventKind::Drag(MouseButton::Right))));\r\n        assert!(!mouse_is_paste(&me(MouseEventKind::ScrollDown)));\r\n    }\r\n\r\n    // [unit->REQ-RC-WIN-PASTE] the gesture→paste decision with an INJECTED clipboard\r\n    // reader (no real clipboard): a non-empty read ⇒ the bracketed paste; an empty\r\n    // read OR a read failure (`None`) ⇒ a clean no-op (never inject garbage).\r\n    #[cfg(windows)]\r\n    #[test]\r\n    fn clipboard_paste_wraps_nonempty_and_noops_on_empty_or_failure() {\r\n        // Non-empty clipboard → bracketed paste of the verbatim content.\r\n        assert_eq!(\r\n            clipboard_paste(|| Some(b\"pasted\".to_vec())),\r\n            Some(b\"\\x1b[200~pasted\\x1b[201~\".to_vec())\r\n        );\r\n        // Empty clipboard → no-op (not an empty bracketed frame — nothing to paste).\r\n        assert_eq!(clipboard_paste(|| Some(Vec::new())), None);\r\n        // Failed read → no-op.\r\n        assert_eq!(clipboard_paste(|| None), None);\r\n    }\r\n\r\n    // [unit->REQ-RC-MOUSE-FORWARD] the scroll-wheel classifier: ScrollUp/Down only,\r\n    // every other mouse event is a non-scroll (None → dropped or paste, not forwarded).\r\n    #[cfg(windows)]\r\n    #[test]\r\n    fn scroll_dir_classifies_wheel_only() {\r\n        use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};\r\n        let me = |kind| MouseEvent {\r\n            kind,\r\n            column: 0,\r\n            row: 0,\r\n            modifiers: KeyModifiers::NONE,\r\n        };\r\n        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollUp)), Some(true));\r\n        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollDown)), Some(false));\r\n        assert_eq!(scroll_dir(&me(MouseEventKind::Down(MouseButton::Right))), None);\r\n        assert_eq!(scroll_dir(&me(MouseEventKind::Moved)), None);\r\n        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollLeft)), None);\r\n    }\r\n\r\n    // [unit->REQ-RC-MOUSE-FORWARD] the scroll→SGR encoder emits EXACT xterm bytes:\r\n    // ESC[<btn;col+1;row+1M, btn 64=up/65=down, 0-based crossterm → 1-based xterm.\r\n    #[cfg(windows)]\r\n    #[test]\r\n    fn scroll_sgr_emits_exact_xterm_bytes() {\r\n        assert_eq!(scroll_sgr(true, 0, 0), b\"\\x1b[<64;1;1M\".to_vec());\r\n        assert_eq!(scroll_sgr(false, 0, 0), b\"\\x1b[<65;1;1M\".to_vec());\r\n        // 0-based (col=4,row=9) → 1-based (5,10).\r\n        assert_eq!(scroll_sgr(true, 4, 9), b\"\\x1b[<64;5;10M\".to_vec());\r\n        assert_eq!(scroll_sgr(false, 79, 23), b\"\\x1b[<65;80;24M\".to_vec());\r\n    }\r\n\r\n    // [unit->REQ-RC-MOUSE-FORWARD] the DECSET scan tracks the harness's mouse-report\r\n    // mode from its output: 1000/1002/1003 toggle `enabled`, 1006 toggles `sgr`,\r\n    // a combined `ESC[?1002;1006h` sets both, and `l` resets — over a mixed stream.\r\n    #[test]\r\n    fn mouse_mode_decset_scan_tracks_enable_and_sgr() {\r\n        use std::sync::atomic::Ordering::Acquire;\r\n        let mode = MouseMode::default();\r\n        let mut sc = MouseModeScanner::default();\r\n        // Plain output, no DECSET → unchanged.\r\n        sc.feed(&mode, b\"hello world\\x1b[0m\");\r\n        assert!(!mode.enabled.load(Acquire));\r\n        assert!(!mode.sgr.load(Acquire));\r\n        // Combined enable (button-event tracking + SGR), amid other output.\r\n        sc.feed(&mode, b\"before\\x1b[?1002;1006hafter\");\r\n        assert!(mode.enabled.load(Acquire));\r\n        assert!(mode.sgr.load(Acquire));\r\n        // 1003 (any-event) also enables; a non-mouse private mode (25 cursor) is ignored.\r\n        sc.feed(&mode, b\"\\x1b[?25h\\x1b[?1003h\");\r\n        assert!(mode.enabled.load(Acquire));\r\n        // Reset mouse but keep SGR.\r\n        sc.feed(&mode, b\"\\x1b[?1003l\");\r\n        assert!(!mode.enabled.load(Acquire));\r\n        assert!(mode.sgr.load(Acquire));\r\n        // Reset SGR.\r\n        sc.feed(&mode, b\"\\x1b[?1006l\");\r\n        assert!(!mode.sgr.load(Acquire));\r\n    }\r\n\r\n    // [unit->REQ-RC-MOUSE-FORWARD] a DECSET sequence SPLIT across two output chunks\r\n    // (the harness's enable can land across a decoder boundary) is still tracked —\r\n    // the scanner carries the partial `ESC[?…` to the next feed.\r\n    #[test]\r\n    fn mouse_mode_decset_scan_survives_split_across_chunks() {\r\n        use std::sync::atomic::Ordering::Acquire;\r\n        let mode = MouseMode::default();\r\n        let mut sc = MouseModeScanner::default();\r\n        // `ESC[?1006h` split right in the middle of the digits.\r\n        sc.feed(&mode, b\"tail\\x1b[?100\");\r\n        assert!(!mode.sgr.load(Acquire), \"incomplete half must not toggle yet\");\r\n        sc.feed(&mode, b\"6hmore\");\r\n        assert!(mode.sgr.load(Acquire), \"the completed sequence toggles sgr\");\r\n        // Split at the very ESC/[/? boundary too.\r\n        sc.feed(&mode, b\"x\\x1b\");\r\n        sc.feed(&mode, b\"[?1002h\");\r\n        assert!(mode.enabled.load(Acquire));\r\n    }\r\n\r\n    // ── REQ-RC-IDENTITY: the controller reserved-row status banner ──────────────\r\n\r\n    // [unit->REQ-RC-IDMARKER-DISABLE] bug #14: the top-right id marker is shipped\r\n    // DISABLED — the controller path yields NO reserved status row (and a viewer\r\n    // never had one). If someone flips STATUS_ROW_ENABLED back on, the controller\r\n    // assert here trips as a reminder to re-verify the sticky-overlay behavior.\r\n    #[test]\r\n    fn status_row_marker_is_disabled_by_flag() {\r\n        assert!(!status_row_active(false), \"controller: id marker is OFF (#14)\");\r\n        assert!(!status_row_active(true), \"a viewer never owns a status row\");\r\n    }\r\n\r\n    // [unit->REQ-RC-IDENTITY] the identity line format: `{subnet} : {id} @ {node}`,\r\n    // with the ` @ {node}` tail OMITTED for an absent / empty / whitespace node\r\n    // label (never render a lie) — and otherwise present verbatim.\r\n    #[test]\r\n    fn identity_line_format() {\r\n        assert_eq!(\r\n            identity_line(\"ops\", \"doyle\", Some(\"HFENDULEAM\")),\r\n            \"ops : doyle @ HFENDULEAM\"\r\n        );\r\n        // local (unset home subnet) still renders the subnet slot.\r\n        assert_eq!(identity_line(\"local\", \"perri\", Some(\"box\")), \"local : perri @ box\");\r\n        // No node → tail omitted.\r\n        assert_eq!(identity_line(\"local\", \"perri\", None), \"local : perri\");\r\n        // Empty / whitespace node → tail omitted (trimmed, never a bare ` @ `).\r\n        assert_eq!(identity_line(\"local\", \"perri\", Some(\"\")), \"local : perri\");\r\n        assert_eq!(identity_line(\"local\", \"perri\", Some(\"   \")), \"local : perri\");\r\n    }\r\n\r\n    // [unit->REQ-RC-IDENTITY] the DECSTBM margin assert reserves row 1 EXACTLY:\r\n    // `ESC[2;{rows}r` (top margin = line 2) followed by the repaint. Asserting the\r\n    // exact byte prefix is what makes a one-char drift (wrong margin, off-by-one\r\n    // bottom) fail.\r\n    #[test]\r\n    fn status_assert_reserves_row1_then_repaints() {\r\n        let s = StatusRow { text: \"x\".to_string(), rows: 24, cols: 80 };\r\n        let bytes = s.assert_bytes();\r\n        // The margin set comes first.\r\n        assert!(\r\n            bytes.starts_with(b\"\\x1b[2;24r\"),\r\n            \"assert must set the reserved-row scroll region first: {:?}\",\r\n            String::from_utf8_lossy(&bytes)\r\n        );\r\n        // The rest is exactly the repaint.\r\n        assert_eq!(&bytes[b\"\\x1b[2;24r\".len()..], &s.repaint_bytes()[..]);\r\n    }\r\n\r\n    // [unit->REQ-RC-IDENTITY] the repaint is the EXACT save→home→clear→right-align\r\n    // →cyan→text→reset→restore sequence, and the right-align column math:\r\n    // col = cols - width + 1 (1-based), clamped to col 1 when the text is ≥ cols.\r\n    #[test]\r\n    fn status_repaint_exact_bytes_and_right_align() {\r\n        // Short text, right-aligned: width 5 in 80 cols → start col 76.\r\n        let s = StatusRow { text: \"abcde\".to_string(), rows: 24, cols: 80 };\r\n        let mut expect = Vec::new();\r\n        expect.extend_from_slice(b\"\\x1b7\"); // DECSC\r\n        expect.extend_from_slice(b\"\\x1b[1;1H\"); // home\r\n        expect.extend_from_slice(b\"\\x1b[2K\"); // clear line\r\n        expect.extend_from_slice(b\"\\x1b[1;76H\"); // right-align start (80-5+1)\r\n        expect.extend_from_slice(b\"\\x1b[36m\"); // cyan\r\n        expect.extend_from_slice(b\"abcde\");\r\n        expect.extend_from_slice(b\"\\x1b[0m\"); // reset\r\n        expect.extend_from_slice(b\"\\x1b8\"); // DECRC\r\n        assert_eq!(s.repaint_bytes(), expect);\r\n\r\n        // Exact fit: width == cols → col 1.\r\n        let s = StatusRow { text: \"abcd\".to_string(), rows: 10, cols: 4 };\r\n        assert!(s.repaint_bytes().windows(6).any(|w| w == b\"\\x1b[1;1H\"));\r\n\r\n        // Text WIDER than cols → clamp to col 1 (never col 0 / negative).\r\n        let s = StatusRow { text: \"wide banner text\".to_string(), rows: 10, cols: 4 };\r\n        let bytes = s.repaint_bytes();\r\n        assert!(\r\n            bytes.windows(6).any(|w| w == b\"\\x1b[1;1H\"),\r\n            \"over-wide text clamps the right-align start to col 1: {:?}\",\r\n            String::from_utf8_lossy(&bytes)\r\n        );\r\n        // The text still rides verbatim.\r\n        assert!(\r\n            bytes.windows(b\"wide banner text\".len()).any(|w| w == b\"wide banner text\")\r\n        );\r\n    }\r\n\r\n    // [unit->REQ-RC-IDENTITY] restore tears the reserved row down: reset the scroll\r\n    // region to full screen (`ESC[r`) then clear the reclaimed row 1 — EXACT bytes.\r\n    #[test]\r\n    fn status_restore_exact_bytes() {\r\n        assert_eq!(StatusRow::restore_bytes(), b\"\\x1b[r\\x1b[1;1H\\x1b[2K\".to_vec());\r\n    }\r\n\r\n    // [unit->REQ-RC-IDENTITY] update_size re-points the tracked terminal size (the\r\n    // banner repaints on the NEW geometry after a window change).\r\n    #[test]\r\n    fn status_update_size_repoints_geometry() {\r\n        let mut s = StatusRow { text: \"id\".to_string(), rows: 24, cols: 80 };\r\n        s.update_size(40, 120);\r\n        assert_eq!((s.rows, s.cols), (40, 120));\r\n        // The repaint now right-aligns against 120 cols: 120 - 2 + 1 = 119.\r\n        assert!(s.repaint_bytes().windows(8).any(|w| w == b\"\\x1b[1;119H\"));\r\n        // And the assert reserves 40 rows.\r\n        assert!(s.assert_bytes().starts_with(b\"\\x1b[2;40r\"));\r\n    }\r\n\r\n    // [unit->REQ-RC-IDENTITY] the re-assert scanner fires on the region-destroying\r\n    // triggers and ONLY those: alt-screen enter (`?1049h`, legacy `?47h`/`?1047h`)\r\n    // and a bare DECSTBM reset (`ESC[r`/`ESC[0r`/`ESC[;r`, no leading `?`); it\r\n    // ignores an alt-screen EXIT (`?1049l`), an unrelated private mode, a real\r\n    // DECSTBM SET (`ESC[2;24r`, has non-zero params), and other CSIs.\r\n    #[test]\r\n    fn reassert_scanner_triggers_only_on_region_destroyers() {\r\n        // Alt-screen enter (modern + both legacy spellings) → fire.\r\n        assert!(ReassertScanner::default().feed(b\"out\\x1b[?1049hmore\"));\r\n        assert!(ReassertScanner::default().feed(b\"\\x1b[?47h\"));\r\n        assert!(ReassertScanner::default().feed(b\"\\x1b[?1047h\"));\r\n        // Bare DECSTBM reset, all three no/zero-param spellings → fire.\r\n        assert!(ReassertScanner::default().feed(b\"\\x1b[r\"));\r\n        assert!(ReassertScanner::default().feed(b\"\\x1b[0r\"));\r\n        assert!(ReassertScanner::default().feed(b\"\\x1b[;r\"));\r\n        // Alt-screen EXIT (reset) is NOT a destroyer (we re-assert on RE-ENTRY).\r\n        assert!(!ReassertScanner::default().feed(b\"\\x1b[?1049l\"));\r\n        // An unrelated private mode (cursor visibility) → no fire.\r\n        assert!(!ReassertScanner::default().feed(b\"\\x1b[?25h\"));\r\n        // A real DECSTBM SET (our own / the harness's margin) has non-zero params →\r\n        // not a reset, no fire.\r\n        assert!(!ReassertScanner::default().feed(b\"\\x1b[2;24r\"));\r\n        // Other CSIs (cursor move, SGR) → no fire.\r\n        assert!(!ReassertScanner::default().feed(b\"\\x1b[10;5H\\x1b[0m\"));\r\n        // Plain text → no fire (fast path).\r\n        assert!(!ReassertScanner::default().feed(b\"hello world\"));\r\n    }\r\n\r\n    // [unit->REQ-RC-IDENTITY] a trigger SPLIT across two output chunks is still\r\n    // detected — the scanner carries the partial `ESC[…` to the next feed (mirrors\r\n    // the mouse scanner's carry discipline).\r\n    #[test]\r\n    fn reassert_scanner_survives_split_across_chunks() {\r\n        // `ESC[?1049h` split mid-digits.\r\n        let mut sc = ReassertScanner::default();\r\n        assert!(!sc.feed(b\"tail\\x1b[?10\"), \"incomplete half must not fire yet\");\r\n        assert!(sc.feed(b\"49hmore\"), \"the completed alt-screen enter fires\");\r\n        // `ESC[r` split at the ESC/[ boundary.\r\n        let mut sc = ReassertScanner::default();\r\n        assert!(!sc.feed(b\"x\\x1b\"));\r\n        assert!(sc.feed(b\"[r\"));\r\n    }\r\n\r\n    // [int->REQ-RC-IDENTITY] the WIRED controller status sequence the pump drives,\r\n    // exercised end-to-end over a real `impl Write` sink (a `Vec<u8>`), using the\r\n    // exact operations the pump performs in order: (a) on attach START it writes\r\n    // `assert_bytes()` (reserve row 1 + paint); (b) when harness output trips the\r\n    // re-assert scanner (an alt-screen enter) it writes `assert_bytes()` (margin +\r\n    // paint — the region was destroyed); (c) on exit it writes `restore_bytes()`.\r\n    // Asserting the captured byte stream proves\r\n    // the reserved-row contract holds through the pump's status hooks WITHOUT a live\r\n    // broker (rc's pump needs a Brain + PTY peer that an in-process unit can't\r\n    // stand up; this drives the same status state-machine the pump invokes, against\r\n    // the Write seam the pump was genericized to accept).\r\n    #[test]\r\n    fn pump_status_emit_sequence_over_write_sink() {\r\n        use std::io::Write as _;\r\n        let status = StatusRow {\r\n            text: identity_line(\"local\", \"doyle\", Some(\"BOX\")),\r\n            rows: 24,\r\n            cols: 80,\r\n        };\r\n        let mut scanner = ReassertScanner::default();\r\n        let mut sink: Vec<u8> = Vec::new();\r\n\r\n        // (a) pump START: reserve + paint.\r\n        sink.write_all(&status.assert_bytes()).unwrap();\r\n        let assert_len = sink.len();\r\n        assert!(sink.starts_with(b\"\\x1b[2;24r\"), \"start reserves the row\");\r\n        assert!(\r\n            sink.windows(b\"local : doyle @ BOX\".len()).any(|w| w == b\"local : doyle @ BOX\"),\r\n            \"the identity banner is painted at start\"\r\n        );\r\n\r\n        // (b) harness output WITHOUT a trigger → no re-assert emitted.\r\n        let plain = b\"hello from the harness\\r\\n\";\r\n        if scanner.feed(plain) {\r\n            sink.write_all(&status.assert_bytes()).unwrap();\r\n        }\r\n        assert_eq!(sink.len(), assert_len, \"plain output emits no status re-assert\");\r\n\r\n        // (b') harness output WITH an alt-screen enter → re-assert (MARGIN + paint).\r\n        // The trigger destroyed the scroll region, so the pump re-sets the margin,\r\n        // not just the text — assert the margin sequence reappears.\r\n        let alt = b\"\\x1b[?1049hfull-screen app\";\r\n        let mut fired = false;\r\n        let before = sink.len();\r\n        if scanner.feed(alt) {\r\n            fired = true;\r\n            sink.write_all(&status.assert_bytes()).unwrap();\r\n        }\r\n        assert!(fired, \"the alt-screen enter trips the re-assert scanner\");\r\n        assert!(sink.len() > before, \"a re-assert was appended after the trigger\");\r\n        assert!(\r\n            sink[before..].starts_with(b\"\\x1b[2;24r\"),\r\n            \"the re-assert re-sets the DECSTBM margin (not just a repaint)\"\r\n        );\r\n\r\n        // (c) pump EXIT: restore (reset region + clear row 1).\r\n        sink.write_all(&StatusRow::restore_bytes()).unwrap();\r\n        assert!(\r\n            sink.ends_with(b\"\\x1b[r\\x1b[1;1H\\x1b[2K\"),\r\n            \"exit restores the full scroll region and clears the reserved row\"\r\n        );\r\n    }\r\n}\r\n","startLine":2002,"lineNumbers":[2002,null,2683,null,2699,2700,2701,2702,2703,2704,2705,2706,2707,2708,2709,2710,2711,2712,2713,2714,2715,2716,2717,2718,2719,2720,2721,2722,2723,2724,2725,2726,2727,2728,2729,2730,2731,2732,2733,2734,2735,2736,2737,2738,2739,2740,2741,2742,2743,2744,2745,2746,2747,2748,2749,2750,2751,2752,2753,2754,2755,2756,2757,2758,2759,2760,2761,2762,2763,2764,2765,2766,2767,2768,2769,2770,2771,2772,2773,2774,2775,2776,2777,2778,2779,2780,2781,2782,2783,2784,2785,2786,2787,2788,2789,2790,2791,2792,2793,2794,2795,2796,2797,2798,2799,2800,2801,2802,2803,2804,2805,2806,2807,2808,2809,2810,2811,2812,2813,2814,2815,2816,2817,2818,2819,2820,2821,2822,2823,2824,2825,2826,2827,2828,2829,2830,2831,2832,2833,2834,2835,2836,2837,2838,2839,2840,2841,2842,2843,2844,2845,2846,2847,2848,2849,2850,2851,2852,2853,2854,2855,2856,2857,2858,2859,2860,2861,2862,2863,2864,2865,2866,2867,2868,2869,2870,2871,2872,2873,2874,2875,2876,2877,2878,2879,2880,2881,2882,2883,2884,2885,2886,2887,2888,2889,2890,2891,2892,2893,2894,2895,2896,2897,2898,2899,2900,2901,2902,2903,2904,2905,2906,2907,2908,2909,2910,2911,2912,2913,2914,2915,2916,2917,2918,2919,2920,2921,2922,2923,2924,2925,2926,2927,2928,2929,2930,2931,2932,2933,2934,2935,2936,2937,2938,2939,2940,2941,2942,2943,2944,2945,2946,2947,2948,2949,2950,2951,2952,2953,2954,2955,2956,2957,2958,2959,2960,2961,2962,2963,2964,2965,2966,2967,2968,2969,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3113,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156,3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,3170,3171,3172,3173,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3184,3185,3186,3187,3188,3189,3190,3191,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212,3213,3214]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"}}},"isError":false,"timestamp":1784195789204}}
{"type":"message","id":"941b2a05","parentId":"da375c0b","timestamp":"2026-07-16T09:56:29.269Z","message":{"role":"toolResult","toolCallId":"call_wnh9DQyJ7v3rm9pmBRXHgFgG|fc_008390f508d1d09b016a58aacd12ac819aaf3de57a5d08f61b","toolName":"read","content":[{"type":"text","text":"fn viewer_writer(\n…\n) {\n…\n    // `tx` dropped. If this was an EVICTION for overflow (append set the flag\n    // before dropping the sink), emit a KIND_VIEWER_EVICTED marker — DISTINCT from\n    // a session-exit EOF — so the serving brain skips to live (re-subscribes from\n    // the ring floor) instead of dying silently. A NORMAL teardown (session end /\n    // conn drop) leaves the flag false → no marker, exit quietly. This write is in\n    // the dedicated writer thread, OFF the log lock / off the drain (the W1/b4\n    // isolation is preserved). REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT.\n    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n    if evicted.load(Ordering::Acquire) {\n        let marker = evicted_envelope(session_id);\n        let _ = send.write(&marker);\n    }\n}\n\n/// Round-4 observability bridge: a controller writer whose bounded conn write\n/// was DEADLINE-retired (`TimedOut` — the conn watchdog aborted a wedged\n/// write, or a gate-wait behind one exhausted the bound) IS the\n/// brain-subscriber stall-evict fact the W2 tally counts. Pre-round-4 that\n/// wedge aged until a logical sampler (`stall_evict_controller`) observed it;\n/// the physical bound now fires FIRST, so the tally is recorded here — same\n/// event class, same `daemon status` surface. An ORGANIC write failure (peer\n/// death, any non-TimedOut error) stays uncounted, exactly as before.\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nfn note_controller_write_retired(sid: u64, conn: u64, e: &io::Error) {\n    if e.kind() != io::ErrorKind::TimedOut {\n        return;\n    }\n    // conn id + stamps (F-039 leg b). [impl->REQ-CONN-POISON-ATTRIBUTION]\n    eprintln!(\n        \"BRAIN_SUBSCRIBER_STALL_EVICT:{sid} conn={conn} {} — controller conn write \\\n         exceeded its bound; physical conn poisoned + retired (writer released, EOF \\\n         cleanup follows) [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\n        crate::conn::log_stamp()\n    );\n    record_stall_evict();\n}\n\n/// A message to the CONTROLLER writer thread (W1). `Output` carries a sequenced\n/// chunk and advances the authoritative `delivered_through` on a successful\n/// write; `Control` is an out-of-band notice (Displaced, …) that is written but\n/// does NOT move the resume cursor.\nenum CtrlMsg {\n    Output(u64, Envelope),\n    Control(Envelope),\n}\n\n/// Advance the shared `delivered_through` cursor to `seq + 1`, monotonically\n/// (D4-1, via [`advanced_cursor`]) and CAS-safe against a brief two-writer\n/// overlap during a controller displace.\nfn advance_delivered(dt: &AtomicU64, seq: u64) {\n    let mut cur = dt.load(Ordering::Acquire);\n    loop {\n        let next = advanced_cursor(cur, seq, true);\n        if next == cur {\n            return;\n        }\n        match dt.compare_exchange_weak(cur, next, Ordering::AcqRel, Ordering::Acquire) {\n            Ok(_) => return,\n            Err(actual) => cur = actual,\n        }\n    }\n}\n\n/// Advance the controller resume cursor CONTIGUOUSLY (b4 / the (a) B2-gate): move to\n/// `seq + 1` ONLY when the cursor is EXACTLY `seq` (the next expected seq). A gap\n/// (`seq > cursor` — a frame the drain dropped while the controller channel was Full,\n/// the b4 backpressure) FREEZES the cursor at last-contiguous, so a re-attaching\n/// brain's `resume_seq` never skips a dropped chunk (a high-watermark jump PAST the\n/// gap would be a not-exactly-once / non-gapless resume = a B2 violation). An\n/// already-delivered `seq` (`seq < cursor`, a rewind re-send) is a no-op. A single CAS,\n/// no loop: it advances by exactly one, or not at all. In the NO-DROP path (every\n/// live frame is contiguous) this is identical to [`advance_delivered`].\n// [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\nfn contiguous_advance(dt: &AtomicU64, seq: u64) {\n    let _ = dt.compare_exchange(seq, seq + 1, Ordering::AcqRel, Ordering::Acquire);\n}\n\n/// The interactive CONTROLLER's dedicated writer thread (W1 /\n/// REQ-HAZARD-INJECT-CONTROL-COEXIST) — the authoritative analog of\n/// [`viewer_writer`]. Write the `initial` ring-replay batch (advancing the resume\n/// cursor on each success), then drain live [`CtrlMsg`]s with a BLOCKING socket\n/// write each — so a backed-up controller blocks ONLY this thread, never the\n/// session drain (which used to write the controller inline under the log lock).\n/// A failed write (dead operator) ends the thread; the drain's next bounded\n/// send then sees `Disconnected` and detaches the row. `Output` advances\n/// `delivered_through`; `Control` (Displaced/…) does not.\n// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\nfn controller_writer(\n    sid: u64,\n    send: SharedSend,\n    initial: Vec<(u64, Vec<u8>)>,\n    rx: Receiver<CtrlMsg>,\n    delivered_through: Arc<AtomicU64>,\n    epoch: u64,\n    controller_epoch: Arc<AtomicU64>,\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\n) {\n    // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): mark the exact window a\n    // BLOCKING socket write is in flight, so a reader can tell a WEDGED writer (brain\n    // conn suspended) from a merely idle one and stall-evict it. Set BEFORE the write,\n    // cleared AFTER — the mutex is never held across `write_frame` (that would just\n    // move the wedge onto the mutex).\n    let mark_blocked = |on: bool| {\n        if let Ok(mut g) = write_blocked_since.lock() {\n            *g = if on { Some(Instant::now()) } else { None };\n        }\n    };\n    for (seq, bytes) in &initial {\n        let frame = output_envelope(sid, *seq, bytes);\n        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\n        // epoch past ours) must write NOTHING further — otherwise its owned\n        // initial batch races the new writer on the same socket and reorders the\n        // seq stream. The gate closure runs AFTER the conn's serialized write\n        // gate is acquired and BEFORE any byte is written — the same\n        // serialization point the old epoch-check-under-`send.lock()` had: both\n        // writers serialize on the one conn gate, and `become_controller` bumps\n        // the epoch BEFORE spawning the new writer, so whichever superseded\n        // writer next holds the gate observes the bump and bails before\n        // committing — no check-then-block-then-write window. (A frame the\n        // stale writer committed BEFORE the bump is irreducible at the broker;\n        // fix #1 removes the contradictory double-take that produced it.)\n        // Round-4: the write itself is bounded + cancelable — a non-draining\n        // consumer poisons + retires the physical conn at the deadline instead\n        // of wedging this thread in the OS write forever.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        mark_blocked(true);\n        let outcome =\n            send.write_gated(&frame, || controller_epoch.load(Ordering::Acquire) == epoch);\n        mark_blocked(false);\n        match outcome {\n            Ok(ConnWrite::Done) => {}\n            Ok(ConnWrite::Superseded) => {\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=superseded\"),\n                );\n                return;\n            }\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n                );\n                return;\n            }\n        }\n        advance_delivered(&delivered_through, *seq);\n    }\n    while let Ok(msg) = rx.recv() {\n        let (frame, seq) = match &msg {\n            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),\n            CtrlMsg::Control(frame) => (frame, None),\n        };\n        // NO epoch gate on the live path (P1c): new output only ever flows to the\n        // CURRENT controller's channel (the drain clones `self.controller.tx`), so\n        // a superseded writer's channel holds only its pre-supersede output\n        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL\n        // control frames — notably the `Displaced` kick the displaced controller\n        // MUST still receive. The loop ends naturally when `tx` drops. Gating\n        // output here would suppress that `Displaced` (the cv-matrix loud-take\n        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.\n        mark_blocked(true);\n        let res = send.write(frame);\n        mark_blocked(false);\n        match res {\n            Ok(_) => {}\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n                );\n                return;\n            }\n        }\n        if let Some(seq) = seq {\n            // (a) CONTIGUOUS cursor (b4 / B2 gapless-resume — doyle's gate): with the\n            // drop-on-full handoff in `append`, the controller's live channel can SKIP\n            // a seq (dropped while Full). The frame still went to the socket (the live\n            // operator sees it), but the durable resume cursor must NOT jump past the\n            // gap — see [`contiguous_advance`].\n            contiguous_advance(&delivered_through, seq);\n        }\n    }\n…\n}\n\n[Showing lines 1419-1608 of 6009. Use :1609 to continue]"}],"details":{"truncation":{"content":"    // `tx` dropped. If this was an EVICTION for overflow (append set the flag\r\n    // before dropping the sink), emit a KIND_VIEWER_EVICTED marker — DISTINCT from\r\n    // a session-exit EOF — so the serving brain skips to live (re-subscribes from\r\n    // the ring floor) instead of dying silently. A NORMAL teardown (session end /\r\n    // conn drop) leaves the flag false → no marker, exit quietly. This write is in\r\n    // the dedicated writer thread, OFF the log lock / off the drain (the W1/b4\r\n    // isolation is preserved). REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT.\r\n    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\r\n    if evicted.load(Ordering::Acquire) {\r\n        let marker = evicted_envelope(session_id);\r\n        let _ = send.write(&marker);\r\n    }\r\n}\r\n\r\n/// Round-4 observability bridge: a controller writer whose bounded conn write\r\n/// was DEADLINE-retired (`TimedOut` — the conn watchdog aborted a wedged\r\n/// write, or a gate-wait behind one exhausted the bound) IS the\r\n/// brain-subscriber stall-evict fact the W2 tally counts. Pre-round-4 that\r\n/// wedge aged until a logical sampler (`stall_evict_controller`) observed it;\r\n/// the physical bound now fires FIRST, so the tally is recorded here — same\r\n/// event class, same `daemon status` surface. An ORGANIC write failure (peer\r\n/// death, any non-TimedOut error) stays uncounted, exactly as before.\r\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\r\nfn note_controller_write_retired(sid: u64, conn: u64, e: &io::Error) {\r\n    if e.kind() != io::ErrorKind::TimedOut {\r\n        return;\r\n    }\r\n    // conn id + stamps (F-039 leg b). [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    eprintln!(\r\n        \"BRAIN_SUBSCRIBER_STALL_EVICT:{sid} conn={conn} {} — controller conn write \\\r\n         exceeded its bound; physical conn poisoned + retired (writer released, EOF \\\r\n         cleanup follows) [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\r\n        crate::conn::log_stamp()\r\n    );\r\n    record_stall_evict();\r\n}\r\n\r\n/// A message to the CONTROLLER writer thread (W1). `Output` carries a sequenced\r\n/// chunk and advances the authoritative `delivered_through` on a successful\r\n/// write; `Control` is an out-of-band notice (Displaced, …) that is written but\r\n/// does NOT move the resume cursor.\r\nenum CtrlMsg {\r\n    Output(u64, Envelope),\r\n    Control(Envelope),\r\n}\r\n\r\n/// Advance the shared `delivered_through` cursor to `seq + 1`, monotonically\r\n/// (D4-1, via [`advanced_cursor`]) and CAS-safe against a brief two-writer\r\n/// overlap during a controller displace.\r\nfn advance_delivered(dt: &AtomicU64, seq: u64) {\r\n    let mut cur = dt.load(Ordering::Acquire);\r\n    loop {\r\n        let next = advanced_cursor(cur, seq, true);\r\n        if next == cur {\r\n            return;\r\n        }\r\n        match dt.compare_exchange_weak(cur, next, Ordering::AcqRel, Ordering::Acquire) {\r\n            Ok(_) => return,\r\n            Err(actual) => cur = actual,\r\n        }\r\n    }\r\n}\r\n\r\n/// Advance the controller resume cursor CONTIGUOUSLY (b4 / the (a) B2-gate): move to\r\n/// `seq + 1` ONLY when the cursor is EXACTLY `seq` (the next expected seq). A gap\r\n/// (`seq > cursor` — a frame the drain dropped while the controller channel was Full,\r\n/// the b4 backpressure) FREEZES the cursor at last-contiguous, so a re-attaching\r\n/// brain's `resume_seq` never skips a dropped chunk (a high-watermark jump PAST the\r\n/// gap would be a not-exactly-once / non-gapless resume = a B2 violation). An\r\n/// already-delivered `seq` (`seq < cursor`, a rewind re-send) is a no-op. A single CAS,\r\n/// no loop: it advances by exactly one, or not at all. In the NO-DROP path (every\r\n/// live frame is contiguous) this is identical to [`advance_delivered`].\r\n// [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\r\nfn contiguous_advance(dt: &AtomicU64, seq: u64) {\r\n    let _ = dt.compare_exchange(seq, seq + 1, Ordering::AcqRel, Ordering::Acquire);\r\n}\r\n\r\n/// The interactive CONTROLLER's dedicated writer thread (W1 /\r\n/// REQ-HAZARD-INJECT-CONTROL-COEXIST) — the authoritative analog of\r\n/// [`viewer_writer`]. Write the `initial` ring-replay batch (advancing the resume\r\n/// cursor on each success), then drain live [`CtrlMsg`]s with a BLOCKING socket\r\n/// write each — so a backed-up controller blocks ONLY this thread, never the\r\n/// session drain (which used to write the controller inline under the log lock).\r\n/// A failed write (dead operator) ends the thread; the drain's next bounded\r\n/// send then sees `Disconnected` and detaches the row. `Output` advances\r\n/// `delivered_through`; `Control` (Displaced/…) does not.\r\n// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\r\n// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\r\n#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\r\nfn controller_writer(\r\n    sid: u64,\r\n    send: SharedSend,\r\n    initial: Vec<(u64, Vec<u8>)>,\r\n    rx: Receiver<CtrlMsg>,\r\n    delivered_through: Arc<AtomicU64>,\r\n    epoch: u64,\r\n    controller_epoch: Arc<AtomicU64>,\r\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\r\n) {\r\n    // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): mark the exact window a\r\n    // BLOCKING socket write is in flight, so a reader can tell a WEDGED writer (brain\r\n    // conn suspended) from a merely idle one and stall-evict it. Set BEFORE the write,\r\n    // cleared AFTER — the mutex is never held across `write_frame` (that would just\r\n    // move the wedge onto the mutex).\r\n    let mark_blocked = |on: bool| {\r\n        if let Ok(mut g) = write_blocked_since.lock() {\r\n            *g = if on { Some(Instant::now()) } else { None };\r\n        }\r\n    };\r\n    for (seq, bytes) in &initial {\r\n        let frame = output_envelope(sid, *seq, bytes);\r\n        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\r\n        // epoch past ours) must write NOTHING further — otherwise its owned\r\n        // initial batch races the new writer on the same socket and reorders the\r\n        // seq stream. The gate closure runs AFTER the conn's serialized write\r\n        // gate is acquired and BEFORE any byte is written — the same\r\n        // serialization point the old epoch-check-under-`send.lock()` had: both\r\n        // writers serialize on the one conn gate, and `become_controller` bumps\r\n        // the epoch BEFORE spawning the new writer, so whichever superseded\r\n        // writer next holds the gate observes the bump and bails before\r\n        // committing — no check-then-block-then-write window. (A frame the\r\n        // stale writer committed BEFORE the bump is irreducible at the broker;\r\n        // fix #1 removes the contradictory double-take that produced it.)\r\n        // Round-4: the write itself is bounded + cancelable — a non-draining\r\n        // consumer poisons + retires the physical conn at the deadline instead\r\n        // of wedging this thread in the OS write forever.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        mark_blocked(true);\r\n        let outcome =\r\n            send.write_gated(&frame, || controller_epoch.load(Ordering::Acquire) == epoch);\r\n        mark_blocked(false);\r\n        match outcome {\r\n            Ok(ConnWrite::Done) => {}\r\n            Ok(ConnWrite::Superseded) => {\r\n                // Bounded writer-exit record (F-039 leg d).\r\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n                send.lifecycle_event(\r\n                    \"writer-exit\",\r\n                    &format!(\"role=controller session={sid} reason=superseded\"),\r\n                );\r\n                return;\r\n            }\r\n            Err(e) => {\r\n                note_controller_write_retired(sid, send.id(), &e);\r\n                send.lifecycle_event(\r\n                    \"writer-exit\",\r\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\r\n                );\r\n                return;\r\n            }\r\n        }\r\n        advance_delivered(&delivered_through, *seq);\r\n    }\r\n    while let Ok(msg) = rx.recv() {\r\n        let (frame, seq) = match &msg {\r\n            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),\r\n            CtrlMsg::Control(frame) => (frame, None),\r\n        };\r\n        // NO epoch gate on the live path (P1c): new output only ever flows to the\r\n        // CURRENT controller's channel (the drain clones `self.controller.tx`), so\r\n        // a superseded writer's channel holds only its pre-supersede output\r\n        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL\r\n        // control frames — notably the `Displaced` kick the displaced controller\r\n        // MUST still receive. The loop ends naturally when `tx` drops. Gating\r\n        // output here would suppress that `Displaced` (the cv-matrix loud-take\r\n        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.\r\n        mark_blocked(true);\r\n        let res = send.write(frame);\r\n        mark_blocked(false);\r\n        match res {\r\n            Ok(_) => {}\r\n            Err(e) => {\r\n                note_controller_write_retired(sid, send.id(), &e);\r\n                // Bounded writer-exit record (F-039 leg d).\r\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n                send.lifecycle_event(\r\n                    \"writer-exit\",\r\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\r\n                );\r\n                return;\r\n            }\r\n        }\r\n        if let Some(seq) = seq {\r\n            // (a) CONTIGUOUS cursor (b4 / B2 gapless-resume — doyle's gate): with the\r\n            // drop-on-full handoff in `append`, the controller's live channel can SKIP\r\n            // a seq (dropped while Full). The frame still went to the socket (the live\r\n            // operator sees it), but the durable resume cursor must NOT jump past the\r\n            // gap — see [`contiguous_advance`].\r\n            contiguous_advance(&delivered_through, seq);\r","truncated":true,"truncatedBy":"lines","totalLines":4591,"totalBytes":10069,"outputLines":190,"outputBytes":10069,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn viewer_writer(\n…\n) {\n…\n    // `tx` dropped. If this was an EVICTION for overflow (append set the flag\r\n    // before dropping the sink), emit a KIND_VIEWER_EVICTED marker — DISTINCT from\r\n    // a session-exit EOF — so the serving brain skips to live (re-subscribes from\r\n    // the ring floor) instead of dying silently. A NORMAL teardown (session end /\r\n    // conn drop) leaves the flag false → no marker, exit quietly. This write is in\r\n    // the dedicated writer thread, OFF the log lock / off the drain (the W1/b4\r\n    // isolation is preserved). REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT.\r\n    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\r\n    if evicted.load(Ordering::Acquire) {\r\n        let marker = evicted_envelope(session_id);\r\n        let _ = send.write(&marker);\r\n    }\r\n}\r\n\r\n/// Round-4 observability bridge: a controller writer whose bounded conn write\r\n/// was DEADLINE-retired (`TimedOut` — the conn watchdog aborted a wedged\r\n/// write, or a gate-wait behind one exhausted the bound) IS the\r\n/// brain-subscriber stall-evict fact the W2 tally counts. Pre-round-4 that\r\n/// wedge aged until a logical sampler (`stall_evict_controller`) observed it;\r\n/// the physical bound now fires FIRST, so the tally is recorded here — same\r\n/// event class, same `daemon status` surface. An ORGANIC write failure (peer\r\n/// death, any non-TimedOut error) stays uncounted, exactly as before.\r\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\r\nfn note_controller_write_retired(sid: u64, conn: u64, e: &io::Error) {\r\n    if e.kind() != io::ErrorKind::TimedOut {\r\n        return;\r\n    }\r\n    // conn id + stamps (F-039 leg b). [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    eprintln!(\r\n        \"BRAIN_SUBSCRIBER_STALL_EVICT:{sid} conn={conn} {} — controller conn write \\\r\n         exceeded its bound; physical conn poisoned + retired (writer released, EOF \\\r\n         cleanup follows) [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\r\n        crate::conn::log_stamp()\r\n    );\r\n    record_stall_evict();\r\n}\r\n\r\n/// A message to the CONTROLLER writer thread (W1). `Output` carries a sequenced\r\n/// chunk and advances the authoritative `delivered_through` on a successful\r\n/// write; `Control` is an out-of-band notice (Displaced, …) that is written but\r\n/// does NOT move the resume cursor.\r\nenum CtrlMsg {\r\n    Output(u64, Envelope),\r\n    Control(Envelope),\r\n}\r\n\r\n/// Advance the shared `delivered_through` cursor to `seq + 1`, monotonically\r\n/// (D4-1, via [`advanced_cursor`]) and CAS-safe against a brief two-writer\r\n/// overlap during a controller displace.\r\nfn advance_delivered(dt: &AtomicU64, seq: u64) {\r\n    let mut cur = dt.load(Ordering::Acquire);\r\n    loop {\r\n        let next = advanced_cursor(cur, seq, true);\r\n        if next == cur {\r\n            return;\r\n        }\r\n        match dt.compare_exchange_weak(cur, next, Ordering::AcqRel, Ordering::Acquire) {\r\n            Ok(_) => return,\r\n            Err(actual) => cur = actual,\r\n        }\r\n    }\r\n}\r\n\r\n/// Advance the controller resume cursor CONTIGUOUSLY (b4 / the (a) B2-gate): move to\r\n/// `seq + 1` ONLY when the cursor is EXACTLY `seq` (the next expected seq). A gap\r\n/// (`seq > cursor` — a frame the drain dropped while the controller channel was Full,\r\n/// the b4 backpressure) FREEZES the cursor at last-contiguous, so a re-attaching\r\n/// brain's `resume_seq` never skips a dropped chunk (a high-watermark jump PAST the\r\n/// gap would be a not-exactly-once / non-gapless resume = a B2 violation). An\r\n/// already-delivered `seq` (`seq < cursor`, a rewind re-send) is a no-op. A single CAS,\r\n/// no loop: it advances by exactly one, or not at all. In the NO-DROP path (every\r\n/// live frame is contiguous) this is identical to [`advance_delivered`].\r\n// [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\r\nfn contiguous_advance(dt: &AtomicU64, seq: u64) {\r\n    let _ = dt.compare_exchange(seq, seq + 1, Ordering::AcqRel, Ordering::Acquire);\r\n}\r\n\r\n/// The interactive CONTROLLER's dedicated writer thread (W1 /\r\n/// REQ-HAZARD-INJECT-CONTROL-COEXIST) — the authoritative analog of\r\n/// [`viewer_writer`]. Write the `initial` ring-replay batch (advancing the resume\r\n/// cursor on each success), then drain live [`CtrlMsg`]s with a BLOCKING socket\r\n/// write each — so a backed-up controller blocks ONLY this thread, never the\r\n/// session drain (which used to write the controller inline under the log lock).\r\n/// A failed write (dead operator) ends the thread; the drain's next bounded\r\n/// send then sees `Disconnected` and detaches the row. `Output` advances\r\n/// `delivered_through`; `Control` (Displaced/…) does not.\r\n// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\r\n// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\r\n#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\r\nfn controller_writer(\r\n    sid: u64,\r\n    send: SharedSend,\r\n    initial: Vec<(u64, Vec<u8>)>,\r\n    rx: Receiver<CtrlMsg>,\r\n    delivered_through: Arc<AtomicU64>,\r\n    epoch: u64,\r\n    controller_epoch: Arc<AtomicU64>,\r\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\r\n) {\r\n    // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): mark the exact window a\r\n    // BLOCKING socket write is in flight, so a reader can tell a WEDGED writer (brain\r\n    // conn suspended) from a merely idle one and stall-evict it. Set BEFORE the write,\r\n    // cleared AFTER — the mutex is never held across `write_frame` (that would just\r\n    // move the wedge onto the mutex).\r\n    let mark_blocked = |on: bool| {\r\n        if let Ok(mut g) = write_blocked_since.lock() {\r\n            *g = if on { Some(Instant::now()) } else { None };\r\n        }\r\n    };\r\n    for (seq, bytes) in &initial {\r\n        let frame = output_envelope(sid, *seq, bytes);\r\n        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\r\n        // epoch past ours) must write NOTHING further — otherwise its owned\r\n        // initial batch races the new writer on the same socket and reorders the\r\n        // seq stream. The gate closure runs AFTER the conn's serialized write\r\n        // gate is acquired and BEFORE any byte is written — the same\r\n        // serialization point the old epoch-check-under-`send.lock()` had: both\r\n        // writers serialize on the one conn gate, and `become_controller` bumps\r\n        // the epoch BEFORE spawning the new writer, so whichever superseded\r\n        // writer next holds the gate observes the bump and bails before\r\n        // committing — no check-then-block-then-write window. (A frame the\r\n        // stale writer committed BEFORE the bump is irreducible at the broker;\r\n        // fix #1 removes the contradictory double-take that produced it.)\r\n        // Round-4: the write itself is bounded + cancelable — a non-draining\r\n        // consumer poisons + retires the physical conn at the deadline instead\r\n        // of wedging this thread in the OS write forever.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        mark_blocked(true);\r\n        let outcome =\r\n            send.write_gated(&frame, || controller_epoch.load(Ordering::Acquire) == epoch);\r\n        mark_blocked(false);\r\n        match outcome {\r\n            Ok(ConnWrite::Done) => {}\r\n            Ok(ConnWrite::Superseded) => {\r\n                // Bounded writer-exit record (F-039 leg d).\r\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n                send.lifecycle_event(\r\n                    \"writer-exit\",\r\n                    &format!(\"role=controller session={sid} reason=superseded\"),\r\n                );\r\n                return;\r\n            }\r\n            Err(e) => {\r\n                note_controller_write_retired(sid, send.id(), &e);\r\n                send.lifecycle_event(\r\n                    \"writer-exit\",\r\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\r\n                );\r\n                return;\r\n            }\r\n        }\r\n        advance_delivered(&delivered_through, *seq);\r\n    }\r\n    while let Ok(msg) = rx.recv() {\r\n        let (frame, seq) = match &msg {\r\n            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),\r\n            CtrlMsg::Control(frame) => (frame, None),\r\n        };\r\n        // NO epoch gate on the live path (P1c): new output only ever flows to the\r\n        // CURRENT controller's channel (the drain clones `self.controller.tx`), so\r\n        // a superseded writer's channel holds only its pre-supersede output\r\n        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL\r\n        // control frames — notably the `Displaced` kick the displaced controller\r\n        // MUST still receive. The loop ends naturally when `tx` drops. Gating\r\n        // output here would suppress that `Displaced` (the cv-matrix loud-take\r\n        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.\r\n        mark_blocked(true);\r\n        let res = send.write(frame);\r\n        mark_blocked(false);\r\n        match res {\r\n            Ok(_) => {}\r\n            Err(e) => {\r\n                note_controller_write_retired(sid, send.id(), &e);\r\n                // Bounded writer-exit record (F-039 leg d).\r\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n                send.lifecycle_event(\r\n                    \"writer-exit\",\r\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\r\n                );\r\n                return;\r\n            }\r\n        }\r\n        if let Some(seq) = seq {\r\n            // (a) CONTIGUOUS cursor (b4 / B2 gapless-resume — doyle's gate): with the\r\n            // drop-on-full handoff in `append`, the controller's live channel can SKIP\r\n            // a seq (dropped while Full). The frame still went to the socket (the live\r\n            // operator sees it), but the durable resume cursor must NOT jump past the\r\n            // gap — see [`contiguous_advance`].\r\n            contiguous_advance(&delivered_through, seq);\r\n        }\n    }\n…\n}","startLine":1376,"lineNumbers":[1376,null,1382,null,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,null,1617]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6009,"totalBytes":10069,"outputLines":190,"outputBytes":10069,"shownRange":{"start":1419,"end":1608},"nextOffset":1609}}},"isError":false,"timestamp":1784195789269}}
{"type":"message","id":"659d43fc","parentId":"941b2a05","timestamp":"2026-07-16T09:56:29.269Z","message":{"role":"toolResult","toolCallId":"call_ZAu1tShC9pWj3aGICZPob6ee|fc_008390f508d1d09b016a58aacd12b4819abd2dce6db56b7706","toolName":"read","content":[{"type":"text","text":"\n    /// The output-log high-water (`next_seq`) of a hosted session, or `None` if\n    /// absent. It advances every time the child emits output — the\n    /// **functional**-survival seam for the D7-1 process-level E2E (doyle F1):\n    /// after a brain-PROCESS swap, writing input to the broker-held PTY child and\n    /// watching THIS advance proves the path is alive end-to-end (input → child →\n    /// echo → broker ring), not merely that the handle is held. A read-only direct\n    /// peek, like [`session_pid`](Self::session_pid).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\n        self.sessions\n            .lock()\n            .unwrap()\n            .get(&id)\n            .map(|h| recover_log(&h.log).next_seq)\n    }\n\n    /// Drive one brain connection: handshake, then dispatch its command frames\n    /// until it disconnects. On disconnect, detach this connection from any\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\n    /// restart must not disturb the hosted children).\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n        struct HandlerTally<'a>(&'a AtomicUsize);\n        impl Drop for HandlerTally<'_> {\n            fn drop(&mut self) {\n                self.0.fetch_sub(1, Ordering::AcqRel);\n            }\n        }\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\n        let _tally = HandlerTally(&self.conn_handlers);\n\n        let (mut recv, send) = conn.split();\n        // Round-4: the send half is owned by the bounded/cancelable conn\n        // primitive from birth — every write on this physical connection is\n        // deadline-bounded, and a wedge poisons + retires the conn.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        send.describe(\"role=brain\");\n\n        // Version handshake: this connection must be a brain (2.3).\n        recv_hello(&mut recv, Role::Brain)?;\n\n        let mut my_subs: Vec<u64> = Vec::new();\n        let mut my_stream_subs: Vec<u64> = Vec::new();\n        let mut my_presence_sub = false;\n\n        loop {\n            let env = match read_frame(&mut recv) {\n                Ok(e) => e,\n                // Any read failure (EOF included) ends the connection.\n                Err(_) => break,\n            };\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_UNSUBSCRIBE => {\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_INPUT => {\n                    if let Err(msg) = self.dispatch_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_RESIZE => {\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_KILL => {\n                    if let Err(msg) = self.dispatch_kill(env) {\n                        send_error(&send, &msg);\n                    }\n                }\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\n                KIND_ADAPTER_APPLY => {\n                    if let Err(msg) = self.dispatch_adapter_apply(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STATUS => self.dispatch_net_status(&send),\n                // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\n                KIND_BROKER_IMAGE => self.dispatch_broker_image(&send),\n                KIND_STALL_EVICTS => self.dispatch_stall_evicts(&send),\n                KIND_NET_DIAL => {\n                    if let Err(msg) = self.dispatch_net_dial(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_DIAL_LOOPBACK => {\n                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                // [impl->REQ-PUMP-PEER-ISOLATION]\n                KIND_NET_DIAL_SUBMIT => {\n                    if let Err(msg) = self.dispatch_net_dial_submit(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_JOIN => {\n                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_MEET => {\n                    if let Err(msg) = self.dispatch_pair_meet(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_CODE_SUBMIT => {\n                    if let Err(msg) = self.dispatch_pair_code_submit(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_OPEN => {\n                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_SEND => {\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAMS => {\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_SESSIONS => {\n                    // Clone the (id, endpoint, log) out from under the sessions\n                    // lock, then read each log's cursor under the log lock alone —\n                    // never both locks at once (the dispatch_subscribe discipline).\n                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {\n                        let sessions = recover(&self.sessions);\n                        sessions\n                            .iter()\n                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))\n                            .collect()\n                    };\n                    // Snapshot each session's truth under its OWN log lock (cursor +\n                    // controller identity + the control/viewer stamp truth), doing NO\n                    // file I/O under the lock (the lock-across-effect discipline, KH\n                    // 7.12/5.16). The convergence re-assert + the reply then run OFF\n                    // the lock.\n                    struct SessSnap {\n                        id: u64,\n                        endpoint: String,\n                        resume_seq: u64,\n                        controller_by: Option<String>,\n                        has_controller: bool,\n                        viewers: u32,\n                    }\n                    let snaps: Vec<SessSnap> = rows\n                        .into_iter()\n                        .map(|(id, endpoint, log)| {\n                            let mut log = recover_log(&log);\n                            // B-2 (REQ-CONTROLLER-LIVENESS-REAP): make the controller\n                            // truth HONEST before reading it — reap a dead-writer\n                            // (severed) controller so both the reply's controller_by\n                            // AND the off-lock converge_perch_stamps below see the\n                            // cleared state and the stale info.json stamp clears.\n                            let _ = log.reap_dead_controller();\n                            SessSnap {\n                                id,\n                                endpoint,\n                                resume_seq: log.delivered_through(),\n                                controller_by: log.controller_by().map(str::to_string),\n                                has_controller: log.has_controller(),\n                                viewers: log.live_viewer_count(),\n                            }\n                        })\n                        .collect();\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\n                    // each live session's control/viewer stamps to broker truth,\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\n                    // pre-bind); converges within one reconcile-poll window after bind.\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n                    for s in &snaps {\n                        converge_perch_stamps(\n                            &s.endpoint,\n                            s.controller_by.as_deref(),\n                            s.has_controller,\n                            s.viewers,\n                        );\n                    }\n                    let reply = SessionsReply {\n                        sessions: snaps\n                            .into_iter()\n                            .map(|s| SessionInfo {\n                                session_id: s.id,\n                                endpoint: s.endpoint,\n                                resume_seq: s.resume_seq,\n                                controller_by: s.controller_by,\n                            })\n                            .collect(),\n                    };\n                    let frame = Envelope::new(\n                        KIND_SESSIONS_REPLY,\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\n                    );\n                    send_frame(&send, &frame);\n                }\n                KIND_ENDPOINT_INPUT => {\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n                    Ok(id) => my_stream_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_NET_PRESENCE_SUBSCRIBE => {\n                    match self.dispatch_net_presence_subscribe(env, &send) {\n                        Ok(()) => my_presence_sub = true,\n                        Err(msg) => send_error(&send, &msg),\n                    }\n                }\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n                // An unknown kind is tolerated (forward-compat): report, don't die.\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n            }\n        }\n\n        // Brain gone: detach as subscriber but leave the sessions running.\n        let sessions = recover(&self.sessions);\n        for id in &my_subs {\n            if let Some(h) = sessions.get(id) {\n                recover_log(&h.log).detach_if(&send);\n            }\n        }\n        drop(sessions);\n        // Same for net streams: the streams + their rings persist (D4b).\n        if let Some(host) = self.net.get() {\n            for id in &my_stream_subs {\n                host.detach_stream_sub(*id, &send);\n            }\n            // And presence: the liveness log + its ring persist (D4c).\n            if my_presence_sub {\n                host.detach_presence_sub(&send);\n            }\n        }\n        Ok(())\n    }\n\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\n    /// connection, start its output drain and exit waiter, and reply `spawned`.\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\n    /// 7.4): a hung child blocks only its own threads — shell instances ride\n    /// exactly this isolation (one owner's hung shell binary stalls neither\n    /// another owner's delivery nor the broker's control dispatch).\n\n[Showing lines 2959-3223 of 6009. Use :3224 to continue]"}],"details":{"truncation":{"content":"\r\n    /// The output-log high-water (`next_seq`) of a hosted session, or `None` if\r\n    /// absent. It advances every time the child emits output — the\r\n    /// **functional**-survival seam for the D7-1 process-level E2E (doyle F1):\r\n    /// after a brain-PROCESS swap, writing input to the broker-held PTY child and\r\n    /// watching THIS advance proves the path is alive end-to-end (input → child →\r\n    /// echo → broker ring), not merely that the handle is held. A read-only direct\r\n    /// peek, like [`session_pid`](Self::session_pid).\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\r\n        self.sessions\r\n            .lock()\r\n            .unwrap()\r\n            .get(&id)\r\n            .map(|h| recover_log(&h.log).next_seq)\r\n    }\r\n\r\n    /// Drive one brain connection: handshake, then dispatch its command frames\r\n    /// until it disconnects. On disconnect, detach this connection from any\r\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\r\n    /// restart must not disturb the hosted children).\r\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\r\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\r\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\r\n        struct HandlerTally<'a>(&'a AtomicUsize);\r\n        impl Drop for HandlerTally<'_> {\r\n            fn drop(&mut self) {\r\n                self.0.fetch_sub(1, Ordering::AcqRel);\r\n            }\r\n        }\r\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\r\n        let _tally = HandlerTally(&self.conn_handlers);\r\n\r\n        let (mut recv, send) = conn.split();\r\n        // Round-4: the send half is owned by the bounded/cancelable conn\r\n        // primitive from birth — every write on this physical connection is\r\n        // deadline-bounded, and a wedge poisons + retires the conn.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\r\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\r\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\r\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        send.describe(\"role=brain\");\r\n\r\n        // Version handshake: this connection must be a brain (2.3).\r\n        recv_hello(&mut recv, Role::Brain)?;\r\n\r\n        let mut my_subs: Vec<u64> = Vec::new();\r\n        let mut my_stream_subs: Vec<u64> = Vec::new();\r\n        let mut my_presence_sub = false;\r\n\r\n        loop {\r\n            let env = match read_frame(&mut recv) {\r\n                Ok(e) => e,\r\n                // Any read failure (EOF included) ends the connection.\r\n                Err(_) => break,\r\n            };\r\n            match env.kind.as_str() {\r\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_UNSUBSCRIBE => {\r\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_INPUT => {\r\n                    if let Err(msg) = self.dispatch_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_RESIZE => {\r\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_KILL => {\r\n                    if let Err(msg) = self.dispatch_kill(env) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n                KIND_ADAPTER_APPLY => {\r\n                    if let Err(msg) = self.dispatch_adapter_apply(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STATUS => self.dispatch_net_status(&send),\r\n                // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\r\n                KIND_BROKER_IMAGE => self.dispatch_broker_image(&send),\r\n                KIND_STALL_EVICTS => self.dispatch_stall_evicts(&send),\r\n                KIND_NET_DIAL => {\r\n                    if let Err(msg) = self.dispatch_net_dial(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_DIAL_LOOPBACK => {\r\n                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-PUMP-PEER-ISOLATION]\r\n                KIND_NET_DIAL_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_net_dial_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_JOIN => {\r\n                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_MEET => {\r\n                    if let Err(msg) = self.dispatch_pair_meet(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_CODE_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_pair_code_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_OPEN => {\r\n                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SEND => {\r\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAMS => {\r\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_SESSIONS => {\r\n                    // Clone the (id, endpoint, log) out from under the sessions\r\n                    // lock, then read each log's cursor under the log lock alone —\r\n                    // never both locks at once (the dispatch_subscribe discipline).\r\n                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {\r\n                        let sessions = recover(&self.sessions);\r\n                        sessions\r\n                            .iter()\r\n                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))\r\n                            .collect()\r\n                    };\r\n                    // Snapshot each session's truth under its OWN log lock (cursor +\r\n                    // controller identity + the control/viewer stamp truth), doing NO\r\n                    // file I/O under the lock (the lock-across-effect discipline, KH\r\n                    // 7.12/5.16). The convergence re-assert + the reply then run OFF\r\n                    // the lock.\r\n                    struct SessSnap {\r\n                        id: u64,\r\n                        endpoint: String,\r\n                        resume_seq: u64,\r\n                        controller_by: Option<String>,\r\n                        has_controller: bool,\r\n                        viewers: u32,\r\n                    }\r\n                    let snaps: Vec<SessSnap> = rows\r\n                        .into_iter()\r\n                        .map(|(id, endpoint, log)| {\r\n                            let mut log = recover_log(&log);\r\n                            // B-2 (REQ-CONTROLLER-LIVENESS-REAP): make the controller\r\n                            // truth HONEST before reading it — reap a dead-writer\r\n                            // (severed) controller so both the reply's controller_by\r\n                            // AND the off-lock converge_perch_stamps below see the\r\n                            // cleared state and the stale info.json stamp clears.\r\n                            let _ = log.reap_dead_controller();\r\n                            SessSnap {\r\n                                id,\r\n                                endpoint,\r\n                                resume_seq: log.delivered_through(),\r\n                                controller_by: log.controller_by().map(str::to_string),\r\n                                has_controller: log.has_controller(),\r\n                                viewers: log.live_viewer_count(),\r\n                            }\r\n                        })\r\n                        .collect();\r\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\r\n                    // each live session's control/viewer stamps to broker truth,\r\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\r\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\r\n                    // pre-bind); converges within one reconcile-poll window after bind.\r\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\n                    for s in &snaps {\r\n                        converge_perch_stamps(\r\n                            &s.endpoint,\r\n                            s.controller_by.as_deref(),\r\n                            s.has_controller,\r\n                            s.viewers,\r\n                        );\r\n                    }\r\n                    let reply = SessionsReply {\r\n                        sessions: snaps\r\n                            .into_iter()\r\n                            .map(|s| SessionInfo {\r\n                                session_id: s.id,\r\n                                endpoint: s.endpoint,\r\n                                resume_seq: s.resume_seq,\r\n                                controller_by: s.controller_by,\r\n                            })\r\n                            .collect(),\r\n                    };\r\n                    let frame = Envelope::new(\r\n                        KIND_SESSIONS_REPLY,\r\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\r\n                    );\r\n                    send_frame(&send, &frame);\r\n                }\r\n                KIND_ENDPOINT_INPUT => {\r\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\r\n                    Ok(id) => my_stream_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_NET_PRESENCE_SUBSCRIBE => {\r\n                    match self.dispatch_net_presence_subscribe(env, &send) {\r\n                        Ok(()) => my_presence_sub = true,\r\n                        Err(msg) => send_error(&send, &msg),\r\n                    }\r\n                }\r\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\r\n                // An unknown kind is tolerated (forward-compat): report, don't die.\r\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\r\n            }\r\n        }\r\n\r\n        // Brain gone: detach as subscriber but leave the sessions running.\r\n        let sessions = recover(&self.sessions);\r\n        for id in &my_subs {\r\n            if let Some(h) = sessions.get(id) {\r\n                recover_log(&h.log).detach_if(&send);\r\n            }\r\n        }\r\n        drop(sessions);\r\n        // Same for net streams: the streams + their rings persist (D4b).\r\n        if let Some(host) = self.net.get() {\r\n            for id in &my_stream_subs {\r\n                host.detach_stream_sub(*id, &send);\r\n            }\r\n            // And presence: the liveness log + its ring persist (D4c).\r\n            if my_presence_sub {\r\n                host.detach_presence_sub(&send);\r\n            }\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\r\n    /// connection, start its output drain and exit waiter, and reply `spawned`.\r\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\r\n    /// 7.4): a hung child blocks only its own threads — shell instances ride\r\n    /// exactly this isolation (one owner's hung shell binary stalls neither\r\n    /// another owner's delivery nor the broker's control dispatch).\r","truncated":true,"truncatedBy":"lines","totalLines":3051,"totalBytes":13050,"outputLines":265,"outputBytes":13050,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"\r\n    /// The output-log high-water (`next_seq`) of a hosted session, or `None` if\r\n    /// absent. It advances every time the child emits output — the\r\n    /// **functional**-survival seam for the D7-1 process-level E2E (doyle F1):\r\n    /// after a brain-PROCESS swap, writing input to the broker-held PTY child and\r\n    /// watching THIS advance proves the path is alive end-to-end (input → child →\r\n    /// echo → broker ring), not merely that the handle is held. A read-only direct\r\n    /// peek, like [`session_pid`](Self::session_pid).\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\r\n        self.sessions\r\n            .lock()\r\n            .unwrap()\r\n            .get(&id)\r\n            .map(|h| recover_log(&h.log).next_seq)\r\n    }\r\n\r\n    /// Drive one brain connection: handshake, then dispatch its command frames\r\n    /// until it disconnects. On disconnect, detach this connection from any\r\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\r\n    /// restart must not disturb the hosted children).\r\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\r\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\r\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\r\n        struct HandlerTally<'a>(&'a AtomicUsize);\r\n        impl Drop for HandlerTally<'_> {\r\n            fn drop(&mut self) {\r\n                self.0.fetch_sub(1, Ordering::AcqRel);\r\n            }\r\n        }\r\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\r\n        let _tally = HandlerTally(&self.conn_handlers);\r\n\r\n        let (mut recv, send) = conn.split();\r\n        // Round-4: the send half is owned by the bounded/cancelable conn\r\n        // primitive from birth — every write on this physical connection is\r\n        // deadline-bounded, and a wedge poisons + retires the conn.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\r\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\r\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\r\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        send.describe(\"role=brain\");\r\n\r\n        // Version handshake: this connection must be a brain (2.3).\r\n        recv_hello(&mut recv, Role::Brain)?;\r\n\r\n        let mut my_subs: Vec<u64> = Vec::new();\r\n        let mut my_stream_subs: Vec<u64> = Vec::new();\r\n        let mut my_presence_sub = false;\r\n\r\n        loop {\r\n            let env = match read_frame(&mut recv) {\r\n                Ok(e) => e,\r\n                // Any read failure (EOF included) ends the connection.\r\n                Err(_) => break,\r\n            };\r\n            match env.kind.as_str() {\r\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_UNSUBSCRIBE => {\r\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_INPUT => {\r\n                    if let Err(msg) = self.dispatch_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_RESIZE => {\r\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_KILL => {\r\n                    if let Err(msg) = self.dispatch_kill(env) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n                KIND_ADAPTER_APPLY => {\r\n                    if let Err(msg) = self.dispatch_adapter_apply(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STATUS => self.dispatch_net_status(&send),\r\n                // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\r\n                KIND_BROKER_IMAGE => self.dispatch_broker_image(&send),\r\n                KIND_STALL_EVICTS => self.dispatch_stall_evicts(&send),\r\n                KIND_NET_DIAL => {\r\n                    if let Err(msg) = self.dispatch_net_dial(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_DIAL_LOOPBACK => {\r\n                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-PUMP-PEER-ISOLATION]\r\n                KIND_NET_DIAL_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_net_dial_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_JOIN => {\r\n                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_MEET => {\r\n                    if let Err(msg) = self.dispatch_pair_meet(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_CODE_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_pair_code_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_OPEN => {\r\n                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SEND => {\r\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAMS => {\r\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_SESSIONS => {\r\n                    // Clone the (id, endpoint, log) out from under the sessions\r\n                    // lock, then read each log's cursor under the log lock alone —\r\n                    // never both locks at once (the dispatch_subscribe discipline).\r\n                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {\r\n                        let sessions = recover(&self.sessions);\r\n                        sessions\r\n                            .iter()\r\n                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))\r\n                            .collect()\r\n                    };\r\n                    // Snapshot each session's truth under its OWN log lock (cursor +\r\n                    // controller identity + the control/viewer stamp truth), doing NO\r\n                    // file I/O under the lock (the lock-across-effect discipline, KH\r\n                    // 7.12/5.16). The convergence re-assert + the reply then run OFF\r\n                    // the lock.\r\n                    struct SessSnap {\r\n                        id: u64,\r\n                        endpoint: String,\r\n                        resume_seq: u64,\r\n                        controller_by: Option<String>,\r\n                        has_controller: bool,\r\n                        viewers: u32,\r\n                    }\r\n                    let snaps: Vec<SessSnap> = rows\r\n                        .into_iter()\r\n                        .map(|(id, endpoint, log)| {\r\n                            let mut log = recover_log(&log);\r\n                            // B-2 (REQ-CONTROLLER-LIVENESS-REAP): make the controller\r\n                            // truth HONEST before reading it — reap a dead-writer\r\n                            // (severed) controller so both the reply's controller_by\r\n                            // AND the off-lock converge_perch_stamps below see the\r\n                            // cleared state and the stale info.json stamp clears.\r\n                            let _ = log.reap_dead_controller();\r\n                            SessSnap {\r\n                                id,\r\n                                endpoint,\r\n                                resume_seq: log.delivered_through(),\r\n                                controller_by: log.controller_by().map(str::to_string),\r\n                                has_controller: log.has_controller(),\r\n                                viewers: log.live_viewer_count(),\r\n                            }\r\n                        })\r\n                        .collect();\r\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\r\n                    // each live session's control/viewer stamps to broker truth,\r\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\r\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\r\n                    // pre-bind); converges within one reconcile-poll window after bind.\r\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\n                    for s in &snaps {\r\n                        converge_perch_stamps(\r\n                            &s.endpoint,\r\n                            s.controller_by.as_deref(),\r\n                            s.has_controller,\r\n                            s.viewers,\r\n                        );\r\n                    }\r\n                    let reply = SessionsReply {\r\n                        sessions: snaps\r\n                            .into_iter()\r\n                            .map(|s| SessionInfo {\r\n                                session_id: s.id,\r\n                                endpoint: s.endpoint,\r\n                                resume_seq: s.resume_seq,\r\n                                controller_by: s.controller_by,\r\n                            })\r\n                            .collect(),\r\n                    };\r\n                    let frame = Envelope::new(\r\n                        KIND_SESSIONS_REPLY,\r\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\r\n                    );\r\n                    send_frame(&send, &frame);\r\n                }\r\n                KIND_ENDPOINT_INPUT => {\r\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\r\n                    Ok(id) => my_stream_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_NET_PRESENCE_SUBSCRIBE => {\r\n                    match self.dispatch_net_presence_subscribe(env, &send) {\r\n                        Ok(()) => my_presence_sub = true,\r\n                        Err(msg) => send_error(&send, &msg),\r\n                    }\r\n                }\r\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\r\n                // An unknown kind is tolerated (forward-compat): report, don't die.\r\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\r\n            }\r\n        }\r\n\r\n        // Brain gone: detach as subscriber but leave the sessions running.\r\n        let sessions = recover(&self.sessions);\r\n        for id in &my_subs {\r\n            if let Some(h) = sessions.get(id) {\r\n                recover_log(&h.log).detach_if(&send);\r\n            }\r\n        }\r\n        drop(sessions);\r\n        // Same for net streams: the streams + their rings persist (D4b).\r\n        if let Some(host) = self.net.get() {\r\n            for id in &my_stream_subs {\r\n                host.detach_stream_sub(*id, &send);\r\n            }\r\n            // And presence: the liveness log + its ring persist (D4c).\r\n            if my_presence_sub {\r\n                host.detach_presence_sub(&send);\r\n            }\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\r\n    /// connection, start its output drain and exit waiter, and reply `spawned`.\r\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\r\n    /// 7.4): a hung child blocks only its own threads — shell instances ride\r\n    /// exactly this isolation (one owner's hung shell binary stalls neither\r\n    /// another owner's delivery nor the broker's control dispatch).\r","startLine":2959,"lineNumbers":[2959,2960,2961,2962,2963,2964,2965,2966,2967,2968,2969,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3113,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156,3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,3170,3171,3172,3173,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3184,3185,3186,3187,3188,3189,3190,3191,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212,3213,3214,3215,3216,3217,3218,3219,3220,3221,3222,3223]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6009,"totalBytes":13050,"outputLines":265,"outputBytes":13050,"shownRange":{"start":2959,"end":3223},"nextOffset":3224}}},"isError":false,"timestamp":1784195789269}}
{"type":"message","id":"45a5b1ec","parentId":"659d43fc","timestamp":"2026-07-16T09:56:29.304Z","message":{"role":"toolResult","toolCallId":"call_bWO5zCI0h99bdO0X6D7WADi8|fc_008390f508d1d09b016a58aacd129c819abaaddb55cd5c9e56","toolName":"read","content":[{"type":"text","text":"/// Pure divergence decision for the control/viewer stamp convergence\n/// (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): does the perch's RECORDED\n/// (`driven_by`, `controlled`, `viewer_count`) differ from the live session TRUTH?\n/// Returns `(control_write_needed, viewer_write_needed)` — the divergence gate that\n/// keeps the per-poll re-assert from writing (fsync) when nothing changed.\n// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\nfn stamp_divergence(\n    rec_driven_by: Option<&str>,\n    rec_controlled: bool,\n    rec_viewers: u32,\n    want_driven_by: Option<&str>,\n    want_controlled: bool,\n    want_viewers: u32,\n) -> (bool, bool) {\n    let control = rec_driven_by != want_driven_by || rec_controlled != want_controlled;\n    let viewer = rec_viewers != want_viewers;\n    (control, viewer)\n}\n\n/// Converge a perch's control/viewer stamps to the live session truth, OFF any log\n/// lock (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). The broker stays the single writer;\n/// this closes the STAMP-BEFORE-BIND loss — a picker-created endpoint's spawn stamps\n/// `controlled=true` BEFORE the adapter binds its perch, so `mutate_info` returns\n/// `NotFound` and the edge stamp is swallowed; the adapter's bind then writes\n/// `controlled:false` and no later edge re-stamps, so the endpoint reads uncontrolled\n/// forever while driven. This DIVERGENCE-GATED re-assert (on the `KIND_SESSIONS` poll,\n/// bounded by the reconcile cadence) writes ONLY on a real difference — no fsync storm.\n/// Best-effort: an unbound / corrupt / perch-less endpoint has nowhere to converge yet.\n// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\nfn converge_perch_stamps(\n    endpoint: &str,\n    want_driven_by: Option<&str>,\n    want_controlled: bool,\n    want_viewers: u32,\n) {\n    if endpoint.is_empty() {\n        return;\n    }\n    let perch = resolve_perch_path(endpoint, ParentHint::Infer);\n    let Some(rec) = spt_store::info::read_info(&perch) else {\n        return; // perch not bound yet (the pre-bind window) / corrupt — nothing to gate on\n    };\n    let (control, viewer) = stamp_divergence(\n        rec.driven_by.as_deref(),\n        rec.controlled,\n        rec.viewer_count.unwrap_or(0),\n        want_driven_by,\n        want_controlled,\n        want_viewers,\n    );\n    if control {\n        let _ = spt_store::info::set_driven_by(&perch, want_driven_by);\n        let _ = spt_store::info::set_controlled(&perch, want_controlled);\n    }\n    if viewer {\n        let _ = spt_store::info::set_viewer_count(&perch, want_viewers);\n    }\n}\n\nimpl OutputLog {\n    fn new(session_id: u64, cap_chunks: usize, endpoint: String, size: (u16, u16)) -> Self {\n        OutputLog {\n            session_id,\n            next_seq: 0,\n            ring: VecDeque::new(),\n            cap_chunks,\n            controller: None,\n            controller_epoch: Arc::new(AtomicU64::new(0)),\n            viewers: HashMap::new(),\n            next_viewer_id: 0,\n            endpoint,\n            size,\n            delivered_through: Arc::new(AtomicU64::new(0)),\n            grid: ScreenGrid::new(size.0, size.1),\n        }\n    }\n\n    /// The synthesized clean-repaint initial batch (#6, ADR-0031): a single frame\n    /// carrying the current screen at pseudo-seq `next_seq - 1`.\n    ///\n    /// **Scoped to a COLD attach (`from_seq == 0`)** — the `spt rc` / `endpoint run\n    /// --attach` case #6 is about (attach to an already-producing TUI). A\n    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\n    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\n    /// exactly-once replay + the strict reject-gap + the irrecoverable-behind\n    /// detection untouched (REQ-HAZARD-CONTROLLER-GAP-RESUME / B2). `None` also when\n    /// nothing has been produced yet (`next_seq == 0`) → an empty initial batch, so\n    /// the first live frame is the contiguous seq 0 (the spawn seat, unchanged).\n    fn repaint_initial(&self, from_seq: u64) -> Option<(u64, Vec<u8>)> {\n        if from_seq != 0 || self.next_seq == 0 {\n            return None;\n        }\n        Some((self.next_seq - 1, self.grid.render_repaint()))\n    }\n\n    /// Append a freshly-produced output chunk: assign the next `seq`, retain it in\n    /// the bounded ring, then FAN OUT to viewers AND the controller — both a SINGLE\n    /// non-blocking `try_send`, so the drain NEVER blocks/sleeps here. Returns\n    /// `Some(epoch)` when the controller must be EVICTED (stalled past the deadline,\n    /// or its writer is gone) so the drain can `mark_controller_gone(epoch)` OFF the\n    /// log lock; `None` otherwise.\n    ///\n    /// b4 (REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE): W1 moved the\n    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but\n    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to\n    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower\n    /// than the PTY floods filled its channel, the inline poll throttled the drain,\n    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\n    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\n    /// exactly like a viewer: the drain runs at full PTY speed regardless of\n    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\n    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\n    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\n    /// (bounded wedge preserved). The writer still does the blocking socket write +\n    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's\n    /// single drain thread.\n    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n    // [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\n    #[must_use]\n    /// Coherence clamp for a poison-RECOVERED ring (B-1, doyle ruling; run only by\n    /// [`recover_log`] on the recovery path). A panic mid-[`append`] can leave the\n    /// ring torn — an over-cap ring (interrupted prune), a last seq not below\n    /// `next_seq`, or a non-monotonic front/back — and serving those bytes blind\n    /// risks garbage to a subscriber. Cheap-check the ring invariants (counts within\n    /// cap, indices ordered and below `next_seq`); on ANY violation RESET the ring\n    /// EMPTY (+ loud log) — `next_seq` is left intact so cursors never rewind, and the\n    /// lost scrollback self-heals on the next PTY output + repaint. Returns `true`\n    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\n    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\n    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n    fn clamp_or_reset(&mut self) -> bool {\n        let within_cap = self.ring.len() <= self.cap_chunks;\n        let last_below_next = match self.ring.back() {\n            Some(&(last, _)) => last < self.next_seq,\n            None => true,\n        };\n        let ordered = match (self.ring.front(), self.ring.back()) {\n            (Some(&(front, _)), Some(&(back, _))) => front <= back,\n            _ => true,\n        };\n        if within_cap && last_below_next && ordered {\n            return false;\n        }\n        let had = self.ring.len();\n        self.ring.clear();\n        eprintln!(\n            \"broker: session {} output ring was torn by a poison-recover \\\n             (had {had} chunks, next_seq {}); reset to empty — scrollback self-heals \\\n             on the next output [REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\",\n            self.session_id, self.next_seq\n        );\n        true\n    }\n\n    /// The current high-water sequence (the next seq to be appended) — the snapshot\n    /// point for the inject leg's settle-gate + echo-verify (REQ-INJECT-MULTILINE-\n    /// INTEGRITY). A rising high-water since a baseline == the PTY produced output.\n    fn high_water(&self) -> u64 {\n        self.next_seq\n    }\n\n    /// Concatenate ring bytes at or after `from_seq` — the output PRODUCED since a\n    /// snapshot, used to confirm a settle probe echoed (readiness, Layer 1) and that\n    /// a typed payload's head echoed back (integrity, Layer 2). Best-effort: bytes\n    /// pruned out of the bounded ring are simply absent (the echo we look for is\n    /// recent, well within cap).\n    fn bytes_since(&self, from_seq: u64) -> Vec<u8> {\n        let mut out = Vec::new();\n        for (seq, chunk) in self.ring.iter() {\n            if *seq >= from_seq {\n                out.extend_from_slice(chunk);\n            }\n        }\n        out\n    }\n\n    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\n        let seq = self.next_seq;\n        self.next_seq += 1;\n        self.ring.push_back((seq, bytes.to_vec()));\n        while self.ring.len() > self.cap_chunks {\n            self.ring.pop_front();\n        }\n        let frame = output_envelope(self.session_id, seq, bytes);\n        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.\n        if !self.viewers.is_empty() {\n            let mut evicted: Vec<u64> = Vec::new();\n            for (vid, sink) in self.viewers.iter() {\n                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {\n                    evicted.push(*vid);\n                }\n            }\n            if !evicted.is_empty() {\n                for vid in evicted {\n                    // Mark eviction BEFORE dropping the sink: the writer's\n                    // `rx.recv()` then returns Err (tx dropped) and reads this flag\n                    // to emit a KIND_VIEWER_EVICTED marker (skip-to-live, off the\n                    // log lock) instead of dying silently (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).\n                    if let Some(sink) = self.viewers.remove(&vid) {\n                        sink.evicted.store(true, Ordering::Release);\n                    }\n                }\n                self.stamp_viewer_count();\n            }\n        }\n        // Controller: the SAME non-blocking discipline as a viewer (b4). One\n        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\n        // slow controller falls behind the ring) unless it has been Full for the\n        // whole deadline since its last success → EVICT. Disconnected (writer gone)\n        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\n        let now = Instant::now();\n        let epoch = self.controller_epoch.load(Ordering::Acquire);\n        let result = if let Some(c) = self.controller.as_mut() {\n            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {\n                Ok(()) => {\n                    c.last_ok = now;\n                    None\n                }\n                Err(TrySendError::Full(_)) => {\n                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {\n                        Some(epoch) // stalled past the deadline → bounded detach\n                    } else {\n                        None // drop this chunk; controller falls behind the ring\n                    }\n                }\n                Err(TrySendError::Disconnected(_)) => Some(epoch),\n            }\n        } else {\n            None\n        };\n        // Advance the render grid AFTER the fan-out — it must never sit between the\n        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\n        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\n        // snapshots it under the same short log lock only when an attach requests it.\n        self.grid.advance(bytes);\n        result\n    }\n\n    /// The current controller's identity (`Some(node)` remote / `None` local),\n    /// or `None` when undriven. The `by`-keyed ownership logic in\n    /// [`resolve_subscribe`] reads this.\n    fn controller_by(&self) -> Option<&str> {\n        self.controller.as_ref().and_then(|c| c.by.as_deref())\n    }\n\n    /// Whether ANY controller currently drives this session (the `controlled`\n    /// any-controller truth — REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). Snapshotted under\n    /// the log lock, then converged onto the perch OFF the lock.\n    fn has_controller(&self) -> bool {\n        self.controller.is_some()\n    }\n\n    /// The live viewer count (the `viewer_count` truth). Snapshotted under the log lock\n    /// for the convergence re-assert.\n    fn live_viewer_count(&self) -> u32 {\n        self.viewers.len() as u32\n    }\n\n    /// Become (or silently re-take) the interactive controller from `from_seq`.\n    ///\n    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller\n    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\n    /// inline under the log lock. The writer thread advances the authoritative\n    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,\n    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\n    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\n    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\n    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\n    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\n    /// single live writer per connection and a monotonic on-wire seq stream,\n    /// [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]). Stamps `driven_by` to the new\n    /// controller's identity.\n    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {\n        // Drop the prior controller sink first (its writer's live loop ends when\n        // tx drops), and bump the generation so (a) an in-flight deadline-evict\n        // for the old controller can't unseat this one and (b) a prior writer\n        // still flushing its initial batch returns before its next frame.\n        // Attribution (F-039 legs b+d): the replaced conn's record names its\n        // successor, and the new conn accretes the controller role fact.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        if let Some(old) = self.controller.take() {\n            old.send.lifecycle_event(\n                \"controller-replaced\",\n                &format!(\"session={} new_conn={}\", self.session_id, sub.id()),\n            );\n        }\n        sub.describe(&format!(\n            \"controller session={} endpoint={} by={}\",\n            self.session_id,\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint },\n            by.as_deref().unwrap_or(\"local\")\n        ));\n        sub.lifecycle_event(\n            \"controller-attach\",\n            &format!(\"session={} from_seq={from_seq}\", self.session_id),\n        );\n        self.controller = None;\n        let epoch = self.controller_epoch.fetch_add(1, Ordering::AcqRel) + 1;\n        // #6 (ADR-0031): a COLD attach's initial batch is a single synthesized clean\n        // repaint of the CURRENT SCREEN (at pseudo-seq `next_seq - 1`) instead of the\n        // raw ring — replaying an alt-screen TUI's raw ring into a fresh terminal is\n        // exactly the corruption #6/#7/#8 report. A resume-from-floor (`from_seq > 0`)\n        // keeps the raw-ring exactly-once re-fetch untouched (B2). Either way the\n        // batch is owned by the writer thread (never pushed through the live channel —\n        // it must not block the drain); live frames stream raw + in-order afterwards.\n        let initial: Vec<(u64, Vec<u8>)> = match self.repaint_initial(from_seq) {\n            Some(repaint) => vec![repaint],\n            None => self\n                .ring\n                .iter()\n                .filter(|(s, _)| *s >= from_seq)\n                .cloned()\n                .collect(),\n        };\n        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n        let writer_send = Arc::clone(&sub);\n        let dt = Arc::clone(&self.delivered_through);\n        let epoch_gate = Arc::clone(&self.controller_epoch);\n        let sid = self.session_id;\n        // W2 decouple: the writer publishes its in-flight-write window here so a\n        // stall-evict can distinguish a wedged writer from an idle one.\n        let write_blocked_since = Arc::new(Mutex::new(None));\n        let writer_blocked = Arc::clone(&write_blocked_since);\n        let writer = thread::spawn(move || {\n            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\n        });\n        self.controller = Some(ControllerSink {\n            tx,\n            send: sub,\n            by,\n            last_ok: Instant::now(),\n            write_blocked_since,\n            _writer: writer,\n        });\n        self.stamp_driven_by();\n    }\n\n    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared\n    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per\n    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its\n    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an\n    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\n    fn clear_controller(&mut self) {\n        if self.controller.take().is_some() {\n            self.stamp_driven_by();\n        }\n    }\n\n    /// B-2 (REQ-CONTROLLER-LIVENESS-REAP): lazily drop a controller whose WRITER\n    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\n    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\n    /// writer exits when a socket write fails (a severed / keepalive-killed controller\n    /// conn) or its `tx` drops; while it is parked on `rx.recv()` (a LIVE but idle\n    /// controller) it is NOT finished, so a live controller is never touched (the\n    /// no-false-reap guard). Reaping here makes the broker report honest truth so the\n…\n}\n\n[Showing lines 619-973 of 6009. Use :974 to continue]"}],"details":{"truncation":{"content":"/// Pure divergence decision for the control/viewer stamp convergence\r\n/// (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): does the perch's RECORDED\r\n/// (`driven_by`, `controlled`, `viewer_count`) differ from the live session TRUTH?\r\n/// Returns `(control_write_needed, viewer_write_needed)` — the divergence gate that\r\n/// keeps the per-poll re-assert from writing (fsync) when nothing changed.\r\n// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\nfn stamp_divergence(\r\n    rec_driven_by: Option<&str>,\r\n    rec_controlled: bool,\r\n    rec_viewers: u32,\r\n    want_driven_by: Option<&str>,\r\n    want_controlled: bool,\r\n    want_viewers: u32,\r\n) -> (bool, bool) {\r\n    let control = rec_driven_by != want_driven_by || rec_controlled != want_controlled;\r\n    let viewer = rec_viewers != want_viewers;\r\n    (control, viewer)\r\n}\r\n\r\n/// Converge a perch's control/viewer stamps to the live session truth, OFF any log\r\n/// lock (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). The broker stays the single writer;\r\n/// this closes the STAMP-BEFORE-BIND loss — a picker-created endpoint's spawn stamps\r\n/// `controlled=true` BEFORE the adapter binds its perch, so `mutate_info` returns\r\n/// `NotFound` and the edge stamp is swallowed; the adapter's bind then writes\r\n/// `controlled:false` and no later edge re-stamps, so the endpoint reads uncontrolled\r\n/// forever while driven. This DIVERGENCE-GATED re-assert (on the `KIND_SESSIONS` poll,\r\n/// bounded by the reconcile cadence) writes ONLY on a real difference — no fsync storm.\r\n/// Best-effort: an unbound / corrupt / perch-less endpoint has nowhere to converge yet.\r\n// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\nfn converge_perch_stamps(\r\n    endpoint: &str,\r\n    want_driven_by: Option<&str>,\r\n    want_controlled: bool,\r\n    want_viewers: u32,\r\n) {\r\n    if endpoint.is_empty() {\r\n        return;\r\n    }\r\n    let perch = resolve_perch_path(endpoint, ParentHint::Infer);\r\n    let Some(rec) = spt_store::info::read_info(&perch) else {\r\n        return; // perch not bound yet (the pre-bind window) / corrupt — nothing to gate on\r\n    };\r\n    let (control, viewer) = stamp_divergence(\r\n        rec.driven_by.as_deref(),\r\n        rec.controlled,\r\n        rec.viewer_count.unwrap_or(0),\r\n        want_driven_by,\r\n        want_controlled,\r\n        want_viewers,\r\n    );\r\n    if control {\r\n        let _ = spt_store::info::set_driven_by(&perch, want_driven_by);\r\n        let _ = spt_store::info::set_controlled(&perch, want_controlled);\r\n    }\r\n    if viewer {\r\n        let _ = spt_store::info::set_viewer_count(&perch, want_viewers);\r\n    }\r\n}\r\n\r\nimpl OutputLog {\r\n    fn new(session_id: u64, cap_chunks: usize, endpoint: String, size: (u16, u16)) -> Self {\r\n        OutputLog {\r\n            session_id,\r\n            next_seq: 0,\r\n            ring: VecDeque::new(),\r\n            cap_chunks,\r\n            controller: None,\r\n            controller_epoch: Arc::new(AtomicU64::new(0)),\r\n            viewers: HashMap::new(),\r\n            next_viewer_id: 0,\r\n            endpoint,\r\n            size,\r\n            delivered_through: Arc::new(AtomicU64::new(0)),\r\n            grid: ScreenGrid::new(size.0, size.1),\r\n        }\r\n    }\r\n\r\n    /// The synthesized clean-repaint initial batch (#6, ADR-0031): a single frame\r\n    /// carrying the current screen at pseudo-seq `next_seq - 1`.\r\n    ///\r\n    /// **Scoped to a COLD attach (`from_seq == 0`)** — the `spt rc` / `endpoint run\r\n    /// --attach` case #6 is about (attach to an already-producing TUI). A\r\n    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\r\n    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\r\n    /// exactly-once replay + the strict reject-gap + the irrecoverable-behind\r\n    /// detection untouched (REQ-HAZARD-CONTROLLER-GAP-RESUME / B2). `None` also when\r\n    /// nothing has been produced yet (`next_seq == 0`) → an empty initial batch, so\r\n    /// the first live frame is the contiguous seq 0 (the spawn seat, unchanged).\r\n    fn repaint_initial(&self, from_seq: u64) -> Option<(u64, Vec<u8>)> {\r\n        if from_seq != 0 || self.next_seq == 0 {\r\n            return None;\r\n        }\r\n        Some((self.next_seq - 1, self.grid.render_repaint()))\r\n    }\r\n\r\n    /// Append a freshly-produced output chunk: assign the next `seq`, retain it in\r\n    /// the bounded ring, then FAN OUT to viewers AND the controller — both a SINGLE\r\n    /// non-blocking `try_send`, so the drain NEVER blocks/sleeps here. Returns\r\n    /// `Some(epoch)` when the controller must be EVICTED (stalled past the deadline,\r\n    /// or its writer is gone) so the drain can `mark_controller_gone(epoch)` OFF the\r\n    /// log lock; `None` otherwise.\r\n    ///\r\n    /// b4 (REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE): W1 moved the\r\n    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but\r\n    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to\r\n    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower\r\n    /// than the PTY floods filled its channel, the inline poll throttled the drain,\r\n    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\r\n    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\r\n    /// exactly like a viewer: the drain runs at full PTY speed regardless of\r\n    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\r\n    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\r\n    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\r\n    /// (bounded wedge preserved). The writer still does the blocking socket write +\r\n    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's\r\n    /// single drain thread.\r\n    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\r\n    // [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\r\n    #[must_use]\r\n    /// Coherence clamp for a poison-RECOVERED ring (B-1, doyle ruling; run only by\r\n    /// [`recover_log`] on the recovery path). A panic mid-[`append`] can leave the\r\n    /// ring torn — an over-cap ring (interrupted prune), a last seq not below\r\n    /// `next_seq`, or a non-monotonic front/back — and serving those bytes blind\r\n    /// risks garbage to a subscriber. Cheap-check the ring invariants (counts within\r\n    /// cap, indices ordered and below `next_seq`); on ANY violation RESET the ring\r\n    /// EMPTY (+ loud log) — `next_seq` is left intact so cursors never rewind, and the\r\n    /// lost scrollback self-heals on the next PTY output + repaint. Returns `true`\r\n    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\r\n    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\r\n    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\r\n    fn clamp_or_reset(&mut self) -> bool {\r\n        let within_cap = self.ring.len() <= self.cap_chunks;\r\n        let last_below_next = match self.ring.back() {\r\n            Some(&(last, _)) => last < self.next_seq,\r\n            None => true,\r\n        };\r\n        let ordered = match (self.ring.front(), self.ring.back()) {\r\n            (Some(&(front, _)), Some(&(back, _))) => front <= back,\r\n            _ => true,\r\n        };\r\n        if within_cap && last_below_next && ordered {\r\n            return false;\r\n        }\r\n        let had = self.ring.len();\r\n        self.ring.clear();\r\n        eprintln!(\r\n            \"broker: session {} output ring was torn by a poison-recover \\\r\n             (had {had} chunks, next_seq {}); reset to empty — scrollback self-heals \\\r\n             on the next output [REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\",\r\n            self.session_id, self.next_seq\r\n        );\r\n        true\r\n    }\r\n\r\n    /// The current high-water sequence (the next seq to be appended) — the snapshot\r\n    /// point for the inject leg's settle-gate + echo-verify (REQ-INJECT-MULTILINE-\r\n    /// INTEGRITY). A rising high-water since a baseline == the PTY produced output.\r\n    fn high_water(&self) -> u64 {\r\n        self.next_seq\r\n    }\r\n\r\n    /// Concatenate ring bytes at or after `from_seq` — the output PRODUCED since a\r\n    /// snapshot, used to confirm a settle probe echoed (readiness, Layer 1) and that\r\n    /// a typed payload's head echoed back (integrity, Layer 2). Best-effort: bytes\r\n    /// pruned out of the bounded ring are simply absent (the echo we look for is\r\n    /// recent, well within cap).\r\n    fn bytes_since(&self, from_seq: u64) -> Vec<u8> {\r\n        let mut out = Vec::new();\r\n        for (seq, chunk) in self.ring.iter() {\r\n            if *seq >= from_seq {\r\n                out.extend_from_slice(chunk);\r\n            }\r\n        }\r\n        out\r\n    }\r\n\r\n    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\r\n        let seq = self.next_seq;\r\n        self.next_seq += 1;\r\n        self.ring.push_back((seq, bytes.to_vec()));\r\n        while self.ring.len() > self.cap_chunks {\r\n            self.ring.pop_front();\r\n        }\r\n        let frame = output_envelope(self.session_id, seq, bytes);\r\n        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.\r\n        if !self.viewers.is_empty() {\r\n            let mut evicted: Vec<u64> = Vec::new();\r\n            for (vid, sink) in self.viewers.iter() {\r\n                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {\r\n                    evicted.push(*vid);\r\n                }\r\n            }\r\n            if !evicted.is_empty() {\r\n                for vid in evicted {\r\n                    // Mark eviction BEFORE dropping the sink: the writer's\r\n                    // `rx.recv()` then returns Err (tx dropped) and reads this flag\r\n                    // to emit a KIND_VIEWER_EVICTED marker (skip-to-live, off the\r\n                    // log lock) instead of dying silently (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).\r\n                    if let Some(sink) = self.viewers.remove(&vid) {\r\n                        sink.evicted.store(true, Ordering::Release);\r\n                    }\r\n                }\r\n                self.stamp_viewer_count();\r\n            }\r\n        }\r\n        // Controller: the SAME non-blocking discipline as a viewer (b4). One\r\n        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\r\n        // slow controller falls behind the ring) unless it has been Full for the\r\n        // whole deadline since its last success → EVICT. Disconnected (writer gone)\r\n        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\r\n        let now = Instant::now();\r\n        let epoch = self.controller_epoch.load(Ordering::Acquire);\r\n        let result = if let Some(c) = self.controller.as_mut() {\r\n            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {\r\n                Ok(()) => {\r\n                    c.last_ok = now;\r\n                    None\r\n                }\r\n                Err(TrySendError::Full(_)) => {\r\n                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {\r\n                        Some(epoch) // stalled past the deadline → bounded detach\r\n                    } else {\r\n                        None // drop this chunk; controller falls behind the ring\r\n                    }\r\n                }\r\n                Err(TrySendError::Disconnected(_)) => Some(epoch),\r\n            }\r\n        } else {\r\n            None\r\n        };\r\n        // Advance the render grid AFTER the fan-out — it must never sit between the\r\n        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\r\n        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\r\n        // snapshots it under the same short log lock only when an attach requests it.\r\n        self.grid.advance(bytes);\r\n        result\r\n    }\r\n\r\n    /// The current controller's identity (`Some(node)` remote / `None` local),\r\n    /// or `None` when undriven. The `by`-keyed ownership logic in\r\n    /// [`resolve_subscribe`] reads this.\r\n    fn controller_by(&self) -> Option<&str> {\r\n        self.controller.as_ref().and_then(|c| c.by.as_deref())\r\n    }\r\n\r\n    /// Whether ANY controller currently drives this session (the `controlled`\r\n    /// any-controller truth — REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). Snapshotted under\r\n    /// the log lock, then converged onto the perch OFF the lock.\r\n    fn has_controller(&self) -> bool {\r\n        self.controller.is_some()\r\n    }\r\n\r\n    /// The live viewer count (the `viewer_count` truth). Snapshotted under the log lock\r\n    /// for the convergence re-assert.\r\n    fn live_viewer_count(&self) -> u32 {\r\n        self.viewers.len() as u32\r\n    }\r\n\r\n    /// Become (or silently re-take) the interactive controller from `from_seq`.\r\n    ///\r\n    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller\r\n    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\r\n    /// inline under the log lock. The writer thread advances the authoritative\r\n    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,\r\n    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\r\n    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\r\n    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\r\n    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\r\n    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\r\n    /// single live writer per connection and a monotonic on-wire seq stream,\r\n    /// [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]). Stamps `driven_by` to the new\r\n    /// controller's identity.\r\n    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {\r\n        // Drop the prior controller sink first (its writer's live loop ends when\r\n        // tx drops), and bump the generation so (a) an in-flight deadline-evict\r\n        // for the old controller can't unseat this one and (b) a prior writer\r\n        // still flushing its initial batch returns before its next frame.\r\n        // Attribution (F-039 legs b+d): the replaced conn's record names its\r\n        // successor, and the new conn accretes the controller role fact.\r\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        if let Some(old) = self.controller.take() {\r\n            old.send.lifecycle_event(\r\n                \"controller-replaced\",\r\n                &format!(\"session={} new_conn={}\", self.session_id, sub.id()),\r\n            );\r\n        }\r\n        sub.describe(&format!(\r\n            \"controller session={} endpoint={} by={}\",\r\n            self.session_id,\r\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint },\r\n            by.as_deref().unwrap_or(\"local\")\r\n        ));\r\n        sub.lifecycle_event(\r\n            \"controller-attach\",\r\n            &format!(\"session={} from_seq={from_seq}\", self.session_id),\r\n        );\r\n        self.controller = None;\r\n        let epoch = self.controller_epoch.fetch_add(1, Ordering::AcqRel) + 1;\r\n        // #6 (ADR-0031): a COLD attach's initial batch is a single synthesized clean\r\n        // repaint of the CURRENT SCREEN (at pseudo-seq `next_seq - 1`) instead of the\r\n        // raw ring — replaying an alt-screen TUI's raw ring into a fresh terminal is\r\n        // exactly the corruption #6/#7/#8 report. A resume-from-floor (`from_seq > 0`)\r\n        // keeps the raw-ring exactly-once re-fetch untouched (B2). Either way the\r\n        // batch is owned by the writer thread (never pushed through the live channel —\r\n        // it must not block the drain); live frames stream raw + in-order afterwards.\r\n        let initial: Vec<(u64, Vec<u8>)> = match self.repaint_initial(from_seq) {\r\n            Some(repaint) => vec![repaint],\r\n            None => self\r\n                .ring\r\n                .iter()\r\n                .filter(|(s, _)| *s >= from_seq)\r\n                .cloned()\r\n                .collect(),\r\n        };\r\n        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\r\n        let writer_send = Arc::clone(&sub);\r\n        let dt = Arc::clone(&self.delivered_through);\r\n        let epoch_gate = Arc::clone(&self.controller_epoch);\r\n        let sid = self.session_id;\r\n        // W2 decouple: the writer publishes its in-flight-write window here so a\r\n        // stall-evict can distinguish a wedged writer from an idle one.\r\n        let write_blocked_since = Arc::new(Mutex::new(None));\r\n        let writer_blocked = Arc::clone(&write_blocked_since);\r\n        let writer = thread::spawn(move || {\r\n            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\r\n        });\r\n        self.controller = Some(ControllerSink {\r\n            tx,\r\n            send: sub,\r\n            by,\r\n            last_ok: Instant::now(),\r\n            write_blocked_since,\r\n            _writer: writer,\r\n        });\r\n        self.stamp_driven_by();\r\n    }\r\n\r\n    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared\r\n    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per\r\n    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its\r\n    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an\r\n    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\r\n    fn clear_controller(&mut self) {\r\n        if self.controller.take().is_some() {\r\n            self.stamp_driven_by();\r\n        }\r\n    }\r\n\r\n    /// B-2 (REQ-CONTROLLER-LIVENESS-REAP): lazily drop a controller whose WRITER\r\n    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\r\n    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\r\n    /// writer exits when a socket write fails (a severed / keepalive-killed controller\r\n    /// conn) or its `tx` drops; while it is parked on `rx.recv()` (a LIVE but idle\r\n    /// controller) it is NOT finished, so a live controller is never touched (the\r\n    /// no-false-reap guard). Reaping here makes the broker report honest truth so the\r","truncated":true,"truncatedBy":"lines","totalLines":5391,"totalBytes":18156,"outputLines":355,"outputBytes":18156,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// Pure divergence decision for the control/viewer stamp convergence\r\n/// (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): does the perch's RECORDED\r\n/// (`driven_by`, `controlled`, `viewer_count`) differ from the live session TRUTH?\r\n/// Returns `(control_write_needed, viewer_write_needed)` — the divergence gate that\r\n/// keeps the per-poll re-assert from writing (fsync) when nothing changed.\r\n// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\nfn stamp_divergence(\r\n    rec_driven_by: Option<&str>,\r\n    rec_controlled: bool,\r\n    rec_viewers: u32,\r\n    want_driven_by: Option<&str>,\r\n    want_controlled: bool,\r\n    want_viewers: u32,\r\n) -> (bool, bool) {\r\n    let control = rec_driven_by != want_driven_by || rec_controlled != want_controlled;\r\n    let viewer = rec_viewers != want_viewers;\r\n    (control, viewer)\r\n}\r\n\r\n/// Converge a perch's control/viewer stamps to the live session truth, OFF any log\r\n/// lock (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). The broker stays the single writer;\r\n/// this closes the STAMP-BEFORE-BIND loss — a picker-created endpoint's spawn stamps\r\n/// `controlled=true` BEFORE the adapter binds its perch, so `mutate_info` returns\r\n/// `NotFound` and the edge stamp is swallowed; the adapter's bind then writes\r\n/// `controlled:false` and no later edge re-stamps, so the endpoint reads uncontrolled\r\n/// forever while driven. This DIVERGENCE-GATED re-assert (on the `KIND_SESSIONS` poll,\r\n/// bounded by the reconcile cadence) writes ONLY on a real difference — no fsync storm.\r\n/// Best-effort: an unbound / corrupt / perch-less endpoint has nowhere to converge yet.\r\n// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\nfn converge_perch_stamps(\r\n    endpoint: &str,\r\n    want_driven_by: Option<&str>,\r\n    want_controlled: bool,\r\n    want_viewers: u32,\r\n) {\r\n    if endpoint.is_empty() {\r\n        return;\r\n    }\r\n    let perch = resolve_perch_path(endpoint, ParentHint::Infer);\r\n    let Some(rec) = spt_store::info::read_info(&perch) else {\r\n        return; // perch not bound yet (the pre-bind window) / corrupt — nothing to gate on\r\n    };\r\n    let (control, viewer) = stamp_divergence(\r\n        rec.driven_by.as_deref(),\r\n        rec.controlled,\r\n        rec.viewer_count.unwrap_or(0),\r\n        want_driven_by,\r\n        want_controlled,\r\n        want_viewers,\r\n    );\r\n    if control {\r\n        let _ = spt_store::info::set_driven_by(&perch, want_driven_by);\r\n        let _ = spt_store::info::set_controlled(&perch, want_controlled);\r\n    }\r\n    if viewer {\r\n        let _ = spt_store::info::set_viewer_count(&perch, want_viewers);\r\n    }\r\n}\r\n\r\nimpl OutputLog {\r\n    fn new(session_id: u64, cap_chunks: usize, endpoint: String, size: (u16, u16)) -> Self {\r\n        OutputLog {\r\n            session_id,\r\n            next_seq: 0,\r\n            ring: VecDeque::new(),\r\n            cap_chunks,\r\n            controller: None,\r\n            controller_epoch: Arc::new(AtomicU64::new(0)),\r\n            viewers: HashMap::new(),\r\n            next_viewer_id: 0,\r\n            endpoint,\r\n            size,\r\n            delivered_through: Arc::new(AtomicU64::new(0)),\r\n            grid: ScreenGrid::new(size.0, size.1),\r\n        }\r\n    }\r\n\r\n    /// The synthesized clean-repaint initial batch (#6, ADR-0031): a single frame\r\n    /// carrying the current screen at pseudo-seq `next_seq - 1`.\r\n    ///\r\n    /// **Scoped to a COLD attach (`from_seq == 0`)** — the `spt rc` / `endpoint run\r\n    /// --attach` case #6 is about (attach to an already-producing TUI). A\r\n    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\r\n    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\r\n    /// exactly-once replay + the strict reject-gap + the irrecoverable-behind\r\n    /// detection untouched (REQ-HAZARD-CONTROLLER-GAP-RESUME / B2). `None` also when\r\n    /// nothing has been produced yet (`next_seq == 0`) → an empty initial batch, so\r\n    /// the first live frame is the contiguous seq 0 (the spawn seat, unchanged).\r\n    fn repaint_initial(&self, from_seq: u64) -> Option<(u64, Vec<u8>)> {\r\n        if from_seq != 0 || self.next_seq == 0 {\r\n            return None;\r\n        }\r\n        Some((self.next_seq - 1, self.grid.render_repaint()))\r\n    }\r\n\r\n    /// Append a freshly-produced output chunk: assign the next `seq`, retain it in\r\n    /// the bounded ring, then FAN OUT to viewers AND the controller — both a SINGLE\r\n    /// non-blocking `try_send`, so the drain NEVER blocks/sleeps here. Returns\r\n    /// `Some(epoch)` when the controller must be EVICTED (stalled past the deadline,\r\n    /// or its writer is gone) so the drain can `mark_controller_gone(epoch)` OFF the\r\n    /// log lock; `None` otherwise.\r\n    ///\r\n    /// b4 (REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE): W1 moved the\r\n    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but\r\n    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to\r\n    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower\r\n    /// than the PTY floods filled its channel, the inline poll throttled the drain,\r\n    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\r\n    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\r\n    /// exactly like a viewer: the drain runs at full PTY speed regardless of\r\n    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\r\n    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\r\n    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\r\n    /// (bounded wedge preserved). The writer still does the blocking socket write +\r\n    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's\r\n    /// single drain thread.\r\n    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\r\n    // [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\r\n    #[must_use]\r\n    /// Coherence clamp for a poison-RECOVERED ring (B-1, doyle ruling; run only by\r\n    /// [`recover_log`] on the recovery path). A panic mid-[`append`] can leave the\r\n    /// ring torn — an over-cap ring (interrupted prune), a last seq not below\r\n    /// `next_seq`, or a non-monotonic front/back — and serving those bytes blind\r\n    /// risks garbage to a subscriber. Cheap-check the ring invariants (counts within\r\n    /// cap, indices ordered and below `next_seq`); on ANY violation RESET the ring\r\n    /// EMPTY (+ loud log) — `next_seq` is left intact so cursors never rewind, and the\r\n    /// lost scrollback self-heals on the next PTY output + repaint. Returns `true`\r\n    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\r\n    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\r\n    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\r\n    fn clamp_or_reset(&mut self) -> bool {\r\n        let within_cap = self.ring.len() <= self.cap_chunks;\r\n        let last_below_next = match self.ring.back() {\r\n            Some(&(last, _)) => last < self.next_seq,\r\n            None => true,\r\n        };\r\n        let ordered = match (self.ring.front(), self.ring.back()) {\r\n            (Some(&(front, _)), Some(&(back, _))) => front <= back,\r\n            _ => true,\r\n        };\r\n        if within_cap && last_below_next && ordered {\r\n            return false;\r\n        }\r\n        let had = self.ring.len();\r\n        self.ring.clear();\r\n        eprintln!(\r\n            \"broker: session {} output ring was torn by a poison-recover \\\r\n             (had {had} chunks, next_seq {}); reset to empty — scrollback self-heals \\\r\n             on the next output [REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\",\r\n            self.session_id, self.next_seq\r\n        );\r\n        true\r\n    }\r\n\r\n    /// The current high-water sequence (the next seq to be appended) — the snapshot\r\n    /// point for the inject leg's settle-gate + echo-verify (REQ-INJECT-MULTILINE-\r\n    /// INTEGRITY). A rising high-water since a baseline == the PTY produced output.\r\n    fn high_water(&self) -> u64 {\r\n        self.next_seq\r\n    }\r\n\r\n    /// Concatenate ring bytes at or after `from_seq` — the output PRODUCED since a\r\n    /// snapshot, used to confirm a settle probe echoed (readiness, Layer 1) and that\r\n    /// a typed payload's head echoed back (integrity, Layer 2). Best-effort: bytes\r\n    /// pruned out of the bounded ring are simply absent (the echo we look for is\r\n    /// recent, well within cap).\r\n    fn bytes_since(&self, from_seq: u64) -> Vec<u8> {\r\n        let mut out = Vec::new();\r\n        for (seq, chunk) in self.ring.iter() {\r\n            if *seq >= from_seq {\r\n                out.extend_from_slice(chunk);\r\n            }\r\n        }\r\n        out\r\n    }\r\n\r\n    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\r\n        let seq = self.next_seq;\r\n        self.next_seq += 1;\r\n        self.ring.push_back((seq, bytes.to_vec()));\r\n        while self.ring.len() > self.cap_chunks {\r\n            self.ring.pop_front();\r\n        }\r\n        let frame = output_envelope(self.session_id, seq, bytes);\r\n        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.\r\n        if !self.viewers.is_empty() {\r\n            let mut evicted: Vec<u64> = Vec::new();\r\n            for (vid, sink) in self.viewers.iter() {\r\n                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {\r\n                    evicted.push(*vid);\r\n                }\r\n            }\r\n            if !evicted.is_empty() {\r\n                for vid in evicted {\r\n                    // Mark eviction BEFORE dropping the sink: the writer's\r\n                    // `rx.recv()` then returns Err (tx dropped) and reads this flag\r\n                    // to emit a KIND_VIEWER_EVICTED marker (skip-to-live, off the\r\n                    // log lock) instead of dying silently (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).\r\n                    if let Some(sink) = self.viewers.remove(&vid) {\r\n                        sink.evicted.store(true, Ordering::Release);\r\n                    }\r\n                }\r\n                self.stamp_viewer_count();\r\n            }\r\n        }\r\n        // Controller: the SAME non-blocking discipline as a viewer (b4). One\r\n        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\r\n        // slow controller falls behind the ring) unless it has been Full for the\r\n        // whole deadline since its last success → EVICT. Disconnected (writer gone)\r\n        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\r\n        let now = Instant::now();\r\n        let epoch = self.controller_epoch.load(Ordering::Acquire);\r\n        let result = if let Some(c) = self.controller.as_mut() {\r\n            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {\r\n                Ok(()) => {\r\n                    c.last_ok = now;\r\n                    None\r\n                }\r\n                Err(TrySendError::Full(_)) => {\r\n                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {\r\n                        Some(epoch) // stalled past the deadline → bounded detach\r\n                    } else {\r\n                        None // drop this chunk; controller falls behind the ring\r\n                    }\r\n                }\r\n                Err(TrySendError::Disconnected(_)) => Some(epoch),\r\n            }\r\n        } else {\r\n            None\r\n        };\r\n        // Advance the render grid AFTER the fan-out — it must never sit between the\r\n        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\r\n        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\r\n        // snapshots it under the same short log lock only when an attach requests it.\r\n        self.grid.advance(bytes);\r\n        result\r\n    }\r\n\r\n    /// The current controller's identity (`Some(node)` remote / `None` local),\r\n    /// or `None` when undriven. The `by`-keyed ownership logic in\r\n    /// [`resolve_subscribe`] reads this.\r\n    fn controller_by(&self) -> Option<&str> {\r\n        self.controller.as_ref().and_then(|c| c.by.as_deref())\r\n    }\r\n\r\n    /// Whether ANY controller currently drives this session (the `controlled`\r\n    /// any-controller truth — REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). Snapshotted under\r\n    /// the log lock, then converged onto the perch OFF the lock.\r\n    fn has_controller(&self) -> bool {\r\n        self.controller.is_some()\r\n    }\r\n\r\n    /// The live viewer count (the `viewer_count` truth). Snapshotted under the log lock\r\n    /// for the convergence re-assert.\r\n    fn live_viewer_count(&self) -> u32 {\r\n        self.viewers.len() as u32\r\n    }\r\n\r\n    /// Become (or silently re-take) the interactive controller from `from_seq`.\r\n    ///\r\n    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller\r\n    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\r\n    /// inline under the log lock. The writer thread advances the authoritative\r\n    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,\r\n    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\r\n    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\r\n    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\r\n    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\r\n    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\r\n    /// single live writer per connection and a monotonic on-wire seq stream,\r\n    /// [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]). Stamps `driven_by` to the new\r\n    /// controller's identity.\r\n    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {\r\n        // Drop the prior controller sink first (its writer's live loop ends when\r\n        // tx drops), and bump the generation so (a) an in-flight deadline-evict\r\n        // for the old controller can't unseat this one and (b) a prior writer\r\n        // still flushing its initial batch returns before its next frame.\r\n        // Attribution (F-039 legs b+d): the replaced conn's record names its\r\n        // successor, and the new conn accretes the controller role fact.\r\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        if let Some(old) = self.controller.take() {\r\n            old.send.lifecycle_event(\r\n                \"controller-replaced\",\r\n                &format!(\"session={} new_conn={}\", self.session_id, sub.id()),\r\n            );\r\n        }\r\n        sub.describe(&format!(\r\n            \"controller session={} endpoint={} by={}\",\r\n            self.session_id,\r\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint },\r\n            by.as_deref().unwrap_or(\"local\")\r\n        ));\r\n        sub.lifecycle_event(\r\n            \"controller-attach\",\r\n            &format!(\"session={} from_seq={from_seq}\", self.session_id),\r\n        );\r\n        self.controller = None;\r\n        let epoch = self.controller_epoch.fetch_add(1, Ordering::AcqRel) + 1;\r\n        // #6 (ADR-0031): a COLD attach's initial batch is a single synthesized clean\r\n        // repaint of the CURRENT SCREEN (at pseudo-seq `next_seq - 1`) instead of the\r\n        // raw ring — replaying an alt-screen TUI's raw ring into a fresh terminal is\r\n        // exactly the corruption #6/#7/#8 report. A resume-from-floor (`from_seq > 0`)\r\n        // keeps the raw-ring exactly-once re-fetch untouched (B2). Either way the\r\n        // batch is owned by the writer thread (never pushed through the live channel —\r\n        // it must not block the drain); live frames stream raw + in-order afterwards.\r\n        let initial: Vec<(u64, Vec<u8>)> = match self.repaint_initial(from_seq) {\r\n            Some(repaint) => vec![repaint],\r\n            None => self\r\n                .ring\r\n                .iter()\r\n                .filter(|(s, _)| *s >= from_seq)\r\n                .cloned()\r\n                .collect(),\r\n        };\r\n        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\r\n        let writer_send = Arc::clone(&sub);\r\n        let dt = Arc::clone(&self.delivered_through);\r\n        let epoch_gate = Arc::clone(&self.controller_epoch);\r\n        let sid = self.session_id;\r\n        // W2 decouple: the writer publishes its in-flight-write window here so a\r\n        // stall-evict can distinguish a wedged writer from an idle one.\r\n        let write_blocked_since = Arc::new(Mutex::new(None));\r\n        let writer_blocked = Arc::clone(&write_blocked_since);\r\n        let writer = thread::spawn(move || {\r\n            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\r\n        });\r\n        self.controller = Some(ControllerSink {\r\n            tx,\r\n            send: sub,\r\n            by,\r\n            last_ok: Instant::now(),\r\n            write_blocked_since,\r\n            _writer: writer,\r\n        });\r\n        self.stamp_driven_by();\r\n    }\r\n\r\n    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared\r\n    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per\r\n    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its\r\n    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an\r\n    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\r\n    fn clear_controller(&mut self) {\r\n        if self.controller.take().is_some() {\r\n            self.stamp_driven_by();\r\n        }\r\n    }\r\n\r\n    /// B-2 (REQ-CONTROLLER-LIVENESS-REAP): lazily drop a controller whose WRITER\r\n    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\r\n    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\r\n    /// writer exits when a socket write fails (a severed / keepalive-killed controller\r\n    /// conn) or its `tx` drops; while it is parked on `rx.recv()` (a LIVE but idle\r\n    /// controller) it is NOT finished, so a live controller is never touched (the\r\n    /// no-false-reap guard). Reaping here makes the broker report honest truth so the\r\n…\n}","startLine":619,"lineNumbers":[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,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,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,null,1369]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6009,"totalBytes":18156,"outputLines":355,"outputBytes":18156,"shownRange":{"start":619,"end":973},"nextOffset":974}}},"isError":false,"timestamp":1784195789304}}
{"type":"message","id":"38e67cac","parentId":"45a5b1ec","timestamp":"2026-07-16T09:56:29.337Z","message":{"role":"toolResult","toolCallId":"call_JG0LmRf32ILv5dtUUpU30Sqi|fc_008390f508d1d09b016a58aacd12b8819aad53e758be42c469","toolName":"read","content":[{"type":"text","text":"    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n…\n        recover(&self.sessions).insert(\n…\n            HostedSession {\n…\n                adapter: req.adapter.clone(),\n                install_dir: req.install_dir.clone().map(std::path::PathBuf::from),\n            },\n        );\n\n        let resp = Spawned {\n            session_id: id,\n            pid,\n        };\n        let frame = Envelope::new(\n            KIND_SPAWNED,\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\n        );\n        send_frame(send, &frame);\n        Ok(id)\n    }\n\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\n    /// self-kick guard.\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n        let req: SubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\n        // Clone the log Arc out from under the sessions lock, then resolve under\n        // the log lock — never hold both locks at once.\n        //\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\n        // the per-session exit-waiter holds for the WHOLE life of a live child\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\n        // post-removal subscribe gets a prompt \"no such session\" error — both\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\n        // the rest.\n        let log = {\n            let sessions = recover(&self.sessions);\n            let h = sessions\n                .get(&req.session_id)\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\n            Arc::clone(&h.log)\n        };\n        let outcome = recover_log(&log).resolve_subscribe(\n            Arc::clone(send),\n            req.from_seq,\n            req.intent,\n            req.by,\n        );\n        send_frame(send, &subscribed_envelope(req.session_id, outcome));\n        Ok(req.session_id)\n    }\n\n    /// Release this connection's controller/viewer role on a session\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\n    /// a no-op if a newer controller already took over, or the session is gone.\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\n        let log = {\n            let sessions = recover(&self.sessions);\n            match sessions.get(&req.session_id) {\n                Some(h) => Arc::clone(&h.log),\n                None => {\n                    eprintln!(\"DBG unsubscribe: no session {}\", req.session_id);\n                    return Ok(());\n                }\n            }\n        };\n        recover_log(&log).detach_if(send);\n        Ok(())\n    }\n\n    /// Inject input bytes into a session's PTY.\n    ///\n    /// With an `op_id` the write is routed through the broker-owned\n    /// [`EffectJournal`] for exactly-once delivery: a replay of the same\n    /// `(session_id, op_id)` after a brain crash is deduped (the PTY is not\n    /// written twice), and either outcome — fresh apply or dedup — is acked back\n    /// with an [`crate::msg::KIND_APPLIED`] frame so the brain can retire the\n    /// effect (B5 / KNOWN-HAZARDS 7.2). Without an `op_id` it is the legacy\n    /// direct write (no dedup, no ack).\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n\n[Showing lines 3424-3513 of 6009. Use :3514 to continue]"}],"details":{"truncation":{"content":"                adapter: req.adapter.clone(),\r\n                install_dir: req.install_dir.clone().map(std::path::PathBuf::from),\r\n            },\r\n        );\r\n\r\n        let resp = Spawned {\r\n            session_id: id,\r\n            pid,\r\n        };\r\n        let frame = Envelope::new(\r\n            KIND_SPAWNED,\r\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\r\n        );\r\n        send_frame(send, &frame);\r\n        Ok(id)\r\n    }\r\n\r\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\r\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\r\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\r\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\r\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\r\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\r\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\r\n    /// self-kick guard.\r\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\r\n        let req: SubscribeReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\r\n        // Clone the log Arc out from under the sessions lock, then resolve under\r\n        // the log lock — never hold both locks at once.\r\n        //\r\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\r\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\r\n        // the per-session exit-waiter holds for the WHOLE life of a live child\r\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\r\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\r\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\r\n        // post-removal subscribe gets a prompt \"no such session\" error — both\r\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\r\n        // the rest.\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            let h = sessions\r\n                .get(&req.session_id)\r\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\r\n            Arc::clone(&h.log)\r\n        };\r\n        let outcome = recover_log(&log).resolve_subscribe(\r\n            Arc::clone(send),\r\n            req.from_seq,\r\n            req.intent,\r\n            req.by,\r\n        );\r\n        send_frame(send, &subscribed_envelope(req.session_id, outcome));\r\n        Ok(req.session_id)\r\n    }\r\n\r\n    /// Release this connection's controller/viewer role on a session\r\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\r\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\r\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\r\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\r\n    /// a no-op if a newer controller already took over, or the session is gone.\r\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            match sessions.get(&req.session_id) {\r\n                Some(h) => Arc::clone(&h.log),\r\n                None => {\r\n                    eprintln!(\"DBG unsubscribe: no session {}\", req.session_id);\r\n                    return Ok(());\r\n                }\r\n            }\r\n        };\r\n        recover_log(&log).detach_if(send);\r\n        Ok(())\r\n    }\r\n\r\n    /// Inject input bytes into a session's PTY.\r\n    ///\r\n    /// With an `op_id` the write is routed through the broker-owned\r\n    /// [`EffectJournal`] for exactly-once delivery: a replay of the same\r\n    /// `(session_id, op_id)` after a brain crash is deduped (the PTY is not\r\n    /// written twice), and either outcome — fresh apply or dedup — is acked back\r\n    /// with an [`crate::msg::KIND_APPLIED`] frame so the brain can retire the\r\n    /// effect (B5 / KNOWN-HAZARDS 7.2). Without an `op_id` it is the legacy\r\n    /// direct write (no dedup, no ack).\r\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\r","truncated":true,"truncatedBy":"lines","totalLines":2586,"totalBytes":4421,"outputLines":90,"outputBytes":4421,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n…\n        recover(&self.sessions).insert(\n…\n            HostedSession {\n…\n                adapter: req.adapter.clone(),\r\n                install_dir: req.install_dir.clone().map(std::path::PathBuf::from),\r\n            },\r\n        );\r\n\r\n        let resp = Spawned {\r\n            session_id: id,\r\n            pid,\r\n        };\r\n        let frame = Envelope::new(\r\n            KIND_SPAWNED,\r\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\r\n        );\r\n        send_frame(send, &frame);\r\n        Ok(id)\r\n    }\r\n\r\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\r\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\r\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\r\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\r\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\r\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\r\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\r\n    /// self-kick guard.\r\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\r\n        let req: SubscribeReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\r\n        // Clone the log Arc out from under the sessions lock, then resolve under\r\n        // the log lock — never hold both locks at once.\r\n        //\r\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\r\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\r\n        // the per-session exit-waiter holds for the WHOLE life of a live child\r\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\r\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\r\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\r\n        // post-removal subscribe gets a prompt \"no such session\" error — both\r\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\r\n        // the rest.\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            let h = sessions\r\n                .get(&req.session_id)\r\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\r\n            Arc::clone(&h.log)\r\n        };\r\n        let outcome = recover_log(&log).resolve_subscribe(\r\n            Arc::clone(send),\r\n            req.from_seq,\r\n            req.intent,\r\n            req.by,\r\n        );\r\n        send_frame(send, &subscribed_envelope(req.session_id, outcome));\r\n        Ok(req.session_id)\r\n    }\r\n\r\n    /// Release this connection's controller/viewer role on a session\r\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\r\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\r\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\r\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\r\n    /// a no-op if a newer controller already took over, or the session is gone.\r\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            match sessions.get(&req.session_id) {\r\n                Some(h) => Arc::clone(&h.log),\r\n                None => {\r\n                    eprintln!(\"DBG unsubscribe: no session {}\", req.session_id);\r\n                    return Ok(());\r\n                }\r\n            }\r\n        };\r\n        recover_log(&log).detach_if(send);\r\n        Ok(())\r\n    }\r\n\r\n    /// Inject input bytes into a session's PTY.\r\n    ///\r\n    /// With an `op_id` the write is routed through the broker-owned\r\n    /// [`EffectJournal`] for exactly-once delivery: a replay of the same\r\n    /// `(session_id, op_id)` after a brain crash is deduped (the PTY is not\r\n    /// written twice), and either outcome — fresh apply or dedup — is acked back\r\n    /// with an [`crate::msg::KIND_APPLIED`] frame so the brain can retire the\r\n    /// effect (B5 / KNOWN-HAZARDS 7.2). Without an `op_id` it is the legacy\r\n    /// direct write (no dedup, no ack).\r\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\r","startLine":3225,"lineNumbers":[3225,null,3413,null,3415,null,3424,3425,3426,3427,3428,3429,3430,3431,3432,3433,3434,3435,3436,3437,3438,3439,3440,3441,3442,3443,3444,3445,3446,3447,3448,3449,3450,3451,3452,3453,3454,3455,3456,3457,3458,3459,3460,3461,3462,3463,3464,3465,3466,3467,3468,3469,3470,3471,3472,3473,3474,3475,3476,3477,3478,3479,3480,3481,3482,3483,3484,3485,3486,3487,3488,3489,3490,3491,3492,3493,3494,3495,3496,3497,3498,3499,3500,3501,3502,3503,3504,3505,3506,3507,3508,3509,3510,3511,3512,3513]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6009,"totalBytes":4421,"outputLines":90,"outputBytes":4421,"shownRange":{"start":3424,"end":3513},"nextOffset":3514}}},"isError":false,"timestamp":1784195789337}}
{"type":"message","id":"e49da12b","parentId":"38e67cac","timestamp":"2026-07-16T09:57:04.068Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Formulating hypotheses on brain respawn stall**\n\n**Analyzing refresh command and connection reuse**\n\n**Tracing brain dispatcher lifecycle and streams**"},{"type":"thinking","thinking":"**Analyzing controller detach and recovery behavior**\n\n**Searching cursor usage and refresh commands**"},{"type":"toolCall","id":"call_Mlil1vPu96pFBlBbSvqL5vT9|fc_008390f508d1d09b016a58aaeffa08819aba2505a69f5d628b","name":"grep","arguments":{"i":"Tracing daemon refresh boundary","pattern":"Daemon.*Refresh|Refresh.*Daemon|cmd_daemon_refresh|daemon refresh|KIND_BRAIN_RESTART|brain_restart|resume_session_cursors|resume_sessions\\(","path":"crates/spt/src;crates/spt-daemon/src;crates/spt/tests;crates/spt-daemon/tests","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_7vvJkFJbHVanpWRPEatr9XOh|fc_008390f508d1d09b016a58aaeffb28819aba85e2cb06566622","name":"read","arguments":{"i":"Reading current brain startup resume","path":"crates/spt-daemon/src/brainproc.rs","selector":"150-320"}},{"type":"toolCall","id":"call_rWFAzgClh7FKjCoKzpFrv5wx|fc_008390f508d1d09b016a58aaeffb30819a86eb2d5c66438167","name":"grep","arguments":{"i":"Finding broker session task spawn","pattern":"drain|exit-waiter|thread::Builder|thread::spawn|take_reader|stamp_reaped|sessions.remove","path":"crates/spt-daemon/src/broker.rs","selector":"3200-3425","case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_hS1bI33yTuKmtR6Xs3y0bvB9|fc_008390f508d1d09b016a58aaeffb34819a91bba4521813b2c1","name":"read","arguments":{"i":"Reading broker session tasks","path":"crates/spt-daemon/src/broker.rs","selector":"3220-3440"}},{"type":"toolCall","id":"call_lG8Smf0S7EisZCk4seAtrSsc|fc_008390f508d1d09b016a58aaeffb3c819aaed3a9a786eb566d","name":"grep","arguments":{"i":"Indexing lifecycle regression tests","pattern":"^fn [a-zA-Z0-9_]+\\(|^#\\[(test|cfg)|\\[int->|\\[unit->","path":"crates/spt-daemon/tests/brain_decouple.rs;crates/spt-daemon/tests/brain_resume_conn_deadlock.rs;crates/spt-daemon/tests/conn_blackhole_lifecycle.rs;crates/spt-daemon/tests/false_promote.rs;crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/tests/attach_wedge_e2e.rs;crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/control_stamp_lifetime.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_jgmyp12ejDxEBLFrsjXr6NXR|fc_008390f508d1d09b016a58aaeffb40819aa7280d58a852370b","name":"glob","arguments":{"i":"Finding wedge RCA documents","path":"docs/**/*{WEDGE,wedge,RCA,SME,CODEREAD,RULING}*.md","hidden":true,"gitignore":true,"limit":200}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":43628,"output":1182,"cacheRead":152064,"cacheWrite":0,"totalTokens":196874,"reasoningTokens":669,"cost":{"input":0.21814000000000003,"output":0.03546,"cacheRead":0.076032,"cacheWrite":0,"total":0.32963200000000004}},"stopReason":"toolUse","timestamp":1784195789378,"responseId":"resp_008390f508d1d09b016a58aacdac2c819a9469bcb4931c0507","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_008390f508d1d09b016a58aad2a510819aa31fcd35ff767931","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKrgr--uMAu7SDRO-aqU0C68vTuw6RbCpfPMRJW_kcvUzliBJ7992mcG5asNdS5EVXdjeURUHmHJrj5UGUUc35JOnOBg2bkVi2M5UmU2Ny-k0S-1OEfRPG3mDjhyCvRhw0LNwouuAvGLM3N9qzbgPZ0b7GA9RjQey0JydKn1blJrmexB8wtXJkJn82W_PLCVsblJnqumXjy3Kd7zkzTDNiL9UsnFj2LX8C1sc8-gxXc4sS88sD_l2kCMaM29i7p4BjGeCdUcQG7UAw2ErFvYYfjv-SXGuwre1Ld--6rHO2L67lQFX6xuSxHwGtxSZhXfmGlkqPSu0yskIPXTWuEXErFMgH3KGTBHPdpIClPssJmhyTpfKTTqL707YBIV5Ua2APgIN6B3jEQG1oxe_akVij7xNtPkzHM7DhrAl9NUaQCQLk8W2pGHnygMYiqCHF-w0G_RQMEdDBg_ESTuyrubArao_NH6efupl467qMKLLgmp1gNHWQCCY4UuTyKlUYkH3mGgDNTkfGdNVodjo8wUwK8uTggZLQ_t6iFWvrtIu803yVPgkH_Tbf16yWVJtwBmRr1CAL1JuKL0C3HeAMJ3-DZBIBkvGl9WJ5J96_4BrwUumPIdEkFo7uCprQ-T75zidu5drcuJaDTPcT0v6HKX0V3eAtVaDwCDY4Oh94C9IYHrL6SV-0d9CBZQIu-RjI3qgL5PEpaDJ-bCG3gwPOEql0fUCzJcjw-AqefimniS_8kO7nGj7sECsuVWXy4xhI3ZD1DgKpPRid_KoQy0i-6g-MdpPIh3fulJpvlaBivaTKrcNM1Hx6gZnS__cqx6OD9mxyoXjJcN5r72_2JaUS_LFkInlFNYsaWqcVoMVtXZ0hOKzQneC4cT_AzVN4Xirdg7gi-Ut9XiKb8F83w4yzpr6qJbIOB-ldLF2b3AEUzzv2OGqS7Gy6LU_BisS8R53KouxqQGsJBSpwp_ocnhgEi6f3UZe004GO9KP8NFOuk46Zc1HSGcdp8PcCY_5EF9yt-gLFHmGfleVaLuWTTq_zheJFNOEpRfZXxXsFnq10PYu-c9XRspIJd_lvDy7Fzhi6LUtgpzyAsAtBPm8mxuyWAB7fuw9jsOEu-CvXuSNJ3-oOZ_6DUx7jPgMg79FqPOUAc0vF34Pr1Uy10rDkUnONhzn-jvxaPD3oLqF93slhvwh4pi2BO8-Vm5BG_lYXGJLIwwGb2kVD2futGObzdqnXajL86__eBj4pHPGW8la5VRXrIdqk0jKrg7sFncVqQO2loYuxSkM0Ntv-CBFpONIdJS3IQB7RU1JRNmAUr0VDOkrZ4jdhFZNch6mi4BvNARo3tY-eqlV_5jTgvG-BH6AuNWYUxQJq37gTMn_N-Hpk6P8_nb6Vd9RRooviQkiCDALwmupn2vuS1iriixcs1Sud_v4l490vXwmx0WSEi7dIa1HackrBFfZG7NmCObkmxLNaQo7wJtcsSnLcF_PgMb-sFYVlzPhUr5hbxhsXg-ruwGwyH7v1A97nOEq1DCrgifcriCzRjHvFmQBaVY1GcAY6eziucWN_1jOhZyLYS0Qa4tFgc8wxhHUN06uNKA9_OxQBtY0hbwrPMdgmNFBE8GkxCOrPh9rJMT-OuHGtyETMuLJI2DtmxqEKiFjrWmZX_IgZFoB36ikO9blbMYZdMgo21MMWOzjYRpf_clRh9UpLIYegCZyA1IiaGwxDW2ceNwZ6wbqn5QBnS4UiDn05ZOM_NqGRxUx8H-a3wpdL9zqYTpPYBO-3UGtOyuSgWAXwshd6w43MDktKNGbq1CpVOTYVrw2WTRwDMG2p9CUj_8BgADthJdTkTOEw7mdH36Jypt_PFq3_icnU6pGwCS9DWKTKba52lFwW-lgNTiSENuPjoqGCKMXq3cQXttaX21Ppo-s0qDmiJSw6xcfJ3h2ml6vj5Ws5fljhb7KcldwGktWljPZagNDo612R_TKhKPUyIoxHQdmF_efeRO1kbjqft7DIGnHtIf-h3kvS6vzGXGpY_KrqmWWLGkznyNPHbowA8NY9wCV_9GOhovYLLWGmtbvhrwDbxiXE875aTx1bJPd6NMnQihrXcSnNSKDNF71Kf2gqawaVtcI9mOONMmV97V71fcX95CAXsypco6rsEsEUglVTmUty-2EtzVf7avPInF6Jy2eg4Z7zsrouoEx6owRgySBVWM9w8H9T6ttCxFSERiVZA6mtK9TNx5yf1Sn7HHrt4XBhCqNbvWSdQW0V_ynPVz7uJS4IjDPpRgkUCJS-47sCHKbd_0eaufLCUe4pgAMNihz_zEIORh5RNHh1Ko-wzIF4zlPBWd5-gEezkeWAHCdvR8GmRMDoqWh4upzTAHlKqL-fWutUlNgPfJpYR7Qua705irmmF5ZsyJcZmuQehyB_Of3Wq0oLN98VOK3IzXnMkF4f37FznVq_X0y738NUfXE-vW6NoK2Dz0kEkPJN-C-ljb6X_eMJloJNuPXOw4_gWqN64YBy5RFIKs-uGUXzXb4yz4i31SknU-XO7LikheoBcgTpqDmGadE81RA_R-SI_rdRTvc3xeE2Lg4r1VVpsAT14X1kRUvAH-zcZbMlXkIKoESOV2qpJa-cjSs1YdtUhqTSQfAm6dR6mdyZ6aOsSnUf0FJFUtAGDtkfMswjr9dbcVrW_c_1E7sUwc5MJi0xnrNajX6VMnuhSn6yl9LPHU9Vyh7K5tyBDbD2elWC_5sHR8-szROteJFx3YWDPRDGcHWhdO9aPSw5MeZ_3cQNvbW4s3TOrbDdbbto3cr1SLiJlR50jb4myVL4fDRBqA2QENTjaWQEt-ZolWK0ke8__BpCqILHmM5FE36vi6RuLK1NsXcs1Nza3Rzmnqd_VgYivEsGghxKLBWjg0R3anjRlIUfR5va3XNiG5lFoW4S4f-gMjwOiE5xuXyGyLLi0P6eJ8q0gJDKuzawwnt-E6C3z_9NHS8cBRayxjybRiqOiGRl6BvIPRvAf1iDsgXybPi6qnTg3Plsykt4IYn2sOGS5SM7G1lusV-nd09o6mi9uaSBMSsT92ocH-uGc448moh8obM1ZNj7O2nUEeDsctvl3GqwnunsF_xP4T_7W_XR3_NrCr2So6REqZf1bGDwxQyXcCzb3ppgNJhUOv4AH_quKiJJaSmdGMoJF2bKRMzP6X7MOmmYgyrPZPnN9g_Nc3_QBnr2nvrAz5UmgeYT0GAi9Z2SsD3E0TifPUuHZKbMrEBsdF-hnfuvO0G2Dn5QUz58j7-Co6KuOWtpyBKQ2I5FHwWaL4_ZrxEbHvKyUkvQkFtmbeI5yjBc21kfurXPbHqiQFtMsjgfF1uD-9fH4Pv5fDt2ZYtCVbrmYqMf72rYX6O-vopp2-r2Q1KRpiBoXtVNXiwggtFl0Pghd5dQ1_5jP6boHlPM4NRX-hJ95JKlOYtiFBiiMzR93d86p85EJv5abDQvRstgNs80V09Qe7M0rjHNeIJfnr25etWKKfRNRUgdxvew-IahUMFU9niLCLXyEizGIN84PEhaDUkorVarCpTKOiYCObjPAerpztJZDdG_Mj-qwFJB5bqg_BIchnaSyqfwOmo1_rz3BMTuDgTYinlzysQdBgwSVjy5kmCBVXnzqgYtt99yA8y51xta_MdQTOommZp8moB0DtmqtQEUuhOyl5Vqz55ZMMNHMqxrKDrkIXF1blv6i5pqHgWrbWZd5lx7sNazkD5Tvq_tL0UQiWTyJitu-iJBGRxiTebVTaE99q-W5fcHYPUl33dltoc8KaBRHK_ir2cwxTnCMBVXZxmUS9Tsdd3eBUVz7mzW3c6-BnU16l8b2Z_y7TBZVFP_G7ST2JF8JGZS11UuNHZhcghICAtvfj2z_LX0ggeT35hpzAuhRif_zquKbTJBR6ZoEQkolfqtkkdoLmelwUNr-l4GPf14lhXFTaOUmTpAe6nWZPfUzbKAzFfVAI0yqQTgNCSOI-NA3jZg6b3d-xr52GcheezAZxr6Y0klQVNcLbCVgAgbvQ3QaldARukEHJ-aXmYqcc0M-bCCaFUPc80CDtl17OJMOolYe0TPcjaZLkUcA7yT9V-jopOEdf082kbOUd60Jc2z875XIgiylfQxfafaBUFhGI1xqt5ps8_4cAw95fdpEhvLZI6N47V1j1w6lrj60qrypXeQ5MOOwFI9pBacYoPvz9zw4oTpxQNo59nJaB5XNF3lzAfSY9MYvtSlAvl8wjuuPsJMeWt0Puy4_w3J_1mq-KPmdpGeUqlCyL0ob9R9U6L5xqxgksc5NSVgYnlYrWHlEfjDf0oi3bbhaGy8QSwNOVvF0seEJvCweQAv-N2Gih6eoCBcNls32PFhy93l97REEjy6gnk_JOA2fqcAlqJTIlK3g2IsAZSIFWE2cKRthOa9QBOnqQRbzIpLfH7WqT_qq2g_zKATcZLoDthC1W9igl7av9Yz2GJ1AVRTEyQvk=","summary":[{"type":"summary_text","text":"**Formulating hypotheses on brain respawn stall**"},{"type":"summary_text","text":"**Analyzing refresh command and connection reuse**"},{"type":"summary_text","text":"**Tracing brain dispatcher lifecycle and streams**"}]},{"id":"rs_008390f508d1d09b016a58aae0e0ec819a8623b565c341ffb1","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKrv0j8huj3BuNd_jQHxFdQIehZ2T9QUnomV9JeR8hIxhlt0fIYLC24qY1-7BKTUMeyGdCizDRg_naJZ3-_IMfMuRX4rZlaLoZpKybgqt6_1ARxgOi2-oFM6Oha5X2xvw3uWwTtdNznW3UJjGZmnJtMqSE0InXxQnIjF40dO2wQ_GP54UJTKfjpaFr67m5FLDen1b94Xe6Y-jb1kj3lLioSDW8WCs5mEjDBGA6op1NaYlwUV10j4-DqJ2pTJx0qaKhVbRqijKftmspVytCUUK-oojtPUS8kAaCSUWbMC-Nye2QAzqKhCm1J3_0hhyXX4o-7okZJWdiK9I7owMAmCSF56WCtCTg3bg_xI1u5wALkHhXxCXYgvBHGbDSdv4nrHo5qArwNUgKughC3gVyHT8Gan3w2R96U_rfVAWuooMkWVUFZjZLjJNtBHW0bkibSCiiBWkpWgTzrUE6TDBJTEVt1_biiiFwlR1Wk9zjZhpgy9UkFeruR9FUMjHjcFmbDGNqKlv5T9Z073WtidhXwVt2JdDVtw34abdQ4a7vo22Ym6Z2u8Iyq3BFOiwxJ1lfN61BUSsJfXvMO4QdE1ikMn2ibrsUjR9OeXvTxlIBoXTGOi0jYxPBrF_L7pLXXOqa88clGCzrq2iFnuXXARBU-JTARIhCwKwULAiZD2yyOqqGqyNvoD4S8cvX-Fpa5wbBAUCmUZyxalIbxdJKKiw732C6sRRT7TqUqQyb9y1-v1uHjTHE4UYjGSmUGp9qAxP2bAf_3Pz0qlxaerVt4c-fPEOKfgfHRMAMkLojbHbafrodvzWcH7n7-ecU6gDZk9x1gc1BBd7KTcOpkefHxvxYHkJdVRK6NypJt5JmlIYj1p-ZuA0OCPy8nEiYWTkMeqMaEU4qZhjhut3yBiMM0weqPINgnkd-3GtHF8NnwcBhszW4z71--pC49rFs8Nuk3Ws0YCYDJugSKFdkn0pwFRZ4BASXERcEXFs0G-oSitrhO0ePXUIzxnat_ClhrLl2z4ohJKH4bzqmVriDxd7Cgx_auv9yZ9zKed7WPyWNxC0yxxJwdQ4pE9gVyPr4AI1jo1R7k6Wae6nI3jGCCL_l3hK15Prkr_tDgRAuuDlng2QhLm9_tDActDySqBBHCui6XBHJQFBTc3RcRvnsEcDZ5RKMpICglVYI45v5WO-_eVqje1-Mw4UNnGrnVmFH46ZNG8rhtC4nh3b8uyfqDqwAR-J6Mus_sTV08oR7lwh9-8NrnhSGVmUBu1BrhFz9B_EMe_PDKxG_YSRYC-AAcYBz7CZQmEKp1j2Bo75oV6f11uCD6DR84rUAZ5DlNe_KPhU-7cFyjnOs3PcAfzN2QUSytbW5-CSlHIEMDBkXSryIYNv8317zfj7C7kKseAxCd3bNdig2PxSGPAut2yNJ3yQS5SH2K7hg3YD2DzdG-7vbLTpOiIJEyEsysGRqhVkwJIjHwarm908fuuKCirb7VtyKQppesZ91c30yIxJ22rTJ7LgQMe8oMJThCCVFz2fp48ZXKC9XCPk8xHftpNHSKxxCBpvUyzDSKWpht_vz0MZnLJOEYfzkTE2PXiCMXDyvCOshJteBIR-i9ETV3LH6LKVw36QErGQroXiPZvBwYbgN6MBcAJ3rN8UovSOSYMy5jDGrbWqIriXipYKI7jufvTr_yzfL5W3aIBiZRY8y2QrWA1dYN29UIe1ZQAN1RGzRpz3b_EQzvXCXlHsLruTUXJKe_m0tHf8lK4a8bZuvaqA1h0DRMK0tOhVOQpJuTTeuIY-uTVf4ELR0zUhM1kfb3mI6CEIfR5bf9r3vAWe2JntPqWEgt_lJxctpTdGuwP77X3fl9RBAraDGgg","summary":[{"type":"summary_text","text":"**Analyzing controller detach and recovery behavior**"},{"type":"summary_text","text":"**Searching cursor usage and refresh commands**"}]},{"id":"fc_008390f508d1d09b016a58aaeffa08819aba2505a69f5d628b","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing daemon refresh boundary\",\"pattern\":\"Daemon.*Refresh|Refresh.*Daemon|cmd_daemon_refresh|daemon refresh|KIND_BRAIN_RESTART|brain_restart|resume_session_cursors|resume_sessions\\\\(\",\"path\":\"crates/spt/src;crates/spt-daemon/src;crates/spt/tests;crates/spt-daemon/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_Mlil1vPu96pFBlBbSvqL5vT9","name":"grep"},{"id":"fc_008390f508d1d09b016a58aaeffb28819aba85e2cb06566622","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading current brain startup resume\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"150-320\"}","call_id":"call_7vvJkFJbHVanpWRPEatr9XOh","name":"read"},{"id":"fc_008390f508d1d09b016a58aaeffb30819a86eb2d5c66438167","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding broker session task spawn\",\"pattern\":\"drain|exit-waiter|thread::Builder|thread::spawn|take_reader|stamp_reaped|sessions.remove\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"3200-3425\",\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_rWFAzgClh7FKjCoKzpFrv5wx","name":"grep"},{"id":"fc_008390f508d1d09b016a58aaeffb34819a91bba4521813b2c1","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker session tasks\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"3220-3440\"}","call_id":"call_hS1bI33yTuKmtR6Xs3y0bvB9","name":"read"},{"id":"fc_008390f508d1d09b016a58aaeffb3c819aaed3a9a786eb566d","type":"function_call","status":"completed","arguments":"{\"i\":\"Indexing lifecycle regression tests\",\"pattern\":\"^fn [a-zA-Z0-9_]+\\\\(|^#\\\\[(test|cfg)|\\\\[int->|\\\\[unit->\",\"path\":\"crates/spt-daemon/tests/brain_decouple.rs;crates/spt-daemon/tests/brain_resume_conn_deadlock.rs;crates/spt-daemon/tests/conn_blackhole_lifecycle.rs;crates/spt-daemon/tests/false_promote.rs;crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/tests/attach_wedge_e2e.rs;crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/control_stamp_lifetime.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_lG8Smf0S7EisZCk4seAtrSsc","name":"grep"},{"id":"fc_008390f508d1d09b016a58aaeffb40819aa7280d58a852370b","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding wedge RCA documents\",\"path\":\"docs/**/*{WEDGE,wedge,RCA,SME,CODEREAD,RULING}*.md\",\"hidden\":true,\"gitignore\":true,\"limit\":200}","call_id":"call_jgmyp12ejDxEBLFrsjXr6NXR","name":"glob"}]},"duration":34676.28579999972,"ttft":5243.269299999811,"contextSnapshot":{"promptTokens":195692,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Mlil1vPu96pFBlBbSvqL5vT9|fc_008390f508d1d09b016a58aaeffa08819aba2505a69f5d628b","toolName":"grep","startedAt":"2026-07-16T09:57:04.069Z","args":{"path":"crates/spt/src;crates/spt-daemon/src;crates/spt/tests;crates/spt-daemon/tests"},"intent":"Tracing daemon refresh boundary"},"id":"e9bebe07","parentId":"e49da12b","timestamp":"2026-07-16T09:57:04.069Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_7vvJkFJbHVanpWRPEatr9XOh|fc_008390f508d1d09b016a58aaeffb28819aba85e2cb06566622","toolName":"read","startedAt":"2026-07-16T09:57:04.069Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading current brain startup resume"},"id":"a7a39bb0","parentId":"e9bebe07","timestamp":"2026-07-16T09:57:04.069Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_rWFAzgClh7FKjCoKzpFrv5wx|fc_008390f508d1d09b016a58aaeffb30819a86eb2d5c66438167","toolName":"grep","startedAt":"2026-07-16T09:57:04.069Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Finding broker session task spawn"},"id":"b812d62c","parentId":"a7a39bb0","timestamp":"2026-07-16T09:57:04.069Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_hS1bI33yTuKmtR6Xs3y0bvB9|fc_008390f508d1d09b016a58aaeffb34819a91bba4521813b2c1","toolName":"read","startedAt":"2026-07-16T09:57:04.069Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker session tasks"},"id":"50a7f27f","parentId":"b812d62c","timestamp":"2026-07-16T09:57:04.069Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_lG8Smf0S7EisZCk4seAtrSsc|fc_008390f508d1d09b016a58aaeffb3c819aaed3a9a786eb566d","toolName":"grep","startedAt":"2026-07-16T09:57:04.069Z","args":{"path":"crates/spt-daemon/tests/brain_decouple.rs;crates/spt-daemon/tests/brain_resume_conn_deadlock.rs;crates/spt-daemon/tests/conn_blackhole_lifecycle.rs;crates/spt-daemon/tests/false_promote.rs;crates/spt/…"},"intent":"Indexing lifecycle regression tests"},"id":"6f3a82dc","parentId":"50a7f27f","timestamp":"2026-07-16T09:57:04.069Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_jgmyp12ejDxEBLFrsjXr6NXR|fc_008390f508d1d09b016a58aaeffb40819aa7280d58a852370b","toolName":"glob","startedAt":"2026-07-16T09:57:04.069Z","args":{"path":"docs/**/*{WEDGE,wedge,RCA,SME,CODEREAD,RULING}*.md"},"intent":"Finding wedge RCA documents"},"id":"fb2f74f3","parentId":"6f3a82dc","timestamp":"2026-07-16T09:57:04.069Z"}
{"type":"message","id":"a5113aff","parentId":"fb2f74f3","timestamp":"2026-07-16T09:57:04.076Z","message":{"role":"toolResult","toolCallId":"call_jgmyp12ejDxEBLFrsjXr6NXR|fc_008390f508d1d09b016a58aaeffb40819aa7280d58a852370b","toolName":"glob","content":[{"type":"text","text":"# docs/\nUPDATE-WEDGE-2-ROUND3-CODEREAD.md\nUPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md\nGATEWAY-RCA-STEP1.md\nF-035-RCA.md\nF-035-RELAY-RCA.md\nPUMP-W2-DISPATCH-RULING.md\nMUSL-W3-DISPATCH-RULING.md\nMUSL-W1-DISPATCH-RULING.md\nW6-DISPATCH-RULING.md\nW5-DISPATCH-RULING.md\nW4-DISPATCH-RULING.md\nW3-DIGEST-GENERATION-UNION-RULING.md\nW2-DESIGN-RULING.md"}],"details":{"scopePath":"docs","fileCount":13,"files":["docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md","docs/GATEWAY-RCA-STEP1.md","docs/F-035-RCA.md","docs/F-035-RELAY-RCA.md","docs/PUMP-W2-DISPATCH-RULING.md","docs/MUSL-W3-DISPATCH-RULING.md","docs/MUSL-W1-DISPATCH-RULING.md","docs/W6-DISPATCH-RULING.md","docs/W5-DISPATCH-RULING.md","docs/W4-DISPATCH-RULING.md","docs/W3-DIGEST-GENERATION-UNION-RULING.md","docs/W2-DESIGN-RULING.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1784195824075}}
{"type":"message","id":"7a0d628a","parentId":"a5113aff","timestamp":"2026-07-16T09:57:04.078Z","message":{"role":"toolResult","toolCallId":"call_rWFAzgClh7FKjCoKzpFrv5wx|fc_008390f508d1d09b016a58aaeffb30819a86eb2d5c66438167","toolName":"grep","content":[{"type":"text","text":" 3218|    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\n*3219|    /// connection, start its output drain and exit waiter, and reply `spawned`.\n*3220|    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\n 3221|    /// 7.4): a hung child blocks only its own threads — shell instances ride\n 3222|    /// exactly this isolation (one owner's hung shell binary stalls neither\n 3223|    /// another owner's delivery nor the broker's control dispatch).\n...\n 3334|\n*3335|        // Output drain → append under the log lock (fast: ring + viewer fan-out),\n 3336|        // then deliver to the controller OFF the lock with a bounded deadline\n 3337|        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no\n 3338|        // longer inline under the log lock, so a backed-up controller can never\n...\n 3340|        // deadline (bounded wedge, never permanent).\n*3341|        let log_drain = Arc::clone(&log);\n*3342|        let drain = session\n*3343|            .drain(move |chunk| {\n 3344|                // append() does the controller handoff inline now (one non-blocking\n 3345|                // try_send — b4) and returns Some(epoch) ONLY when the controller is\n 3346|                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\n*3347|                // the first lock (a fresh lock, no self-deadlock). The drain never\n 3348|                // sleeps here, so a slow controller can't throttle the viewer fan-out.\n*3349|                let evict = recover_log(&log_drain).append(chunk);\n 3350|                if let Some(epoch) = evict {\n*3351|                    recover_log(&log_drain).mark_controller_gone(epoch);\n 3352|                }\n 3353|            })\n*3354|            .map_err(|e| format!(\"drain failed: {e}\"))?;\n 3355|\n 3356|        // Exit waiter → one `exit` frame when the child is reaped, then the dead\n 3357|        // session is removed from the table. Runs on its own thread; `wait`\n...\n 3361|        let waiter = Arc::clone(&session);\n*3362|        thread::spawn(move || {\n 3363|            if let Ok(status) = waiter.wait() {\n 3364|                let ev = ExitEvent {\n 3365|                    session_id: id,\n...\n 3376|                    // Bounded conn write (round-4): a wedged sink can no longer\n*3377|                    // park the exit-waiter thread indefinitely.\n 3378|                    let _ = sink.write(&frame);\n 3379|                }\n 3380|            }\n...\n 3385|            // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n*3386|            recover_log(&log_exit).stamp_reaped();\n 3387|            // Reap the dead session from the table.\n 3388|            recover(&sessions_exit).remove(&id);\n 3389|        });\n...\n 3417|                input,\n*3418|                drain,\n 3419|                log,\n 3420|                endpoint: req.endpoint.clone(),\n 3421|                translation,"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs","searchPath":"C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/broker.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":14,"fileCount":1,"files":["crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":14}],"truncated":false,"displayContent":" 3218│    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\n*3219│    /// connection, start its output drain and exit waiter, and reply `spawned`.\n*3220│    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\n 3221│    /// 7.4): a hung child blocks only its own threads — shell instances ride\n 3222│    /// exactly this isolation (one owner's hung shell binary stalls neither\n 3223│    /// another owner's delivery nor the broker's control dispatch).\n     │...\n 3334│\n*3335│        // Output drain → append under the log lock (fast: ring + viewer fan-out),\n 3336│        // then deliver to the controller OFF the lock with a bounded deadline\n 3337│        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no\n 3338│        // longer inline under the log lock, so a backed-up controller can never\n     │...\n 3340│        // deadline (bounded wedge, never permanent).\n*3341│        let log_drain = Arc::clone(&log);\n*3342│        let drain = session\n*3343│            .drain(move |chunk| {\n 3344│                // append() does the controller handoff inline now (one non-blocking\n 3345│                // try_send — b4) and returns Some(epoch) ONLY when the controller is\n 3346│                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\n*3347│                // the first lock (a fresh lock, no self-deadlock). The drain never\n 3348│                // sleeps here, so a slow controller can't throttle the viewer fan-out.\n*3349│                let evict = recover_log(&log_drain).append(chunk);\n 3350│                if let Some(epoch) = evict {\n*3351│                    recover_log(&log_drain).mark_controller_gone(epoch);\n 3352│                }\n 3353│            })\n*3354│            .map_err(|e| format!(\"drain failed: {e}\"))?;\n 3355│\n 3356│        // Exit waiter → one `exit` frame when the child is reaped, then the dead\n 3357│        // session is removed from the table. Runs on its own thread; `wait`\n     │...\n 3361│        let waiter = Arc::clone(&session);\n*3362│        thread::spawn(move || {\n 3363│            if let Ok(status) = waiter.wait() {\n 3364│                let ev = ExitEvent {\n 3365│                    session_id: id,\n     │...\n 3376│                    // Bounded conn write (round-4): a wedged sink can no longer\n*3377│                    // park the exit-waiter thread indefinitely.\n 3378│                    let _ = sink.write(&frame);\n 3379│                }\n 3380│            }\n     │...\n 3385│            // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n*3386│            recover_log(&log_exit).stamp_reaped();\n 3387│            // Reap the dead session from the table.\n 3388│            recover(&sessions_exit).remove(&id);\n 3389│        });\n     │...\n 3417│                input,\n*3418│                drain,\n 3419│                log,\n 3420│                endpoint: req.endpoint.clone(),\n 3421│                translation,"},"isError":false,"timestamp":1784195824078}}
{"type":"message","id":"2ff06037","parentId":"7a0d628a","timestamp":"2026-07-16T09:57:04.109Z","message":{"role":"toolResult","toolCallId":"call_7vvJkFJbHVanpWRPEatr9XOh|fc_008390f508d1d09b016a58aaeffb28819aba85e2cb06566622","toolName":"read","content":[{"type":"text","text":"/// meaningful only once the broker-owned NetHost is up, so we gate on the\n/// existing `net-status` verb (which also serves as the broker-liveness probe)\n/// and start them exactly once, on the first `enabled` reply. That single gate\n/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the\n/// NetHost bind in the background and `net-status` flips `enabled` when it\n/// attaches, at which point the brain starts the consumers — no extra signal.\n///\n/// It inherits the broker's already-unelevated token as a plain child, so it\n/// runs no de-elevation guard of its own.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\n    let _ = crate::stderrlog::install(\"brain\", generation);\n    let name = broker_socket_name();\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n    // *counter* is rehydrated, never the wall-clock stamp.\n    eprintln!(\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\n        std::process::id(),\n        reason.as_arg(),\n    );\n\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\n    // from its delivered cursor, the production replacement for the retired\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    match brain.resume_session_cursors() {\n        Ok(ids) if !ids.is_empty() => {\n            eprintln!(\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n                ids.len()\n            )\n        }\n        Ok(_) => {}\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\n    }\n\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\n    // breadcrumb would accept a brain that connected but never rebuilt\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\n    // this is forward-correct: the gate is defined on the real readiness\n    // semantic the live-agent adapter inherits.\n    write_ready(generation);\n\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n    // online/offline exclusivity — a reconcile loop that runs each offline\n    // instance's `wake_command` as a supervised watcher child. Relocated from\n    // the broker process to the brain child: shellwake is brain-owned logic\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\n    // posture. The stop flag is held for symmetry, never raised (the watchers\n    // die with the brain process and the next brain re-sweeps them).\n    // [impl->REQ-SHELL-2]\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\n\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\n    // shell waker). Online-only is staged behind the interim `api listen`\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\n    // brain the first-host. The stop flag is held for symmetry; the threads die\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\n    // [impl->REQ-DAEMON-1]\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\n\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\n    // reconcile in the background (ready-without-warm is preserved because\n    // write_ready already fired above), then drain/debounce/coalesce the\n    // invalidation queue. Same posture as the hosts above: the stop flag is\n    // held for symmetry, the thread dies with the brain, and the next brain\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n\n    let mut consumers_started = false;\n    loop {\n        thread::sleep(BRAIN_HEARTBEAT);\n        // `net-status` is both the net-capability probe and the broker-liveness\n        // probe: an `Err` is the broker gone (or a future planned\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n        let status = match brain.net_status() {\n            Ok(s) => s,\n            Err(e) => {\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\n                return Err(e);\n            }\n        };\n        if consumer_gate(status.enabled, consumers_started) {\n            let hex = status.node_id_hex.clone().unwrap_or_default();\n            spawn_net_consumers(&hex);\n            consumers_started = true;\n            eprintln!(\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n            );\n        }\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\n        // window, so without this the wedge never drains: the promotion DRAINED gate\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n        // best-effort — a transient sessions error must never fell the heartbeat (the\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\n        // its own trial against a live older broker — no coordinated broker restart.\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n        let _ = brain.sessions();\n        write_ready(generation);\n    }\n}\n\n/// Whether to spawn the net consumers this heartbeat: only once the broker\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\n/// semantics that keep a single dispatcher/peer-pump set running.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\n    net_enabled && !already_started\n}\n\n/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\n/// (D9-1) — against the broker's socket. **Relocated from the broker process to\n/// the brain child (restoration D2-1):** both are standalone IPC clients\n/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\n/// restartable brain. Each mints its own registry + stop flag and connects\n/// independently; when the brain process exits these in-process threads die with\n/// it, and the respawned brain re-spawns them from the current binary. Mirrors\n/// the wiring the broker boot used to do, `node_hex` now sourced from the\n/// `net-status` reply rather than the broker's identity load.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n// [impl->REQ-DAEMON-9]\nfn spawn_net_consumers(node_hex: &str) {\n    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\n    let stop = Arc::new(AtomicBool::new(false));\n…\n}\n\n[Showing lines 149-323 of 2116. Use :324 to continue]"}],"details":{"truncation":{"content":"/// meaningful only once the broker-owned NetHost is up, so we gate on the\r\n/// existing `net-status` verb (which also serves as the broker-liveness probe)\r\n/// and start them exactly once, on the first `enabled` reply. That single gate\r\n/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the\r\n/// NetHost bind in the background and `net-status` flips `enabled` when it\r\n/// attaches, at which point the brain starts the consumers — no extra signal.\r\n///\r\n/// It inherits the broker's already-unelevated token as a plain child, so it\r\n/// runs no de-elevation guard of its own.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\r\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\r\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\r\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\r\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\r\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\r\n    let _ = crate::stderrlog::install(\"brain\", generation);\r\n    let name = broker_socket_name();\r\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\r\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\r\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\r\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\r\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\r\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\r\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\r\n    // *counter* is rehydrated, never the wall-clock stamp.\r\n    eprintln!(\r\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\r\n        std::process::id(),\r\n        reason.as_arg(),\r\n    );\r\n\r\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\r\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\r\n    // from its delivered cursor, the production replacement for the retired\r\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\r\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\r\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\r\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\r\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\r\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\r\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\r\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\r\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\r\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\r\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\r\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\r\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\r\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\r\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\r\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\r\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    match brain.resume_session_cursors() {\r\n        Ok(ids) if !ids.is_empty() => {\r\n            eprintln!(\r\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\r\n                ids.len()\r\n            )\r\n        }\r\n        Ok(_) => {}\r\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\r\n    }\r\n\r\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\r\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\r\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\r\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\r\n    // breadcrumb would accept a brain that connected but never rebuilt\r\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\r\n    // this is forward-correct: the gate is defined on the real readiness\r\n    // semantic the live-agent adapter inherits.\r\n    write_ready(generation);\r\n\r\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\r\n    // online/offline exclusivity — a reconcile loop that runs each offline\r\n    // instance's `wake_command` as a supervised watcher child. Relocated from\r\n    // the broker process to the brain child: shellwake is brain-owned logic\r\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\r\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\r\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\r\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\r\n    // posture. The stop flag is held for symmetry, never raised (the watchers\r\n    // die with the brain process and the next brain re-sweeps them).\r\n    // [impl->REQ-SHELL-2]\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\r\n\r\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\r\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\r\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\r\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\r\n    // shell waker). Online-only is staged behind the interim `api listen`\r\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\r\n    // brain the first-host. The stop flag is held for symmetry; the threads die\r\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\r\n    // [impl->REQ-DAEMON-1]\r\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\r\n\r\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\r\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\r\n    // reconcile in the background (ready-without-warm is preserved because\r\n    // write_ready already fired above), then drain/debounce/coalesce the\r\n    // invalidation queue. Same posture as the hosts above: the stop flag is\r\n    // held for symmetry, the thread dies with the brain, and the next brain\r\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\r\n\r\n    let mut consumers_started = false;\r\n    loop {\r\n        thread::sleep(BRAIN_HEARTBEAT);\r\n        // `net-status` is both the net-capability probe and the broker-liveness\r\n        // probe: an `Err` is the broker gone (or a future planned\r\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\r\n        let status = match brain.net_status() {\r\n            Ok(s) => s,\r\n            Err(e) => {\r\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\r\n                return Err(e);\r\n            }\r\n        };\r\n        if consumer_gate(status.enabled, consumers_started) {\r\n            let hex = status.node_id_hex.clone().unwrap_or_default();\r\n            spawn_net_consumers(&hex);\r\n            consumers_started = true;\r\n            eprintln!(\r\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\r\n            );\r\n        }\r\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\r\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\r\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\r\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\r\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\r\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\r\n        // window, so without this the wedge never drains: the promotion DRAINED gate\r\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\r\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\r\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\r\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\r\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\r\n        // best-effort — a transient sessions error must never fell the heartbeat (the\r\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\r\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\r\n        // its own trial against a live older broker — no coordinated broker restart.\r\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n        let _ = brain.sessions();\r\n        write_ready(generation);\r\n    }\r\n}\r\n\r\n/// Whether to spawn the net consumers this heartbeat: only once the broker\r\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\r\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\r\n/// semantics that keep a single dispatcher/peer-pump set running.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\r\n    net_enabled && !already_started\r\n}\r\n\r\n/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\r\n/// (D9-1) — against the broker's socket. **Relocated from the broker process to\r\n/// the brain child (restoration D2-1):** both are standalone IPC clients\r\n/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\r\n/// restartable brain. Each mints its own registry + stop flag and connects\r\n/// independently; when the brain process exits these in-process threads die with\r\n/// it, and the respawned brain re-spawns them from the current binary. Mirrors\r\n/// the wiring the broker boot used to do, `node_hex` now sourced from the\r\n/// `net-status` reply rather than the broker's identity load.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n// [impl->REQ-DAEMON-9]\r\nfn spawn_net_consumers(node_hex: &str) {\r\n    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\r\n    let stop = Arc::new(AtomicBool::new(false));\r","truncated":true,"truncatedBy":"lines","totalLines":1968,"totalBytes":11050,"outputLines":175,"outputBytes":11050,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// meaningful only once the broker-owned NetHost is up, so we gate on the\r\n/// existing `net-status` verb (which also serves as the broker-liveness probe)\r\n/// and start them exactly once, on the first `enabled` reply. That single gate\r\n/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the\r\n/// NetHost bind in the background and `net-status` flips `enabled` when it\r\n/// attaches, at which point the brain starts the consumers — no extra signal.\r\n///\r\n/// It inherits the broker's already-unelevated token as a plain child, so it\r\n/// runs no de-elevation guard of its own.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\r\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\r\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\r\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\r\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\r\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\r\n    let _ = crate::stderrlog::install(\"brain\", generation);\r\n    let name = broker_socket_name();\r\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\r\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\r\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\r\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\r\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\r\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\r\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\r\n    // *counter* is rehydrated, never the wall-clock stamp.\r\n    eprintln!(\r\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\r\n        std::process::id(),\r\n        reason.as_arg(),\r\n    );\r\n\r\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\r\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\r\n    // from its delivered cursor, the production replacement for the retired\r\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\r\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\r\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\r\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\r\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\r\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\r\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\r\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\r\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\r\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\r\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\r\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\r\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\r\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\r\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\r\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\r\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    match brain.resume_session_cursors() {\r\n        Ok(ids) if !ids.is_empty() => {\r\n            eprintln!(\r\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\r\n                ids.len()\r\n            )\r\n        }\r\n        Ok(_) => {}\r\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\r\n    }\r\n\r\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\r\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\r\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\r\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\r\n    // breadcrumb would accept a brain that connected but never rebuilt\r\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\r\n    // this is forward-correct: the gate is defined on the real readiness\r\n    // semantic the live-agent adapter inherits.\r\n    write_ready(generation);\r\n\r\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\r\n    // online/offline exclusivity — a reconcile loop that runs each offline\r\n    // instance's `wake_command` as a supervised watcher child. Relocated from\r\n    // the broker process to the brain child: shellwake is brain-owned logic\r\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\r\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\r\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\r\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\r\n    // posture. The stop flag is held for symmetry, never raised (the watchers\r\n    // die with the brain process and the next brain re-sweeps them).\r\n    // [impl->REQ-SHELL-2]\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\r\n\r\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\r\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\r\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\r\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\r\n    // shell waker). Online-only is staged behind the interim `api listen`\r\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\r\n    // brain the first-host. The stop flag is held for symmetry; the threads die\r\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\r\n    // [impl->REQ-DAEMON-1]\r\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\r\n\r\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\r\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\r\n    // reconcile in the background (ready-without-warm is preserved because\r\n    // write_ready already fired above), then drain/debounce/coalesce the\r\n    // invalidation queue. Same posture as the hosts above: the stop flag is\r\n    // held for symmetry, the thread dies with the brain, and the next brain\r\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\r\n\r\n    let mut consumers_started = false;\r\n    loop {\r\n        thread::sleep(BRAIN_HEARTBEAT);\r\n        // `net-status` is both the net-capability probe and the broker-liveness\r\n        // probe: an `Err` is the broker gone (or a future planned\r\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\r\n        let status = match brain.net_status() {\r\n            Ok(s) => s,\r\n            Err(e) => {\r\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\r\n                return Err(e);\r\n            }\r\n        };\r\n        if consumer_gate(status.enabled, consumers_started) {\r\n            let hex = status.node_id_hex.clone().unwrap_or_default();\r\n            spawn_net_consumers(&hex);\r\n            consumers_started = true;\r\n            eprintln!(\r\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\r\n            );\r\n        }\r\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\r\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\r\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\r\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\r\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\r\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\r\n        // window, so without this the wedge never drains: the promotion DRAINED gate\r\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\r\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\r\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\r\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\r\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\r\n        // best-effort — a transient sessions error must never fell the heartbeat (the\r\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\r\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\r\n        // its own trial against a live older broker — no coordinated broker restart.\r\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n        let _ = brain.sessions();\r\n        write_ready(generation);\r\n    }\r\n}\r\n\r\n/// Whether to spawn the net consumers this heartbeat: only once the broker\r\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\r\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\r\n/// semantics that keep a single dispatcher/peer-pump set running.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\r\n    net_enabled && !already_started\r\n}\r\n\r\n/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\r\n/// (D9-1) — against the broker's socket. **Relocated from the broker process to\r\n/// the brain child (restoration D2-1):** both are standalone IPC clients\r\n/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\r\n/// restartable brain. Each mints its own registry + stop flag and connects\r\n/// independently; when the brain process exits these in-process threads die with\r\n/// it, and the respawned brain re-spawns them from the current binary. Mirrors\r\n/// the wiring the broker boot used to do, `node_hex` now sourced from the\r\n/// `net-status` reply rather than the broker's identity load.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n// [impl->REQ-DAEMON-9]\r\nfn spawn_net_consumers(node_hex: &str) {\r\n    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\r\n    let stop = Arc::new(AtomicBool::new(false));\r\n…\n}","startLine":149,"lineNumbers":[149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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,null,333]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2116,"totalBytes":11050,"outputLines":175,"outputBytes":11050,"shownRange":{"start":149,"end":323},"nextOffset":324}}},"isError":false,"timestamp":1784195824109}}
{"type":"message","id":"f884e6f4","parentId":"2ff06037","timestamp":"2026-07-16T09:57:04.118Z","message":{"role":"toolResult","toolCallId":"call_lG8Smf0S7EisZCk4seAtrSsc|fc_008390f508d1d09b016a58aaeffb3c819aaed3a9a786eb566d","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/tests/\n### brain_decouple.rs\n 69|static SEQ: AtomicU32 = AtomicU32::new(0);\n*70|fn unique_name() -> String {\n 71|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 72|    format!(\"spt-daemon-braindecouple-{}-{}.sock\", std::process::id(), n)\n 73|}\n...\n 75|static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n*76|fn init_home() {\n 77|    HOME.get_or_init(|| {\n 78|        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n 79|        std::env::set_var(\"SPT_HOME\", dir.path());\n...\n 83|\n*84|fn kill_pid(pid: u32) {\n 85|    #[cfg(windows)]\n 86|    let _ = std::process::Command::new(\"taskkill\")\n 87|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n...\n 96|/// that fills a non-reading controller's socket and parks its writer.\n*97|fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 98|    #[cfg(unix)]\n 99|    let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);\n 100|    #[cfg(windows)]\n...\n 121|\n*122|fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 123|    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 124|}\n 125|\n*126|fn connect(name: &str) -> Stream {\n 127|    for _ in 0..200 {\n 128|        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n 129|            send_hello(&mut c, Role::Brain).expect(\"hello\");\n...\n 138|/// Runs the read on a bounded thread so a wedge surfaces as a timeout, not a hang.\n*139|fn subscribe(\n 140|    name: &str,\n 141|    sid: u64,\n 142|    intent: AttachIntent,\n...\n 195|/// Query the broker's stall-evict tally.\n*196|fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 197|    let name = name.to_string();\n 198|    let (tx, rx) = std::sync::mpsc::channel();\n 199|    thread::spawn(move || {\n...\n 219|\n*220|// [int->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*221|#[test]\n*222|fn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\n 223|    init_home();\n 224|    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 225|    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\n...\n 419|/// never stops, so it can't prove a frozen cursor / bounded high-water advance).\n*420|#[cfg(windows)]\n*421|fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n 422|    SpawnReq {\n 423|        program: \"powershell\".to_string(),\n 424|        args: vec![\n...\n 447|/// thread returns.\n*448|#[cfg(windows)]\n*449|fn query_sessions(name: &str, deadline: Duration) -> Option<spt_daemon::msg::SessionsReply> {\n 450|    let name = name.to_string();\n 451|    let (tx, rx) = std::sync::mpsc::channel();\n 452|    thread::spawn(move || {\n...\n 470|\n*471|// [int->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n*472|#[cfg(windows)]\n*473|#[test]\n*474|fn non_draining_controller_stall_evict_releases_writer_and_connection() {\n 475|    init_home();\n 476|    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms.\n 477|    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n### brain_resume_conn_deadlock.rs\n 46|static SEQ: AtomicU32 = AtomicU32::new(0);\n*47|fn unique_name() -> String {\n 48|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 49|    format!(\"spt-daemon-resumedeadlock-{}-{}.sock\", std::process::id(), n)\n 50|}\n...\n 52|static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n*53|fn init_home() {\n 54|    HOME.get_or_init(|| {\n 55|        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n 56|        std::env::set_var(\"SPT_HOME\", dir.path());\n...\n 60|\n*61|fn kill_pid(pid: u32) {\n 62|    if pid == 0 {\n 63|        return; // unknown pid — never signal pid 0 (unix process group)\n 64|    }\n...\n 76|/// non-draining subscriber's conn and parks its writer inside `write_frame`.\n*77|fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 78|    #[cfg(unix)]\n 79|    let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);\n 80|    #[cfg(windows)]\n...\n 101|\n*102|fn connect(name: &str) -> Stream {\n 103|    for _ in 0..200 {\n 104|        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n 105|            send_hello(&mut c, Role::Brain).expect(\"hello\");\n...\n 116|/// brain's viewer subscription keeps receiving live output.\n*117|fn spawn_flood(name: &str, endpoint: &str) -> (u64, u32, Stream) {\n 118|    let mut c = connect(name);\n 119|    write_frame(\n 120|        &mut c,\n...\n 147|/// shipping daemon heartbeat under flood.\n*148|fn run_heartbeat_probe(\n 149|    name: &str,\n 150|    mode: ResumeMode,\n 151|    ticks: Arc<AtomicU32>,\n...\n 180|/// N flood PTY sessions are already streaming before the probe starts.\n*181|fn heartbeat_ticks_under_flood(mode: ResumeMode, n_sessions: usize, window: Duration) -> u32 {\n 182|    init_home();\n 183|    let name = unique_name();\n 184|    let broker = Broker::bind(&name).expect(\"bind broker\");\n...\n 220|/// starves to a standstill.\n*221|// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n*222|#[test]\n*223|fn daemon_cursor_only_resume_keeps_heartbeat_live_steady_state() {\n 224|    const N: usize = 6;\n 225|    let window = Duration::from_secs(6);\n 226|    let ticks = heartbeat_ticks_under_flood(ResumeMode::CursorOnly, N, window);\n...\n 239|/// its readiness/promotion — is never starved.\n*240|// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n*241|#[test]\n*242|fn daemon_cursor_only_resume_keeps_heartbeat_live_respawn_interleave() {\n 243|    const N: usize = 6;\n 244|    let window = Duration::from_secs(6);\n 245|    let ticks = heartbeat_ticks_under_flood(ResumeMode::CursorOnly, N, window);\n...\n 288|/// barrier `sessions()` returns.\n*289|// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n*290|#[test]\n*291|fn daemon_resume_leaves_zero_brain_subscribers() {\n 292|    const N: usize = 3;\n 293|    init_home();\n 294|    let name = unique_name();\n### conn_blackhole_lifecycle.rs\n 99|static SEQ: AtomicU32 = AtomicU32::new(0);\n*100|fn unique_name() -> String {\n 101|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 102|    format!(\"spt-daemon-blackhole-lc-{}-{}.sock\", std::process::id(), n)\n 103|}\n...\n 105|static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n*106|fn init_home() {\n 107|    HOME.get_or_init(|| {\n 108|        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n 109|        std::env::set_var(\"SPT_HOME\", dir.path());\n...\n 113|\n*114|fn kill_pid(pid: u32) {\n 115|    let _ = std::process::Command::new(\"taskkill\")\n 116|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 117|        .output();\n...\n 119|\n*120|fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 121|    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 122|}\n 123|\n*124|fn connect(name: &str) -> Stream {\n 125|    for _ in 0..200 {\n 126|        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n 127|            send_hello(&mut c, Role::Brain).expect(\"hello\");\n...\n 137|/// throughout the black-hole incident (invariant 1).\n*138|fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139|    SpawnReq {\n 140|        program: \"cmd\".to_string(),\n 141|        args: vec![\n...\n 161|/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n*162|fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n 163|    SpawnReq {\n 164|        program: \"powershell\".to_string(),\n 165|        args: vec![\n...\n 183|/// Spawn a PTY child from a fresh conn and return `(spawn_conn, session_id)`.\n*184|fn spawn_session(name: &str, req: SpawnReq) -> (Stream, u64) {\n 185|    let mut c = connect(name);\n 186|    send(&mut c, KIND_SPAWN, serde_json::to_value(req).unwrap());\n 187|    let sid = loop {\n...\n 197|/// surfaces as a timeout, not a hang). Verbatim sibling of the r4 gate helper.\n*198|fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 199|    let name = name.to_string();\n 200|    let (tx, rx) = std::sync::mpsc::channel();\n 201|    thread::spawn(move || {\n...\n 225|/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n*226|fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n 227|    let name = name.to_string();\n 228|    let (tx, rx) = std::sync::mpsc::channel();\n 229|    thread::spawn(move || {\n...\n 246|\n*247|// [int->REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS]\n*248|#[test]\n*249|fn blackholed_controller_lifecycle_five_invariants() {\n 250|    init_home();\n 251|    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 252|    // instead of 15 s — the structural outcome, fast + deterministic (the knob\n### false_promote.rs\n 74|static SEQ: AtomicU32 = AtomicU32::new(0);\n*75|fn unique_name() -> String {\n 76|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 77|    format!(\"spt-daemon-falsepromote-{}-{}.sock\", std::process::id(), n)\n 78|}\n...\n 80|static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n*81|fn init_home() {\n 82|    HOME.get_or_init(|| {\n 83|        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n 84|        std::env::set_var(\"SPT_HOME\", dir.path());\n...\n 88|\n*89|fn kill_pid(pid: u32) {\n 90|    #[cfg(windows)]\n 91|    let _ = Command::new(\"taskkill\")\n 92|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n...\n 99|/// controller's socket buffer and parks its broker-side writer (the wedge mechanism).\n*100|fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 101|    #[cfg(unix)]\n 102|    let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);\n 103|    #[cfg(windows)]\n...\n 126|/// stamp + the drained truth; the candidate only has to stay alive through the trial).\n*127|fn candidate_child() -> std::io::Result<Child> {\n 128|    #[cfg(windows)]\n 129|    {\n 130|        Command::new(\"cmd\")\n...\n 140|\n*141|fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 142|    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 143|}\n 144|\n*145|fn connect(name: &str) -> Stream {\n 146|    for _ in 0..200 {\n 147|        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n 148|            send_hello(&mut c, Role::Brain).expect(\"hello\");\n...\n 158|/// conn briefly so the take is not immediately detached, then lets it fall.\n*159|fn remote_take(name: &str, sid: u64) {\n 160|    let name = name.to_string();\n 161|    thread::spawn(move || {\n 162|        let mut c = connect(&name);\n...\n 234|\n*235|// [int->REQ-UPDATE-PROMOTE-DRAINED]\n*236|#[test]\n*237|fn ready_candidate_does_not_promote_until_the_wedged_old_gen_conn_drains() {\n 238|    init_home();\n 239|    // Shrink the brain-write deadline so the wedged old-gen conn is \"stalled\" in\n 240|    // ~400 ms instead of 15 s (the structural outcome, fast + deterministic).\n### attach.rs\n 31|// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n*32|#[cfg(unix)]\n 33|use spt_daemon::attach::request_attach_endpoint;\n 34|use spt_daemon::brain::{Brain, BrainState, BrokerEvent};\n 35|use spt_daemon::effect::{Minter, MintedOp};\n...\n 49|\n*50|fn init_home() {\n 51|    HOME.get_or_init(|| {\n 52|        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n 53|        std::env::set_var(\"SPT_HOME\", dir.path());\n...\n 58|static SEQ: AtomicU32 = AtomicU32::new(0);\n*59|fn unique_name() -> String {\n 60|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 61|    format!(\"spt-daemon-d5b-{}-{}.sock\", std::process::id(), n)\n 62|}\n 63|\n*64|fn hermetic() -> NetConfig {\n 65|    NetConfig {\n 66|        identity: Identity::generate(),\n 67|        relay: RelayPolicy::Disabled,\n...\n 73|\n*74|fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n 75|    let host = NetHost::start(hermetic()).expect(\"net host start\");\n 76|    let broker =\n 77|        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n...\n 84|\n*85|fn connect_retry(name: &str) -> Brain {\n 86|    for _ in 0..300 {\n 87|        if let Ok(b) = Brain::cold_start(name, 1) {\n 88|            return b;\n...\n 94|\n*95|fn echo_spawn_req() -> SpawnReq {\n 96|    #[cfg(unix)]\n 97|    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n 98|    #[cfg(windows)]\n...\n 118|/// row and the cross-wired peer row: the serve target is always the peer row.\n*119|fn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n 120|    for _ in 0..400 {\n 121|        let reply = brain.net_streams().expect(\"net-streams\");\n 122|        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {\n...\n 144|#[allow(clippy::too_many_arguments)]\n*145|fn attach_drive_detach(\n 146|    operator: &mut Brain,\n 147|    target_broker: &str,\n 148|    mut target: Brain,\n...\n 203|/// the rendered-through cursor (last accepted seq + 1).\n*204|fn render_until(\n 205|    brain: &mut Brain,\n 206|    stream_id: u64,\n 207|    decoder: &mut AttachDecoder,\n...\n 245|/// Count non-overlapping occurrences of `needle` in `hay`.\n*246|fn count(hay: &[u8], needle: &[u8]) -> usize {\n 247|    if needle.is_empty() || hay.len() < needle.len() {\n 248|        return 0;\n 249|    }\n...\n 261|\n*262|// [int->REQ-RC-1] loopback self-dial is REFUSED by the transport — pins the\n 263|// finding that drives `spt rc`'s LOCAL transport choice. A node cannot QUIC-dial\n 264|// its own node id (iroh: \"Connecting to ourself is not supported\"), so local\n 265|// attach CANNOT ride a self-dialed QUIC conn. The one-pump invariant therefore\n...\n 268|// Cross-node attach (the prize) is unaffected: it dials a real remote peer.\n*269|#[test]\n*270|fn loopback_self_dial_is_refused_local_uses_fallback_transport() {\n 271|    init_home();\n 272|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 273|    let name = unique_name();\n...\n 288|\n*289|// [int->REQ-HOST-RUN-1] [int->REQ-RC-1] spt-hosted harness bringup + cross-node\n 290|// attach, end to end: `harnesshost::launch_harness_brokered_in` spawns a harness's\n 291|// `[session.self]` into broker A's PTY (the spt-hosted bringup — the daemon owns\n 292|// the PTY, not an external launcher), then an operator on broker B attaches\n...\n 296|// the viewport (detach never kills it).\n*297|#[test]\n*298|fn spt_hosted_bringup_then_cross_node_attach_drives_the_pty() {\n 299|    init_home();\n 300|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 301|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 369|\n*370|// [unit->REQ-INST-8] drive an agent('s session) on machine A from machine B:\n 371|// B's keystrokes reach A's real PTY child over the wire (journaled input\n 372|// path), the child's output streams back as sequenced records, and B renders\n 373|// its own marker echoed remotely. The serve gate sees the handshake-proven\n 374|// origin; B detaching ends the viewport with the session still running on A.\n*375|#[test]\n*376|fn remote_attach_drives_a_real_pty_cross_daemon() {\n 377|    init_home();\n 378|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 379|    let (name_a, name_b) = (unique_name(), unique_name());\n### control_stamp_lifetime.rs\n 55|static SEQ: AtomicU32 = AtomicU32::new(0);\n*56|fn unique_name() -> String {\n 57|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 58|    format!(\"spt-daemon-ctlstamp-{}-{}.sock\", std::process::id(), n)\n 59|}\n...\n 61|static STAMP_HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n*62|fn init_stamp_home() {\n 63|    STAMP_HOME.get_or_init(|| {\n 64|        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n 65|        std::env::set_var(\"SPT_HOME\", dir.path());\n...\n 69|\n*70|fn kill_pid(pid: u32) {\n 71|    #[cfg(windows)]\n 72|    let _ = std::process::Command::new(\"taskkill\")\n 73|        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n...\n 80|\n*81|fn start_broker(name: &str) -> Arc<Broker> {\n 82|    let broker = Broker::bind(name).expect(\"bind broker\");\n 83|    let serve = Arc::clone(&broker);\n 84|    thread::spawn(move || {\n...\n 89|\n*90|fn connect_brain(name: &str) -> Brain {\n 91|    for _ in 0..300 {\n 92|        if let Ok(b) = Brain::cold_start(name, 1) {\n 93|            return b;\n...\n 101|/// kill it (cross-OS).\n*102|fn idle_spawn_req(endpoint: &str) -> SpawnReq {\n 103|    #[cfg(unix)]\n 104|    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n 105|    #[cfg(windows)]\n...\n 122|/// the broker's stamps land on a real info.json (mirrors the selfheal rig's stamp).\n*123|fn create_hosted_perch(id: &str) -> std::path::PathBuf {\n 124|    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n 125|    std::fs::create_dir_all(&self_perch).unwrap();\n 126|    let mut rec =\n...\n 133|\n*134|#[test]\n*135|fn reap_clears_control_and_viewer_stamps() {\n 136|    init_stamp_home();\n 137|    let node = \"PEERNODEHEX_CTLSTAMP\";\n 138|    let id = format!(\"ctlstamp-{}\", std::process::id());\n...\n 208|\n*209|    // [int->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n 210|    assert!(\n 211|        cleared,\n 212|        \"reap must clear every control/viewer stamp within the poll window — \\\n...\n 237|/// `controlled:false` forever.\n*238|#[test]\n*239|fn converge_stamps_on_sessions_poll_after_late_bind() {\n 240|    init_stamp_home();\n 241|    let id = format!(\"ctlconv-{}\", std::process::id());\n 242|\n...\n 284|\n*285|    // [int->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n 286|    assert!(\n 287|        converged,\n 288|        \"the KIND_SESSIONS poll must converge controlled=true within the window — got {after:?}\"\n...\n 320|//       restarted daemon must not resurrect the stamps).\n*321|// [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n 322|\n 323|/// Seed a spt-hosted (`controllable=Some(true)`) live_agent self-perch at `status`,\n 324|/// carrying the FULL sticky control latch a departed remote-driven session leaves:\n 325|/// `controlled=true` + `driven_by=Some(node)`. Returns the perch path.\n*326|fn seed_sticky_hosted_perch(id: &str, status: &str, node: &str) -> std::path::PathBuf {\n 327|    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n 328|    std::fs::create_dir_all(&self_perch).unwrap();\n 329|    let mut rec =\n...\n 344|\n*345|#[test]\n*346|fn control_reap_clears_sticky_stamps_no_session_online_and_boot_sweep() {\n 347|    init_stamp_home();\n 348|    let node = \"PEERNODEHEX_B3\";\n 349|    let pid = std::process::id();\n...\n 412|    // Arm A — ONLINE + sticky: BOTH stamps cleared AND the perch offlined.\n*413|    // [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n 414|    assert!(\n 415|        !after_online.controlled,\n 416|        \"B3 arm A: reap must clear controlled on an online sessionless spt-hosted perch\"\n...\n 429|    // (the perri F-b bug: >20min + across a daemon restart). Presence stays OFFLINE.\n*430|    // [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n 431|    assert!(\n 432|        !after_offline.controlled,\n 433|        \"B3 arm B (boot sweep): a restarted daemon must clear a sticky controlled on an \\\n\n## spt/tests/\n### resume_no_control_steal_e2e.rs\n 117|\n*118|fn kill_pid(pid: u32) {\n 119|    #[cfg(windows)]\n 120|    let _ = Command::new(\"taskkill\")\n 121|        .no_window()\n...\n 131|/// resume STOLE control (RED). Unbuffered small writes so a tick reaches the PTY promptly.\n*132|fn ticker_spawn_req(endpoint: &str) -> SpawnReq {\n 133|    #[cfg(unix)]\n 134|    let (program, args) = (\n 135|        \"sh\".to_string(),\n...\n 168|/// supervisor exits before it runs — this only satisfies the closure's `io::Result<Child>`).\n*169|fn placeholder_child() -> std::io::Result<Child> {\n 170|    #[cfg(windows)]\n 171|    {\n 172|        Command::new(\"cmd\")\n...\n 186|/// subprocess brain boots the proven production way.\n*187|fn hermetic(identity: Identity) -> NetConfig {\n 188|    NetConfig {\n 189|        identity,\n 190|        relay: RelayPolicy::Disabled,\n...\n 197|/// Bind a net-enabled broker (serving on a background thread) at `name`.\n*198|fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n 199|    let host = NetHost::start(hermetic(Identity::generate())).expect(\"net host start\");\n 200|    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\n 201|        .expect(\"bind broker with net\");\n...\n 214|/// concrete stream is `Send + 'static` so the per-session reader thread can own it.\n*215|fn connect(name: &str) -> impl std::io::Read + std::io::Write + Send + 'static {\n 216|    for _ in 0..300 {\n 217|        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n 218|            send_hello(&mut c, Role::Brain).expect(\"hello\");\n...\n 229|/// controller's live output feed — it FREEZES the instant a resume-steal displaces it.\n*230|fn spawn_ticker_controller(name: &str, endpoint: &str) -> (u64, Arc<AtomicU64>) {\n 231|    let mut c = connect(name);\n 232|    send(\n 233|        &mut c,\n...\n 265|/// any, has already happened). Returns that pid, or `None` if it never appears in `budget`.\n*266|fn wait_ready_pid(path: &std::path::Path, budget: Duration) -> Option<u32> {\n 267|    let deadline = Instant::now() + budget;\n 268|    while Instant::now() < deadline {\n 269|        if let Some(pid) = std::fs::read_to_string(path)\n...\n 330|/// `msg` — keeps a failing assertion from orphaning the subprocess brain + the children.\n*331|fn teardown_panic(stop: &AtomicBool, msg: &str) -> ! {\n 332|    stop.store(true, Ordering::SeqCst);\n 333|    panic!(\"{msg}\");\n 334|}\n 335|\n*336|// [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n*337|#[test]\n*338|fn brain_respawn_keeps_every_session_controller_and_still_promotes() {\n 339|    let home = tempfile::tempdir().expect(\"tempdir\");\n 340|    std::env::set_var(\"SPT_HOME\", home.path());\n 341|    // Parity with trial_drain_drive: shrink the broker-side brain-write deadline (grants\n...\n 485|    // each slot, dropping A_i → its tally froze → gained==0. GREEN (Viewer): A_i untouched.\n*486|    // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n 487|    for (i, g) in gained.iter().enumerate() {\n 488|        assert!(\n 489|            *g > 0,\n...\n 503|    // old_gen_drained() is true and the ready candidate promotes.\n*504|    // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n 505|    assert!(\n 506|        promoted,\n 507|        \"the trial must still PROMOTE the candidate under N live sessions (no regression of \\\n### attach_wedge_e2e.rs\n 37|\n*38|fn kill_pid(pid: u32) {\n 39|    #[cfg(windows)]\n 40|    let _ = Command::new(\"taskkill\")\n 41|        .no_window()\n...\n 47|\n*48|fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {\n 49|    let (tx, rx) = std::sync::mpsc::channel();\n 50|    std::thread::spawn(move || {\n 51|        let _ = tx.send(cmd.output());\n...\n 57|\n*58|fn ready_pid(path: &Path) -> Option<u32> {\n 59|    let s = std::fs::read_to_string(path).ok()?;\n 60|    serde_json::from_str::<serde_json::Value>(&s)\n 61|        .ok()?\n...\n 66|\n*67|fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {\n 68|    let deadline = Instant::now() + budget;\n 69|    while Instant::now() < deadline {\n 70|        if let Some(p) = ready_pid(path) {\n...\n 77|\n*78|fn sibling_bin(name: &str) -> PathBuf {\n 79|    PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"))\n 80|        .with_file_name(format!(\"{name}{}\", std::env::consts::EXE_SUFFIX))\n 81|}\n...\n 85|/// `ENDPOINT_RUN:… pid=NNN` stderr machine token — the long-lived PTY child).\n*86|fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {\n 87|    let mut cmd = Command::new(spt_bin);\n 88|    cmd.no_window()\n 89|        .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id, \"--start\"])\n...\n 101|/// Poll `<id>`'s self-perch status until it equals `want` or the budget elapses.\n*102|fn wait_for_status(id: &str, want: &str, budget: Duration) -> bool {\n 103|    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\n 104|    let deadline = Instant::now() + budget;\n 105|    while Instant::now() < deadline {\n...\n 120|/// pipe (read → 0/Err) and exits.\n*121|fn rc_sees_tick(spt_bin: &Path, home: &Path, id: &str, window: Duration) -> bool {\n 122|    let mut rc: Child = Command::new(spt_bin)\n 123|        .no_window()\n 124|        .args([\"rc\", id])\n...\n 161|\n*162|#[test]\n*163|fn attach_wedge_dead_child_plus_dropped_pump_does_not_wedge_the_broker() {\n 164|    // ── (1) Isolated, NET-ENABLED SPT_HOME (rc attaches over a broker loopback conn\n 165|    //    that surfaces as net-stream events — needs a net-bound broker). ──\n 166|    let home = tempfile::tempdir().unwrap();\n...\n 262|        // STOP-LIVE-SESSION-WARN contract refuses a plain stop — this teardown\n*263|        // intends to kill it. [int->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\n 264|        cmd.no_window().args([\"daemon\", \"stop\", \"--force\"]).env(\"SPT_HOME\", home.path());\n 265|        output_bounded(cmd, Duration::from_secs(20))\n 266|    };\n...\n 302|    // L2 keystone: the broker survived the dead-child + dropped-pump and still serves.\n*303|    // [int->REQ-HAZARD-ATTACH-WEDGE]\n 304|    assert!(\n 305|        fresh_online,\n 306|        \"after a dead PTY child + a dropped operator pump, a NEW `endpoint run` must \\\n...\n 314|    // L3 (folded): the dead endpoint is offlined within a reconcile tick.\n*315|    // [int->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\n 316|    assert!(\n 317|        victim_offlined,\n 318|        \"the dead victim endpoint must be OFFLINED within a reconcile tick (its broker \\"}],"details":{"scopePath":"crates/spt-daemon/tests/brain_decouple.rs, crates/spt-daemon/tests/brain_resume_conn_deadlock.rs, crates/spt-daemon/tests/conn_blackhole_lifecycle.rs, crates/spt-daemon/tests/false_promote.rs, crates/spt/tests/resume_no_control_steal_e2e.rs, crates/spt/tests/attach_wedge_e2e.rs, crates/spt-daemon/tests/attach.rs, crates/spt-daemon/tests/control_stamp_lifetime.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":126,"fileCount":8,"files":["crates/spt-daemon/tests/brain_decouple.rs","crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt/tests/attach_wedge_e2e.rs","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/control_stamp_lifetime.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/brain_decouple.rs","count":19},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","count":17},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","count":13},{"path":"crates/spt-daemon/tests/false_promote.rs","count":11},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","count":14},{"path":"crates/spt/tests/attach_wedge_e2e.rs","count":13},{"path":"crates/spt-daemon/tests/attach.rs","count":20},{"path":"crates/spt-daemon/tests/control_stamp_lifetime.rs","count":19}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/tests/\n### brain_decouple.rs\n  69│static SEQ: AtomicU32 = AtomicU32::new(0);\n *70│fn unique_name() -> String {\n  71│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n  72│    format!(\"spt-daemon-braindecouple-{}-{}.sock\", std::process::id(), n)\n  73│}\n    │...\n  75│static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n *76│fn init_home() {\n  77│    HOME.get_or_init(|| {\n  78│        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n  79│        std::env::set_var(\"SPT_HOME\", dir.path());\n    │...\n  83│\n *84│fn kill_pid(pid: u32) {\n  85│    #[cfg(windows)]\n  86│    let _ = std::process::Command::new(\"taskkill\")\n  87│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n    │...\n  96│/// that fills a non-reading controller's socket and parks its writer.\n *97│fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n  98│    #[cfg(unix)]\n  99│    let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);\n 100│    #[cfg(windows)]\n    │...\n 121│\n*122│fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 123│    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 124│}\n 125│\n*126│fn connect(name: &str) -> Stream {\n 127│    for _ in 0..200 {\n 128│        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n 129│            send_hello(&mut c, Role::Brain).expect(\"hello\");\n    │...\n 138│/// Runs the read on a bounded thread so a wedge surfaces as a timeout, not a hang.\n*139│fn subscribe(\n 140│    name: &str,\n 141│    sid: u64,\n 142│    intent: AttachIntent,\n    │...\n 195│/// Query the broker's stall-evict tally.\n*196│fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 197│    let name = name.to_string();\n 198│    let (tx, rx) = std::sync::mpsc::channel();\n 199│    thread::spawn(move || {\n    │...\n 219│\n*220│// [int->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*221│#[test]\n*222│fn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\n 223│    init_home();\n 224│    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 225│    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\n    │...\n 419│/// never stops, so it can't prove a frozen cursor / bounded high-water advance).\n*420│#[cfg(windows)]\n*421│fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n 422│    SpawnReq {\n 423│        program: \"powershell\".to_string(),\n 424│        args: vec![\n    │...\n 447│/// thread returns.\n*448│#[cfg(windows)]\n*449│fn query_sessions(name: &str, deadline: Duration) -> Option<spt_daemon::msg::SessionsReply> {\n 450│    let name = name.to_string();\n 451│    let (tx, rx) = std::sync::mpsc::channel();\n 452│    thread::spawn(move || {\n    │...\n 470│\n*471│// [int->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n*472│#[cfg(windows)]\n*473│#[test]\n*474│fn non_draining_controller_stall_evict_releases_writer_and_connection() {\n 475│    init_home();\n 476│    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms.\n 477│    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n### brain_resume_conn_deadlock.rs\n  46│static SEQ: AtomicU32 = AtomicU32::new(0);\n *47│fn unique_name() -> String {\n  48│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n  49│    format!(\"spt-daemon-resumedeadlock-{}-{}.sock\", std::process::id(), n)\n  50│}\n    │...\n  52│static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n *53│fn init_home() {\n  54│    HOME.get_or_init(|| {\n  55│        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n  56│        std::env::set_var(\"SPT_HOME\", dir.path());\n    │...\n  60│\n *61│fn kill_pid(pid: u32) {\n  62│    if pid == 0 {\n  63│        return; // unknown pid — never signal pid 0 (unix process group)\n  64│    }\n    │...\n  76│/// non-draining subscriber's conn and parks its writer inside `write_frame`.\n *77│fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n  78│    #[cfg(unix)]\n  79│    let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);\n  80│    #[cfg(windows)]\n    │...\n 101│\n*102│fn connect(name: &str) -> Stream {\n 103│    for _ in 0..200 {\n 104│        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n 105│            send_hello(&mut c, Role::Brain).expect(\"hello\");\n    │...\n 116│/// brain's viewer subscription keeps receiving live output.\n*117│fn spawn_flood(name: &str, endpoint: &str) -> (u64, u32, Stream) {\n 118│    let mut c = connect(name);\n 119│    write_frame(\n 120│        &mut c,\n    │...\n 147│/// shipping daemon heartbeat under flood.\n*148│fn run_heartbeat_probe(\n 149│    name: &str,\n 150│    mode: ResumeMode,\n 151│    ticks: Arc<AtomicU32>,\n    │...\n 180│/// N flood PTY sessions are already streaming before the probe starts.\n*181│fn heartbeat_ticks_under_flood(mode: ResumeMode, n_sessions: usize, window: Duration) -> u32 {\n 182│    init_home();\n 183│    let name = unique_name();\n 184│    let broker = Broker::bind(&name).expect(\"bind broker\");\n    │...\n 220│/// starves to a standstill.\n*221│// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n*222│#[test]\n*223│fn daemon_cursor_only_resume_keeps_heartbeat_live_steady_state() {\n 224│    const N: usize = 6;\n 225│    let window = Duration::from_secs(6);\n 226│    let ticks = heartbeat_ticks_under_flood(ResumeMode::CursorOnly, N, window);\n    │...\n 239│/// its readiness/promotion — is never starved.\n*240│// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n*241│#[test]\n*242│fn daemon_cursor_only_resume_keeps_heartbeat_live_respawn_interleave() {\n 243│    const N: usize = 6;\n 244│    let window = Duration::from_secs(6);\n 245│    let ticks = heartbeat_ticks_under_flood(ResumeMode::CursorOnly, N, window);\n    │...\n 288│/// barrier `sessions()` returns.\n*289│// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n*290│#[test]\n*291│fn daemon_resume_leaves_zero_brain_subscribers() {\n 292│    const N: usize = 3;\n 293│    init_home();\n 294│    let name = unique_name();\n### conn_blackhole_lifecycle.rs\n  99│static SEQ: AtomicU32 = AtomicU32::new(0);\n*100│fn unique_name() -> String {\n 101│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n 102│    format!(\"spt-daemon-blackhole-lc-{}-{}.sock\", std::process::id(), n)\n 103│}\n    │...\n 105│static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n*106│fn init_home() {\n 107│    HOME.get_or_init(|| {\n 108│        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n 109│        std::env::set_var(\"SPT_HOME\", dir.path());\n    │...\n 113│\n*114│fn kill_pid(pid: u32) {\n 115│    let _ = std::process::Command::new(\"taskkill\")\n 116│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n 117│        .output();\n    │...\n 119│\n*120│fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 121│    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 122│}\n 123│\n*124│fn connect(name: &str) -> Stream {\n 125│    for _ in 0..200 {\n 126│        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n 127│            send_hello(&mut c, Role::Brain).expect(\"hello\");\n    │...\n 137│/// throughout the black-hole incident (invariant 1).\n*138│fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139│    SpawnReq {\n 140│        program: \"cmd\".to_string(),\n 141│        args: vec![\n    │...\n 161│/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n*162│fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n 163│    SpawnReq {\n 164│        program: \"powershell\".to_string(),\n 165│        args: vec![\n    │...\n 183│/// Spawn a PTY child from a fresh conn and return `(spawn_conn, session_id)`.\n*184│fn spawn_session(name: &str, req: SpawnReq) -> (Stream, u64) {\n 185│    let mut c = connect(name);\n 186│    send(&mut c, KIND_SPAWN, serde_json::to_value(req).unwrap());\n 187│    let sid = loop {\n    │...\n 197│/// surfaces as a timeout, not a hang). Verbatim sibling of the r4 gate helper.\n*198│fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 199│    let name = name.to_string();\n 200│    let (tx, rx) = std::sync::mpsc::channel();\n 201│    thread::spawn(move || {\n    │...\n 225│/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n*226│fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n 227│    let name = name.to_string();\n 228│    let (tx, rx) = std::sync::mpsc::channel();\n 229│    thread::spawn(move || {\n    │...\n 246│\n*247│// [int->REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS]\n*248│#[test]\n*249│fn blackholed_controller_lifecycle_five_invariants() {\n 250│    init_home();\n 251│    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 252│    // instead of 15 s — the structural outcome, fast + deterministic (the knob\n### false_promote.rs\n  74│static SEQ: AtomicU32 = AtomicU32::new(0);\n *75│fn unique_name() -> String {\n  76│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n  77│    format!(\"spt-daemon-falsepromote-{}-{}.sock\", std::process::id(), n)\n  78│}\n    │...\n  80│static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n *81│fn init_home() {\n  82│    HOME.get_or_init(|| {\n  83│        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n  84│        std::env::set_var(\"SPT_HOME\", dir.path());\n    │...\n  88│\n *89│fn kill_pid(pid: u32) {\n  90│    #[cfg(windows)]\n  91│    let _ = Command::new(\"taskkill\")\n  92│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n    │...\n  99│/// controller's socket buffer and parks its broker-side writer (the wedge mechanism).\n*100│fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 101│    #[cfg(unix)]\n 102│    let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);\n 103│    #[cfg(windows)]\n    │...\n 126│/// stamp + the drained truth; the candidate only has to stay alive through the trial).\n*127│fn candidate_child() -> std::io::Result<Child> {\n 128│    #[cfg(windows)]\n 129│    {\n 130│        Command::new(\"cmd\")\n    │...\n 140│\n*141│fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 142│    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 143│}\n 144│\n*145│fn connect(name: &str) -> Stream {\n 146│    for _ in 0..200 {\n 147│        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n 148│            send_hello(&mut c, Role::Brain).expect(\"hello\");\n    │...\n 158│/// conn briefly so the take is not immediately detached, then lets it fall.\n*159│fn remote_take(name: &str, sid: u64) {\n 160│    let name = name.to_string();\n 161│    thread::spawn(move || {\n 162│        let mut c = connect(&name);\n    │...\n 234│\n*235│// [int->REQ-UPDATE-PROMOTE-DRAINED]\n*236│#[test]\n*237│fn ready_candidate_does_not_promote_until_the_wedged_old_gen_conn_drains() {\n 238│    init_home();\n 239│    // Shrink the brain-write deadline so the wedged old-gen conn is \"stalled\" in\n 240│    // ~400 ms instead of 15 s (the structural outcome, fast + deterministic).\n### attach.rs\n  31│// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n *32│#[cfg(unix)]\n  33│use spt_daemon::attach::request_attach_endpoint;\n  34│use spt_daemon::brain::{Brain, BrainState, BrokerEvent};\n  35│use spt_daemon::effect::{Minter, MintedOp};\n    │...\n  49│\n *50│fn init_home() {\n  51│    HOME.get_or_init(|| {\n  52│        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n  53│        std::env::set_var(\"SPT_HOME\", dir.path());\n    │...\n  58│static SEQ: AtomicU32 = AtomicU32::new(0);\n *59│fn unique_name() -> String {\n  60│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n  61│    format!(\"spt-daemon-d5b-{}-{}.sock\", std::process::id(), n)\n  62│}\n  63│\n *64│fn hermetic() -> NetConfig {\n  65│    NetConfig {\n  66│        identity: Identity::generate(),\n  67│        relay: RelayPolicy::Disabled,\n    │...\n  73│\n *74│fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n  75│    let host = NetHost::start(hermetic()).expect(\"net host start\");\n  76│    let broker =\n  77│        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n    │...\n  84│\n *85│fn connect_retry(name: &str) -> Brain {\n  86│    for _ in 0..300 {\n  87│        if let Ok(b) = Brain::cold_start(name, 1) {\n  88│            return b;\n    │...\n  94│\n *95│fn echo_spawn_req() -> SpawnReq {\n  96│    #[cfg(unix)]\n  97│    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n  98│    #[cfg(windows)]\n    │...\n 118│/// row and the cross-wired peer row: the serve target is always the peer row.\n*119│fn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n 120│    for _ in 0..400 {\n 121│        let reply = brain.net_streams().expect(\"net-streams\");\n 122│        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {\n    │...\n 144│#[allow(clippy::too_many_arguments)]\n*145│fn attach_drive_detach(\n 146│    operator: &mut Brain,\n 147│    target_broker: &str,\n 148│    mut target: Brain,\n    │...\n 203│/// the rendered-through cursor (last accepted seq + 1).\n*204│fn render_until(\n 205│    brain: &mut Brain,\n 206│    stream_id: u64,\n 207│    decoder: &mut AttachDecoder,\n    │...\n 245│/// Count non-overlapping occurrences of `needle` in `hay`.\n*246│fn count(hay: &[u8], needle: &[u8]) -> usize {\n 247│    if needle.is_empty() || hay.len() < needle.len() {\n 248│        return 0;\n 249│    }\n    │...\n 261│\n*262│// [int->REQ-RC-1] loopback self-dial is REFUSED by the transport — pins the\n 263│// finding that drives `spt rc`'s LOCAL transport choice. A node cannot QUIC-dial\n 264│// its own node id (iroh: \"Connecting to ourself is not supported\"), so local\n 265│// attach CANNOT ride a self-dialed QUIC conn. The one-pump invariant therefore\n    │...\n 268│// Cross-node attach (the prize) is unaffected: it dials a real remote peer.\n*269│#[test]\n*270│fn loopback_self_dial_is_refused_local_uses_fallback_transport() {\n 271│    init_home();\n 272│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 273│    let name = unique_name();\n    │...\n 288│\n*289│// [int->REQ-HOST-RUN-1] [int->REQ-RC-1] spt-hosted harness bringup + cross-node\n 290│// attach, end to end: `harnesshost::launch_harness_brokered_in` spawns a harness's\n 291│// `[session.self]` into broker A's PTY (the spt-hosted bringup — the daemon owns\n 292│// the PTY, not an external launcher), then an operator on broker B attaches\n    │...\n 296│// the viewport (detach never kills it).\n*297│#[test]\n*298│fn spt_hosted_bringup_then_cross_node_attach_drives_the_pty() {\n 299│    init_home();\n 300│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 301│    let (name_a, name_b) = (unique_name(), unique_name());\n    │...\n 369│\n*370│// [unit->REQ-INST-8] drive an agent('s session) on machine A from machine B:\n 371│// B's keystrokes reach A's real PTY child over the wire (journaled input\n 372│// path), the child's output streams back as sequenced records, and B renders\n 373│// its own marker echoed remotely. The serve gate sees the handshake-proven\n 374│// origin; B detaching ends the viewport with the session still running on A.\n*375│#[test]\n*376│fn remote_attach_drives_a_real_pty_cross_daemon() {\n 377│    init_home();\n 378│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 379│    let (name_a, name_b) = (unique_name(), unique_name());\n### control_stamp_lifetime.rs\n  55│static SEQ: AtomicU32 = AtomicU32::new(0);\n *56│fn unique_name() -> String {\n  57│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n  58│    format!(\"spt-daemon-ctlstamp-{}-{}.sock\", std::process::id(), n)\n  59│}\n    │...\n  61│static STAMP_HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n *62│fn init_stamp_home() {\n  63│    STAMP_HOME.get_or_init(|| {\n  64│        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n  65│        std::env::set_var(\"SPT_HOME\", dir.path());\n    │...\n  69│\n *70│fn kill_pid(pid: u32) {\n  71│    #[cfg(windows)]\n  72│    let _ = std::process::Command::new(\"taskkill\")\n  73│        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n    │...\n  80│\n *81│fn start_broker(name: &str) -> Arc<Broker> {\n  82│    let broker = Broker::bind(name).expect(\"bind broker\");\n  83│    let serve = Arc::clone(&broker);\n  84│    thread::spawn(move || {\n    │...\n  89│\n *90│fn connect_brain(name: &str) -> Brain {\n  91│    for _ in 0..300 {\n  92│        if let Ok(b) = Brain::cold_start(name, 1) {\n  93│            return b;\n    │...\n 101│/// kill it (cross-OS).\n*102│fn idle_spawn_req(endpoint: &str) -> SpawnReq {\n 103│    #[cfg(unix)]\n 104│    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n 105│    #[cfg(windows)]\n    │...\n 122│/// the broker's stamps land on a real info.json (mirrors the selfheal rig's stamp).\n*123│fn create_hosted_perch(id: &str) -> std::path::PathBuf {\n 124│    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n 125│    std::fs::create_dir_all(&self_perch).unwrap();\n 126│    let mut rec =\n    │...\n 133│\n*134│#[test]\n*135│fn reap_clears_control_and_viewer_stamps() {\n 136│    init_stamp_home();\n 137│    let node = \"PEERNODEHEX_CTLSTAMP\";\n 138│    let id = format!(\"ctlstamp-{}\", std::process::id());\n    │...\n 208│\n*209│    // [int->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n 210│    assert!(\n 211│        cleared,\n 212│        \"reap must clear every control/viewer stamp within the poll window — \\\n    │...\n 237│/// `controlled:false` forever.\n*238│#[test]\n*239│fn converge_stamps_on_sessions_poll_after_late_bind() {\n 240│    init_stamp_home();\n 241│    let id = format!(\"ctlconv-{}\", std::process::id());\n 242│\n    │...\n 284│\n*285│    // [int->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n 286│    assert!(\n 287│        converged,\n 288│        \"the KIND_SESSIONS poll must converge controlled=true within the window — got {after:?}\"\n    │...\n 320│//       restarted daemon must not resurrect the stamps).\n*321│// [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n 322│\n 323│/// Seed a spt-hosted (`controllable=Some(true)`) live_agent self-perch at `status`,\n 324│/// carrying the FULL sticky control latch a departed remote-driven session leaves:\n 325│/// `controlled=true` + `driven_by=Some(node)`. Returns the perch path.\n*326│fn seed_sticky_hosted_perch(id: &str, status: &str, node: &str) -> std::path::PathBuf {\n 327│    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n 328│    std::fs::create_dir_all(&self_perch).unwrap();\n 329│    let mut rec =\n    │...\n 344│\n*345│#[test]\n*346│fn control_reap_clears_sticky_stamps_no_session_online_and_boot_sweep() {\n 347│    init_stamp_home();\n 348│    let node = \"PEERNODEHEX_B3\";\n 349│    let pid = std::process::id();\n    │...\n 412│    // Arm A — ONLINE + sticky: BOTH stamps cleared AND the perch offlined.\n*413│    // [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n 414│    assert!(\n 415│        !after_online.controlled,\n 416│        \"B3 arm A: reap must clear controlled on an online sessionless spt-hosted perch\"\n    │...\n 429│    // (the perri F-b bug: >20min + across a daemon restart). Presence stays OFFLINE.\n*430│    // [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n 431│    assert!(\n 432│        !after_offline.controlled,\n 433│        \"B3 arm B (boot sweep): a restarted daemon must clear a sticky controlled on an \\\n\n## spt/tests/\n### resume_no_control_steal_e2e.rs\n 117│\n*118│fn kill_pid(pid: u32) {\n 119│    #[cfg(windows)]\n 120│    let _ = Command::new(\"taskkill\")\n 121│        .no_window()\n    │...\n 131│/// resume STOLE control (RED). Unbuffered small writes so a tick reaches the PTY promptly.\n*132│fn ticker_spawn_req(endpoint: &str) -> SpawnReq {\n 133│    #[cfg(unix)]\n 134│    let (program, args) = (\n 135│        \"sh\".to_string(),\n    │...\n 168│/// supervisor exits before it runs — this only satisfies the closure's `io::Result<Child>`).\n*169│fn placeholder_child() -> std::io::Result<Child> {\n 170│    #[cfg(windows)]\n 171│    {\n 172│        Command::new(\"cmd\")\n    │...\n 186│/// subprocess brain boots the proven production way.\n*187│fn hermetic(identity: Identity) -> NetConfig {\n 188│    NetConfig {\n 189│        identity,\n 190│        relay: RelayPolicy::Disabled,\n    │...\n 197│/// Bind a net-enabled broker (serving on a background thread) at `name`.\n*198│fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n 199│    let host = NetHost::start(hermetic(Identity::generate())).expect(\"net host start\");\n 200│    let broker = Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host))\n 201│        .expect(\"bind broker with net\");\n    │...\n 214│/// concrete stream is `Send + 'static` so the per-session reader thread can own it.\n*215│fn connect(name: &str) -> impl std::io::Read + std::io::Write + Send + 'static {\n 216│    for _ in 0..300 {\n 217│        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n 218│            send_hello(&mut c, Role::Brain).expect(\"hello\");\n    │...\n 229│/// controller's live output feed — it FREEZES the instant a resume-steal displaces it.\n*230│fn spawn_ticker_controller(name: &str, endpoint: &str) -> (u64, Arc<AtomicU64>) {\n 231│    let mut c = connect(name);\n 232│    send(\n 233│        &mut c,\n    │...\n 265│/// any, has already happened). Returns that pid, or `None` if it never appears in `budget`.\n*266│fn wait_ready_pid(path: &std::path::Path, budget: Duration) -> Option<u32> {\n 267│    let deadline = Instant::now() + budget;\n 268│    while Instant::now() < deadline {\n 269│        if let Some(pid) = std::fs::read_to_string(path)\n    │...\n 330│/// `msg` — keeps a failing assertion from orphaning the subprocess brain + the children.\n*331│fn teardown_panic(stop: &AtomicBool, msg: &str) -> ! {\n 332│    stop.store(true, Ordering::SeqCst);\n 333│    panic!(\"{msg}\");\n 334│}\n 335│\n*336│// [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n*337│#[test]\n*338│fn brain_respawn_keeps_every_session_controller_and_still_promotes() {\n 339│    let home = tempfile::tempdir().expect(\"tempdir\");\n 340│    std::env::set_var(\"SPT_HOME\", home.path());\n 341│    // Parity with trial_drain_drive: shrink the broker-side brain-write deadline (grants\n    │...\n 485│    // each slot, dropping A_i → its tally froze → gained==0. GREEN (Viewer): A_i untouched.\n*486│    // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n 487│    for (i, g) in gained.iter().enumerate() {\n 488│        assert!(\n 489│            *g > 0,\n    │...\n 503│    // old_gen_drained() is true and the ready candidate promotes.\n*504│    // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n 505│    assert!(\n 506│        promoted,\n 507│        \"the trial must still PROMOTE the candidate under N live sessions (no regression of \\\n### attach_wedge_e2e.rs\n  37│\n *38│fn kill_pid(pid: u32) {\n  39│    #[cfg(windows)]\n  40│    let _ = Command::new(\"taskkill\")\n  41│        .no_window()\n    │...\n  47│\n *48│fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {\n  49│    let (tx, rx) = std::sync::mpsc::channel();\n  50│    std::thread::spawn(move || {\n  51│        let _ = tx.send(cmd.output());\n    │...\n  57│\n *58│fn ready_pid(path: &Path) -> Option<u32> {\n  59│    let s = std::fs::read_to_string(path).ok()?;\n  60│    serde_json::from_str::<serde_json::Value>(&s)\n  61│        .ok()?\n    │...\n  66│\n *67│fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {\n  68│    let deadline = Instant::now() + budget;\n  69│    while Instant::now() < deadline {\n  70│        if let Some(p) = ready_pid(path) {\n    │...\n  77│\n *78│fn sibling_bin(name: &str) -> PathBuf {\n  79│    PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"))\n  80│        .with_file_name(format!(\"{name}{}\", std::env::consts::EXE_SUFFIX))\n  81│}\n    │...\n  85│/// `ENDPOINT_RUN:… pid=NNN` stderr machine token — the long-lived PTY child).\n *86│fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {\n  87│    let mut cmd = Command::new(spt_bin);\n  88│    cmd.no_window()\n  89│        .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id, \"--start\"])\n    │...\n 101│/// Poll `<id>`'s self-perch status until it equals `want` or the budget elapses.\n*102│fn wait_for_status(id: &str, want: &str, budget: Duration) -> bool {\n 103│    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\n 104│    let deadline = Instant::now() + budget;\n 105│    while Instant::now() < deadline {\n    │...\n 120│/// pipe (read → 0/Err) and exits.\n*121│fn rc_sees_tick(spt_bin: &Path, home: &Path, id: &str, window: Duration) -> bool {\n 122│    let mut rc: Child = Command::new(spt_bin)\n 123│        .no_window()\n 124│        .args([\"rc\", id])\n    │...\n 161│\n*162│#[test]\n*163│fn attach_wedge_dead_child_plus_dropped_pump_does_not_wedge_the_broker() {\n 164│    // ── (1) Isolated, NET-ENABLED SPT_HOME (rc attaches over a broker loopback conn\n 165│    //    that surfaces as net-stream events — needs a net-bound broker). ──\n 166│    let home = tempfile::tempdir().unwrap();\n    │...\n 262│        // STOP-LIVE-SESSION-WARN contract refuses a plain stop — this teardown\n*263│        // intends to kill it. [int->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\n 264│        cmd.no_window().args([\"daemon\", \"stop\", \"--force\"]).env(\"SPT_HOME\", home.path());\n 265│        output_bounded(cmd, Duration::from_secs(20))\n 266│    };\n    │...\n 302│    // L2 keystone: the broker survived the dead-child + dropped-pump and still serves.\n*303│    // [int->REQ-HAZARD-ATTACH-WEDGE]\n 304│    assert!(\n 305│        fresh_online,\n 306│        \"after a dead PTY child + a dropped operator pump, a NEW `endpoint run` must \\\n    │...\n 314│    // L3 (folded): the dead endpoint is offlined within a reconcile tick.\n*315│    // [int->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\n 316│    assert!(\n 317│        victim_offlined,\n 318│        \"the dead victim endpoint must be OFFLINED within a reconcile tick (its broker \\"},"isError":false,"timestamp":1784195824118}}
{"type":"message","id":"1224e352","parentId":"f884e6f4","timestamp":"2026-07-16T09:57:04.157Z","message":{"role":"toolResult","toolCallId":"call_hS1bI33yTuKmtR6Xs3y0bvB9|fc_008390f508d1d09b016a58aaeffb34819a91bba4521813b2c1","toolName":"read","content":[{"type":"text","text":"    /// connection, start its output drain and exit waiter, and reply `spawned`.\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\n    /// 7.4): a hung child blocks only its own threads — shell instances ride\n    /// exactly this isolation (one owner's hung shell binary stalls neither\n    /// another owner's delivery nor the broker's control dispatch).\n    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\n    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n        let req: SpawnReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n        // Single-flight WAKE (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP): the broker is the\n        // sole spawner post-W3, so its own spawn choke is the single-flight authority.\n        // Dedup a duplicate wake/resume for an endpoint that already has a live session\n        // OR a spawn in flight — closing the check-then-spawn TOCTOU that let one wake\n        // spawn two identical launch trees (perri's flynn dup: duplicate perch writers\n        // then stomped info.json, routing injects to a contended record). The claim +\n        // the live-session check are read together under one atomic critical section so\n        // a concurrent spawn either sees our claim or its registered session — never a\n        // gap; NO I/O under the lock (process_id reads a cached pid). An empty endpoint\n        // is an ad-hoc/legacy spawn (no id to dedup on) → never gated. `_wake_claim`\n        // holds the claim to fn end, releasing (RAII) after the session is table-visible\n        // or on any early error.\n        // [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\n        let _wake_claim: Option<WakeClaimGuard> = if req.endpoint.is_empty() {\n            None\n        } else {\n            let deadline = Instant::now() + WAKE_DEDUP_WAIT;\n            loop {\n                enum Gate<'a> {\n                    Claimed(WakeClaimGuard<'a>),\n                    AlreadyLive(u64, Option<u32>),\n                    Racing,\n                }\n                let gate = {\n                    let sessions = recover(&self.sessions);\n                    let mut inflight = recover(&self.wake_inflight);\n                    let live = sessions\n                        .iter()\n                        .find(|(_, h)| h.endpoint == req.endpoint)\n                        .map(|(sid, h)| (*sid, h.session.process_id()));\n                    match wake_gate_decision(\n                        live.is_some(),\n                        inflight.contains(&req.endpoint),\n                        Instant::now() >= deadline,\n                    ) {\n                        WakeGate::AlreadyLive => {\n                            let (sid, spid) = live.expect(\"live is Some on AlreadyLive\");\n                            Gate::AlreadyLive(sid, spid)\n                        }\n                        WakeGate::Racing => Gate::Racing,\n                        WakeGate::Claim => {\n                            inflight.insert(req.endpoint.clone());\n                            Gate::Claimed(WakeClaimGuard {\n                                set: &self.wake_inflight,\n                                endpoint: req.endpoint.clone(),\n                            })\n                        }\n                    }\n                };\n                match gate {\n                    Gate::Claimed(guard) => break Some(guard),\n                    Gate::AlreadyLive(sid, spid) => {\n                        eprintln!(\n                            \"SPAWN_DEDUP:{}: endpoint already has a live session {sid} — \\\n                             single-flight wake no-op ack [REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\",\n                            req.endpoint\n                        );\n                        let frame = Envelope::new(\n                            KIND_SPAWNED,\n                            serde_json::to_value(Spawned { session_id: sid, pid: spid })\n                                .expect(\"Spawned serializes\"),\n                        );\n                        send_frame(send, &frame);\n                        return Ok(sid);\n                    }\n                    Gate::Racing => thread::sleep(WAKE_DEDUP_POLL),\n                }\n            }\n        };\n        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;\n        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).\n        // Inject the manifest [env] vars (REQ-HAZARD-ENV-SUBST) on top of the\n        // inherited env — empty for a legacy/N-1 spawn (inherit only).\n        let session = PtySession::spawn_program_in_env(\n            &req.program,\n            &req.args,\n            SurfaceSize::new(req.rows, req.cols),\n            req.cwd.as_deref(),\n            req.env.iter().map(|(k, v)| (k.as_str(), v.as_str())),\n        )\n        .map_err(|e| format!(\"spawn failed: {e}\"))?;\n        let session = Arc::new(session);\n        let id = self.next_id.fetch_add(1, Ordering::Relaxed);\n        let pid = session.process_id();\n\n        // The session's lone PTY input-writer (P0): the SOLE caller of the\n        // blocking `write_input`. Every dispatch-side write enqueues here, so a\n        // paste burst parks only this thread — never the broker dispatch thread\n        // (REQ-HAZARD-PTY-INPUT-WRITER-WEDGE). The inject worker writes through it\n        // too, so the lone writer enforces the W2 inject-floor order.\n        let input = InputWriter::spawn(Arc::clone(&session), req.endpoint.clone());\n\n        // The output log, with the spawning connection pre-attached as the\n        // controller (identity `None` = LOCAL, not a remote driver) so no chunk\n        // is missed before an explicit subscribe — and so a remote attach to a\n        // spawned-but-undriven session takes control silently (no loud displace\n        // of the local-spawner placeholder; REQ-RCVIEW-1).\n        let log = Arc::new(Mutex::new(OutputLog::new(\n            id,\n            self.log_cap_chunks,\n            req.endpoint.clone(),\n            (req.rows, req.cols),\n        )));\n        log.lock()\n            .unwrap()\n            .become_controller(Arc::clone(send), None, 0);\n\n        // Output drain → append under the log lock (fast: ring + viewer fan-out),\n        // then deliver to the controller OFF the lock with a bounded deadline\n        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no\n        // longer inline under the log lock, so a backed-up controller can never\n        // wedge the session; a fully-stalled controller is detached after the\n        // deadline (bounded wedge, never permanent).\n        let log_drain = Arc::clone(&log);\n        let drain = session\n            .drain(move |chunk| {\n                // append() does the controller handoff inline now (one non-blocking\n                // try_send — b4) and returns Some(epoch) ONLY when the controller is\n                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\n                // the first lock (a fresh lock, no self-deadlock). The drain never\n                // sleeps here, so a slow controller can't throttle the viewer fan-out.\n                let evict = recover_log(&log_drain).append(chunk);\n                if let Some(epoch) = evict {\n                    recover_log(&log_drain).mark_controller_gone(epoch);\n                }\n            })\n            .map_err(|e| format!(\"drain failed: {e}\"))?;\n\n        // Exit waiter → one `exit` frame when the child is reaped, then the dead\n        // session is removed from the table. Runs on its own thread; `wait`\n        // holds the child lock but `kill` uses a split killer.\n        let sessions_exit = Arc::clone(&self.sessions);\n        let log_exit = Arc::clone(&log);\n        let waiter = Arc::clone(&session);\n        thread::spawn(move || {\n            if let Ok(status) = waiter.wait() {\n                let ev = ExitEvent {\n                    session_id: id,\n                    code: Some(status.exit_code() as i32),\n                };\n                let frame = Envelope::new(\n                    KIND_EXIT,\n                    serde_json::to_value(ev).expect(\"ExitEvent serializes\"),\n                );\n                // Send to every attached sink — controller + all viewers (under\n                // the log lock).\n                let sinks = recover_log(&log_exit).all_sinks();\n                for sink in sinks {\n                    // Bounded conn write (round-4): a wedged sink can no longer\n                    // park the exit-waiter thread indefinitely.\n                    let _ = sink.write(&frame);\n                }\n            }\n            // Reap-time unlatch (REQ-HAZARD-CONTROL-STAMP-LIFETIME): the child\n            // died, not the controller conn, so no detach path clears the perch\n            // — do it here so ONLINE+CONTROLLED / driven_by / viewer_count cannot\n            // outlive the session in info.json (or gossip a stale controller).\n            // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n            recover_log(&log_exit).stamp_reaped();\n            // Reap the dead session from the table.\n            recover(&sessions_exit).remove(&id);\n        });\n\n        // Layer C: if the spawn carried a translation binary, bring it up with the\n        // session — spawn the pure stdin→stdout filter + the inject worker that\n        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to SPOOLING\n        // (logged, `None` → idle inbound spools, never raw-injected) rather than\n        // aborting the session — a missing binary costs idle delivery, never the\n        // operator their agent.\n        // C-1: the shared bounded-respawn give-up counter (starts at 0; the worker\n        // resets it on a healthy commit, the dispatch respawn path increments it).\n        let translation_respawns = Arc::new(AtomicU32::new(0));\n        let translation = req\n            .translation_binary\n            .as_deref()\n            .and_then(|argv| {\n                build_translation(\n                    argv,\n                    &req.endpoint,\n                    &input,\n                    Arc::clone(&translation_respawns),\n                    &log,\n                )\n            });\n\n        recover(&self.sessions).insert(\n            id,\n            HostedSession {\n                session,\n                input,\n                drain,\n                log,\n                endpoint: req.endpoint.clone(),\n                translation,\n                translation_argv: req.translation_binary.clone(),\n                translation_respawns,\n                adapter: req.adapter.clone(),\n                install_dir: req.install_dir.clone().map(std::path::PathBuf::from),\n            },\n        );\n\n        let resp = Spawned {\n            session_id: id,\n            pid,\n        };\n        let frame = Envelope::new(\n            KIND_SPAWNED,\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\n        );\n        send_frame(send, &frame);\n        Ok(id)\n    }\n\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\n\n[Showing lines 3219-3443 of 6009. Use :3444 to continue]"}],"details":{"truncation":{"content":"    /// connection, start its output drain and exit waiter, and reply `spawned`.\r\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\r\n    /// 7.4): a hung child blocks only its own threads — shell instances ride\r\n    /// exactly this isolation (one owner's hung shell binary stalls neither\r\n    /// another owner's delivery nor the broker's control dispatch).\r\n    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\r\n    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\r\n        let req: SpawnReq =\r\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\r\n        // Single-flight WAKE (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP): the broker is the\r\n        // sole spawner post-W3, so its own spawn choke is the single-flight authority.\r\n        // Dedup a duplicate wake/resume for an endpoint that already has a live session\r\n        // OR a spawn in flight — closing the check-then-spawn TOCTOU that let one wake\r\n        // spawn two identical launch trees (perri's flynn dup: duplicate perch writers\r\n        // then stomped info.json, routing injects to a contended record). The claim +\r\n        // the live-session check are read together under one atomic critical section so\r\n        // a concurrent spawn either sees our claim or its registered session — never a\r\n        // gap; NO I/O under the lock (process_id reads a cached pid). An empty endpoint\r\n        // is an ad-hoc/legacy spawn (no id to dedup on) → never gated. `_wake_claim`\r\n        // holds the claim to fn end, releasing (RAII) after the session is table-visible\r\n        // or on any early error.\r\n        // [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\r\n        let _wake_claim: Option<WakeClaimGuard> = if req.endpoint.is_empty() {\r\n            None\r\n        } else {\r\n            let deadline = Instant::now() + WAKE_DEDUP_WAIT;\r\n            loop {\r\n                enum Gate<'a> {\r\n                    Claimed(WakeClaimGuard<'a>),\r\n                    AlreadyLive(u64, Option<u32>),\r\n                    Racing,\r\n                }\r\n                let gate = {\r\n                    let sessions = recover(&self.sessions);\r\n                    let mut inflight = recover(&self.wake_inflight);\r\n                    let live = sessions\r\n                        .iter()\r\n                        .find(|(_, h)| h.endpoint == req.endpoint)\r\n                        .map(|(sid, h)| (*sid, h.session.process_id()));\r\n                    match wake_gate_decision(\r\n                        live.is_some(),\r\n                        inflight.contains(&req.endpoint),\r\n                        Instant::now() >= deadline,\r\n                    ) {\r\n                        WakeGate::AlreadyLive => {\r\n                            let (sid, spid) = live.expect(\"live is Some on AlreadyLive\");\r\n                            Gate::AlreadyLive(sid, spid)\r\n                        }\r\n                        WakeGate::Racing => Gate::Racing,\r\n                        WakeGate::Claim => {\r\n                            inflight.insert(req.endpoint.clone());\r\n                            Gate::Claimed(WakeClaimGuard {\r\n                                set: &self.wake_inflight,\r\n                                endpoint: req.endpoint.clone(),\r\n                            })\r\n                        }\r\n                    }\r\n                };\r\n                match gate {\r\n                    Gate::Claimed(guard) => break Some(guard),\r\n                    Gate::AlreadyLive(sid, spid) => {\r\n                        eprintln!(\r\n                            \"SPAWN_DEDUP:{}: endpoint already has a live session {sid} — \\\r\n                             single-flight wake no-op ack [REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\",\r\n                            req.endpoint\r\n                        );\r\n                        let frame = Envelope::new(\r\n                            KIND_SPAWNED,\r\n                            serde_json::to_value(Spawned { session_id: sid, pid: spid })\r\n                                .expect(\"Spawned serializes\"),\r\n                        );\r\n                        send_frame(send, &frame);\r\n                        return Ok(sid);\r\n                    }\r\n                    Gate::Racing => thread::sleep(WAKE_DEDUP_POLL),\r\n                }\r\n            }\r\n        };\r\n        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;\r\n        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).\r\n        // Inject the manifest [env] vars (REQ-HAZARD-ENV-SUBST) on top of the\r\n        // inherited env — empty for a legacy/N-1 spawn (inherit only).\r\n        let session = PtySession::spawn_program_in_env(\r\n            &req.program,\r\n            &req.args,\r\n            SurfaceSize::new(req.rows, req.cols),\r\n            req.cwd.as_deref(),\r\n            req.env.iter().map(|(k, v)| (k.as_str(), v.as_str())),\r\n        )\r\n        .map_err(|e| format!(\"spawn failed: {e}\"))?;\r\n        let session = Arc::new(session);\r\n        let id = self.next_id.fetch_add(1, Ordering::Relaxed);\r\n        let pid = session.process_id();\r\n\r\n        // The session's lone PTY input-writer (P0): the SOLE caller of the\r\n        // blocking `write_input`. Every dispatch-side write enqueues here, so a\r\n        // paste burst parks only this thread — never the broker dispatch thread\r\n        // (REQ-HAZARD-PTY-INPUT-WRITER-WEDGE). The inject worker writes through it\r\n        // too, so the lone writer enforces the W2 inject-floor order.\r\n        let input = InputWriter::spawn(Arc::clone(&session), req.endpoint.clone());\r\n\r\n        // The output log, with the spawning connection pre-attached as the\r\n        // controller (identity `None` = LOCAL, not a remote driver) so no chunk\r\n        // is missed before an explicit subscribe — and so a remote attach to a\r\n        // spawned-but-undriven session takes control silently (no loud displace\r\n        // of the local-spawner placeholder; REQ-RCVIEW-1).\r\n        let log = Arc::new(Mutex::new(OutputLog::new(\r\n            id,\r\n            self.log_cap_chunks,\r\n            req.endpoint.clone(),\r\n            (req.rows, req.cols),\r\n        )));\r\n        log.lock()\r\n            .unwrap()\r\n            .become_controller(Arc::clone(send), None, 0);\r\n\r\n        // Output drain → append under the log lock (fast: ring + viewer fan-out),\r\n        // then deliver to the controller OFF the lock with a bounded deadline\r\n        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no\r\n        // longer inline under the log lock, so a backed-up controller can never\r\n        // wedge the session; a fully-stalled controller is detached after the\r\n        // deadline (bounded wedge, never permanent).\r\n        let log_drain = Arc::clone(&log);\r\n        let drain = session\r\n            .drain(move |chunk| {\r\n                // append() does the controller handoff inline now (one non-blocking\r\n                // try_send — b4) and returns Some(epoch) ONLY when the controller is\r\n                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\r\n                // the first lock (a fresh lock, no self-deadlock). The drain never\r\n                // sleeps here, so a slow controller can't throttle the viewer fan-out.\r\n                let evict = recover_log(&log_drain).append(chunk);\r\n                if let Some(epoch) = evict {\r\n                    recover_log(&log_drain).mark_controller_gone(epoch);\r\n                }\r\n            })\r\n            .map_err(|e| format!(\"drain failed: {e}\"))?;\r\n\r\n        // Exit waiter → one `exit` frame when the child is reaped, then the dead\r\n        // session is removed from the table. Runs on its own thread; `wait`\r\n        // holds the child lock but `kill` uses a split killer.\r\n        let sessions_exit = Arc::clone(&self.sessions);\r\n        let log_exit = Arc::clone(&log);\r\n        let waiter = Arc::clone(&session);\r\n        thread::spawn(move || {\r\n            if let Ok(status) = waiter.wait() {\r\n                let ev = ExitEvent {\r\n                    session_id: id,\r\n                    code: Some(status.exit_code() as i32),\r\n                };\r\n                let frame = Envelope::new(\r\n                    KIND_EXIT,\r\n                    serde_json::to_value(ev).expect(\"ExitEvent serializes\"),\r\n                );\r\n                // Send to every attached sink — controller + all viewers (under\r\n                // the log lock).\r\n                let sinks = recover_log(&log_exit).all_sinks();\r\n                for sink in sinks {\r\n                    // Bounded conn write (round-4): a wedged sink can no longer\r\n                    // park the exit-waiter thread indefinitely.\r\n                    let _ = sink.write(&frame);\r\n                }\r\n            }\r\n            // Reap-time unlatch (REQ-HAZARD-CONTROL-STAMP-LIFETIME): the child\r\n            // died, not the controller conn, so no detach path clears the perch\r\n            // — do it here so ONLINE+CONTROLLED / driven_by / viewer_count cannot\r\n            // outlive the session in info.json (or gossip a stale controller).\r\n            // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\r\n            recover_log(&log_exit).stamp_reaped();\r\n            // Reap the dead session from the table.\r\n            recover(&sessions_exit).remove(&id);\r\n        });\r\n\r\n        // Layer C: if the spawn carried a translation binary, bring it up with the\r\n        // session — spawn the pure stdin→stdout filter + the inject worker that\r\n        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to SPOOLING\r\n        // (logged, `None` → idle inbound spools, never raw-injected) rather than\r\n        // aborting the session — a missing binary costs idle delivery, never the\r\n        // operator their agent.\r\n        // C-1: the shared bounded-respawn give-up counter (starts at 0; the worker\r\n        // resets it on a healthy commit, the dispatch respawn path increments it).\r\n        let translation_respawns = Arc::new(AtomicU32::new(0));\r\n        let translation = req\r\n            .translation_binary\r\n            .as_deref()\r\n            .and_then(|argv| {\r\n                build_translation(\r\n                    argv,\r\n                    &req.endpoint,\r\n                    &input,\r\n                    Arc::clone(&translation_respawns),\r\n                    &log,\r\n                )\r\n            });\r\n\r\n        recover(&self.sessions).insert(\r\n            id,\r\n            HostedSession {\r\n                session,\r\n                input,\r\n                drain,\r\n                log,\r\n                endpoint: req.endpoint.clone(),\r\n                translation,\r\n                translation_argv: req.translation_binary.clone(),\r\n                translation_respawns,\r\n                adapter: req.adapter.clone(),\r\n                install_dir: req.install_dir.clone().map(std::path::PathBuf::from),\r\n            },\r\n        );\r\n\r\n        let resp = Spawned {\r\n            session_id: id,\r\n            pid,\r\n        };\r\n        let frame = Envelope::new(\r\n            KIND_SPAWNED,\r\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\r\n        );\r\n        send_frame(send, &frame);\r\n        Ok(id)\r\n    }\r\n\r\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\r\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\r\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\r","truncated":true,"truncatedBy":"lines","totalLines":2791,"totalBytes":11525,"outputLines":225,"outputBytes":11525,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    /// connection, start its output drain and exit waiter, and reply `spawned`.\r\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\r\n    /// 7.4): a hung child blocks only its own threads — shell instances ride\r\n    /// exactly this isolation (one owner's hung shell binary stalls neither\r\n    /// another owner's delivery nor the broker's control dispatch).\r\n    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\r\n    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\r\n        let req: SpawnReq =\r\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\r\n        // Single-flight WAKE (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP): the broker is the\r\n        // sole spawner post-W3, so its own spawn choke is the single-flight authority.\r\n        // Dedup a duplicate wake/resume for an endpoint that already has a live session\r\n        // OR a spawn in flight — closing the check-then-spawn TOCTOU that let one wake\r\n        // spawn two identical launch trees (perri's flynn dup: duplicate perch writers\r\n        // then stomped info.json, routing injects to a contended record). The claim +\r\n        // the live-session check are read together under one atomic critical section so\r\n        // a concurrent spawn either sees our claim or its registered session — never a\r\n        // gap; NO I/O under the lock (process_id reads a cached pid). An empty endpoint\r\n        // is an ad-hoc/legacy spawn (no id to dedup on) → never gated. `_wake_claim`\r\n        // holds the claim to fn end, releasing (RAII) after the session is table-visible\r\n        // or on any early error.\r\n        // [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\r\n        let _wake_claim: Option<WakeClaimGuard> = if req.endpoint.is_empty() {\r\n            None\r\n        } else {\r\n            let deadline = Instant::now() + WAKE_DEDUP_WAIT;\r\n            loop {\r\n                enum Gate<'a> {\r\n                    Claimed(WakeClaimGuard<'a>),\r\n                    AlreadyLive(u64, Option<u32>),\r\n                    Racing,\r\n                }\r\n                let gate = {\r\n                    let sessions = recover(&self.sessions);\r\n                    let mut inflight = recover(&self.wake_inflight);\r\n                    let live = sessions\r\n                        .iter()\r\n                        .find(|(_, h)| h.endpoint == req.endpoint)\r\n                        .map(|(sid, h)| (*sid, h.session.process_id()));\r\n                    match wake_gate_decision(\r\n                        live.is_some(),\r\n                        inflight.contains(&req.endpoint),\r\n                        Instant::now() >= deadline,\r\n                    ) {\r\n                        WakeGate::AlreadyLive => {\r\n                            let (sid, spid) = live.expect(\"live is Some on AlreadyLive\");\r\n                            Gate::AlreadyLive(sid, spid)\r\n                        }\r\n                        WakeGate::Racing => Gate::Racing,\r\n                        WakeGate::Claim => {\r\n                            inflight.insert(req.endpoint.clone());\r\n                            Gate::Claimed(WakeClaimGuard {\r\n                                set: &self.wake_inflight,\r\n                                endpoint: req.endpoint.clone(),\r\n                            })\r\n                        }\r\n                    }\r\n                };\r\n                match gate {\r\n                    Gate::Claimed(guard) => break Some(guard),\r\n                    Gate::AlreadyLive(sid, spid) => {\r\n                        eprintln!(\r\n                            \"SPAWN_DEDUP:{}: endpoint already has a live session {sid} — \\\r\n                             single-flight wake no-op ack [REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\",\r\n                            req.endpoint\r\n                        );\r\n                        let frame = Envelope::new(\r\n                            KIND_SPAWNED,\r\n                            serde_json::to_value(Spawned { session_id: sid, pid: spid })\r\n                                .expect(\"Spawned serializes\"),\r\n                        );\r\n                        send_frame(send, &frame);\r\n                        return Ok(sid);\r\n                    }\r\n                    Gate::Racing => thread::sleep(WAKE_DEDUP_POLL),\r\n                }\r\n            }\r\n        };\r\n        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;\r\n        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).\r\n        // Inject the manifest [env] vars (REQ-HAZARD-ENV-SUBST) on top of the\r\n        // inherited env — empty for a legacy/N-1 spawn (inherit only).\r\n        let session = PtySession::spawn_program_in_env(\r\n            &req.program,\r\n            &req.args,\r\n            SurfaceSize::new(req.rows, req.cols),\r\n            req.cwd.as_deref(),\r\n            req.env.iter().map(|(k, v)| (k.as_str(), v.as_str())),\r\n        )\r\n        .map_err(|e| format!(\"spawn failed: {e}\"))?;\r\n        let session = Arc::new(session);\r\n        let id = self.next_id.fetch_add(1, Ordering::Relaxed);\r\n        let pid = session.process_id();\r\n\r\n        // The session's lone PTY input-writer (P0): the SOLE caller of the\r\n        // blocking `write_input`. Every dispatch-side write enqueues here, so a\r\n        // paste burst parks only this thread — never the broker dispatch thread\r\n        // (REQ-HAZARD-PTY-INPUT-WRITER-WEDGE). The inject worker writes through it\r\n        // too, so the lone writer enforces the W2 inject-floor order.\r\n        let input = InputWriter::spawn(Arc::clone(&session), req.endpoint.clone());\r\n\r\n        // The output log, with the spawning connection pre-attached as the\r\n        // controller (identity `None` = LOCAL, not a remote driver) so no chunk\r\n        // is missed before an explicit subscribe — and so a remote attach to a\r\n        // spawned-but-undriven session takes control silently (no loud displace\r\n        // of the local-spawner placeholder; REQ-RCVIEW-1).\r\n        let log = Arc::new(Mutex::new(OutputLog::new(\r\n            id,\r\n            self.log_cap_chunks,\r\n            req.endpoint.clone(),\r\n            (req.rows, req.cols),\r\n        )));\r\n        log.lock()\r\n            .unwrap()\r\n            .become_controller(Arc::clone(send), None, 0);\r\n\r\n        // Output drain → append under the log lock (fast: ring + viewer fan-out),\r\n        // then deliver to the controller OFF the lock with a bounded deadline\r\n        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no\r\n        // longer inline under the log lock, so a backed-up controller can never\r\n        // wedge the session; a fully-stalled controller is detached after the\r\n        // deadline (bounded wedge, never permanent).\r\n        let log_drain = Arc::clone(&log);\r\n        let drain = session\r\n            .drain(move |chunk| {\r\n                // append() does the controller handoff inline now (one non-blocking\r\n                // try_send — b4) and returns Some(epoch) ONLY when the controller is\r\n                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\r\n                // the first lock (a fresh lock, no self-deadlock). The drain never\r\n                // sleeps here, so a slow controller can't throttle the viewer fan-out.\r\n                let evict = recover_log(&log_drain).append(chunk);\r\n                if let Some(epoch) = evict {\r\n                    recover_log(&log_drain).mark_controller_gone(epoch);\r\n                }\r\n            })\r\n            .map_err(|e| format!(\"drain failed: {e}\"))?;\r\n\r\n        // Exit waiter → one `exit` frame when the child is reaped, then the dead\r\n        // session is removed from the table. Runs on its own thread; `wait`\r\n        // holds the child lock but `kill` uses a split killer.\r\n        let sessions_exit = Arc::clone(&self.sessions);\r\n        let log_exit = Arc::clone(&log);\r\n        let waiter = Arc::clone(&session);\r\n        thread::spawn(move || {\r\n            if let Ok(status) = waiter.wait() {\r\n                let ev = ExitEvent {\r\n                    session_id: id,\r\n                    code: Some(status.exit_code() as i32),\r\n                };\r\n                let frame = Envelope::new(\r\n                    KIND_EXIT,\r\n                    serde_json::to_value(ev).expect(\"ExitEvent serializes\"),\r\n                );\r\n                // Send to every attached sink — controller + all viewers (under\r\n                // the log lock).\r\n                let sinks = recover_log(&log_exit).all_sinks();\r\n                for sink in sinks {\r\n                    // Bounded conn write (round-4): a wedged sink can no longer\r\n                    // park the exit-waiter thread indefinitely.\r\n                    let _ = sink.write(&frame);\r\n                }\r\n            }\r\n            // Reap-time unlatch (REQ-HAZARD-CONTROL-STAMP-LIFETIME): the child\r\n            // died, not the controller conn, so no detach path clears the perch\r\n            // — do it here so ONLINE+CONTROLLED / driven_by / viewer_count cannot\r\n            // outlive the session in info.json (or gossip a stale controller).\r\n            // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\r\n            recover_log(&log_exit).stamp_reaped();\r\n            // Reap the dead session from the table.\r\n            recover(&sessions_exit).remove(&id);\r\n        });\r\n\r\n        // Layer C: if the spawn carried a translation binary, bring it up with the\r\n        // session — spawn the pure stdin→stdout filter + the inject worker that\r\n        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to SPOOLING\r\n        // (logged, `None` → idle inbound spools, never raw-injected) rather than\r\n        // aborting the session — a missing binary costs idle delivery, never the\r\n        // operator their agent.\r\n        // C-1: the shared bounded-respawn give-up counter (starts at 0; the worker\r\n        // resets it on a healthy commit, the dispatch respawn path increments it).\r\n        let translation_respawns = Arc::new(AtomicU32::new(0));\r\n        let translation = req\r\n            .translation_binary\r\n            .as_deref()\r\n            .and_then(|argv| {\r\n                build_translation(\r\n                    argv,\r\n                    &req.endpoint,\r\n                    &input,\r\n                    Arc::clone(&translation_respawns),\r\n                    &log,\r\n                )\r\n            });\r\n\r\n        recover(&self.sessions).insert(\r\n            id,\r\n            HostedSession {\r\n                session,\r\n                input,\r\n                drain,\r\n                log,\r\n                endpoint: req.endpoint.clone(),\r\n                translation,\r\n                translation_argv: req.translation_binary.clone(),\r\n                translation_respawns,\r\n                adapter: req.adapter.clone(),\r\n                install_dir: req.install_dir.clone().map(std::path::PathBuf::from),\r\n            },\r\n        );\r\n\r\n        let resp = Spawned {\r\n            session_id: id,\r\n            pid,\r\n        };\r\n        let frame = Envelope::new(\r\n            KIND_SPAWNED,\r\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\r\n        );\r\n        send_frame(send, &frame);\r\n        Ok(id)\r\n    }\r\n\r\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\r\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\r\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\r","startLine":3219,"lineNumbers":[3219,3220,3221,3222,3223,3224,3225,3226,3227,3228,3229,3230,3231,3232,3233,3234,3235,3236,3237,3238,3239,3240,3241,3242,3243,3244,3245,3246,3247,3248,3249,3250,3251,3252,3253,3254,3255,3256,3257,3258,3259,3260,3261,3262,3263,3264,3265,3266,3267,3268,3269,3270,3271,3272,3273,3274,3275,3276,3277,3278,3279,3280,3281,3282,3283,3284,3285,3286,3287,3288,3289,3290,3291,3292,3293,3294,3295,3296,3297,3298,3299,3300,3301,3302,3303,3304,3305,3306,3307,3308,3309,3310,3311,3312,3313,3314,3315,3316,3317,3318,3319,3320,3321,3322,3323,3324,3325,3326,3327,3328,3329,3330,3331,3332,3333,3334,3335,3336,3337,3338,3339,3340,3341,3342,3343,3344,3345,3346,3347,3348,3349,3350,3351,3352,3353,3354,3355,3356,3357,3358,3359,3360,3361,3362,3363,3364,3365,3366,3367,3368,3369,3370,3371,3372,3373,3374,3375,3376,3377,3378,3379,3380,3381,3382,3383,3384,3385,3386,3387,3388,3389,3390,3391,3392,3393,3394,3395,3396,3397,3398,3399,3400,3401,3402,3403,3404,3405,3406,3407,3408,3409,3410,3411,3412,3413,3414,3415,3416,3417,3418,3419,3420,3421,3422,3423,3424,3425,3426,3427,3428,3429,3430,3431,3432,3433,3434,3435,3436,3437,3438,3439,3440,3441,3442,3443]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6009,"totalBytes":11525,"outputLines":225,"outputBytes":11525,"shownRange":{"start":3219,"end":3443},"nextOffset":3444}}},"isError":false,"timestamp":1784195824157}}
{"type":"message","id":"aa2921f9","parentId":"1224e352","timestamp":"2026-07-16T09:57:04.177Z","message":{"role":"toolResult","toolCallId":"call_Mlil1vPu96pFBlBbSvqL5vT9|fc_008390f508d1d09b016a58aaeffa08819aba2505a69f5d628b","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/\n\n### src/\n#### cli.rs\n 1475|            DaemonCmd::Status => cmd_daemon_status(json),\n*1476|            // [impl->REQ-DAEMON-REFRESH]\n*1477|            DaemonCmd::Refresh => cmd_daemon_refresh(),\n 1478|            DaemonCmd::Brain {\n 1479|                generation,\n 1480|                start_reason,\n...\n 4057|\n*4058|/// `spt daemon refresh` — cycle the daemon's brain in place with NO binary swap\n*4059|/// (REQ-DAEMON-REFRESH): the apply_staged brain-cycle path minus the swap, so the\n 4060|/// broker and every hosted PTY survive by construction. Requires a running\n*4061|/// daemon (nothing to refresh otherwise — `daemon start` is the cold path). A\n 4062|/// broker without a supervisor wired accepts but cannot act; that is loud, not\n 4063|/// silent — the operator asked for a cycle that did not happen.\n*4064|// [impl->REQ-DAEMON-REFRESH]\n*4065|fn cmd_daemon_refresh() -> i32 {\n 4066|    if !spt_daemon::is_running() {\n*4067|        eprintln!(\"DAEMON_NOT_RUNNING: nothing to refresh — `spt daemon start` brings it up fresh\");\n 4068|        return 1;\n 4069|    }\n*4070|    match spt_daemon::refresh_brain(&spt_daemon::broker_socket_name()) {\n 4071|        Ok(true) => {\n 4072|            println!(\n 4073|                \"Daemon coordinator restarted in place — hosted sessions and connections were \\\n...\n 4079|            eprintln!(\n*4080|                \"DAEMON_REFRESH_NOT_HONORED: the daemon accepted the request but has no \\\n 4081|                 supervisor to cycle its coordinator (an older or unmanaged daemon) — use \\\n 4082|                 `spt daemon stop` then `spt daemon start` instead (hosted sessions come back \\\n 4083|                 automatically).\"\n...\n 4087|        Err(e) => {\n*4088|            eprintln!(\"DAEMON_REFRESH_FAIL:{e}\");\n 4089|            1\n 4090|        }\n 4091|    }\n...\n 12719|    // REQ-UPDATE-DEFAULT-COMPOSITE, REQ-UPDATE-RESTART-SAFE-SWAP,\n*12720|    // REQ-DAEMON-REFRESH). ──\n 12721|\n 12722|    // [unit->REQ-UPDATE-ADAPTERS-VERB] comma-list parsing shared by both verb\n 12723|    // forms: None sweeps; items trim; duplicates drop first-wins; an empty item\n...\n 12926|\n*12927|    // [unit->REQ-DAEMON-REFRESH] the verb parses; the brain-cycle routing (no\n 12928|    // staging/swap preconditions) is unit-gated in spt-daemon applyhost.\n 12929|    #[test]\n*12930|    fn daemon_refresh_verb_parses() {\n 12931|        assert!(matches!(\n*12932|            parse(&[\"spt\", \"daemon\", \"refresh\"]).unwrap().cmd,\n 12933|            Some(Cmd::Daemon {\n*12934|                action: Some(DaemonCmd::Refresh)\n 12935|            })\n 12936|        ));\n 12937|    }\n\n### tests/\n#### daemon_refresh_e2e.rs\n*1|//! THE-FORKENING W4 — `spt daemon refresh` on a LIVE daemon with a hosted\n*2|//! endpoint (REQ-DAEMON-REFRESH): the brain generation cycles (new brain pid,\n 3|//! generation bumped) with NO binary swap, while the broker process, the hosted\n 4|//! harness session, and the endpoint's ONLINE perch all survive untouched — the\n 5|//! apply_staged brain-cycle path minus the swap, driven through the REAL CLI\n...\n 13|//\n*14|// [int->REQ-DAEMON-REFRESH]\n 15|\n 16|use std::path::{Path, PathBuf};\n 17|use std::process::{Child, Command, Output, Stdio};\n...\n 75|#[test]\n*76|fn daemon_refresh_cycles_brain_while_hosted_endpoint_survives() {\n 77|    let now_ms = || {\n 78|        std::time::SystemTime::now()\n 79|            .duration_since(std::time::UNIX_EPOCH)\n...\n 182|\n*183|    // ── (5) THE VERB: `spt daemon refresh` — the production CLI path. ──\n 184|    let refresh = {\n 185|        let mut cmd = Command::new(&spt_bin);\n 186|        cmd.no_window()\n*187|            .args([\"daemon\", \"refresh\"])\n 188|            .env(\"SPT_HOME\", home.path());\n 189|        output_bounded(cmd, Duration::from_secs(30))\n 190|    };\n...\n 216|    eprintln!(\n*217|        \"=== W4 daemon refresh: run_ok={} online={online} harness_pid={harness_pid:?} \\\n 218|         refresh_ok={} brain_before={brain_pid_before}/g{gen_before} cycled={cycled:?} \\\n 219|         broker_alive={broker_still_running} harness_alive_after={harness_alive_after} \\\n 220|         still_online={still_online} ===\\n--- refresh stdout ---\\n{refresh_stdout}\\n\\\n...\n 260|        refresh.status.success(),\n*261|        \"REQ-DAEMON-REFRESH: `spt daemon refresh` must exit 0 against a live daemon \\\n 262|         (stdout: {refresh_stdout}; stderr: {refresh_stderr}).\\n\\\n 263|         === brain stderr ===\\n{brain_stderr}\"\n 264|    );\n...\n 266|        panic!(\n*267|            \"REQ-DAEMON-REFRESH: the brain must CYCLE (a new pid must stamp brain.ready) \\\n 268|             after refresh — pid stayed {brain_pid_before}.\\n=== brain stderr ===\\n{brain_stderr}\"\n 269|        )\n 270|    });\n...\n 276|        gen_after > gen_before,\n*277|        \"REQ-DAEMON-REFRESH: the brain GENERATION must advance across a refresh \\\n 278|         (before g{gen_before}, after g{gen_after}) — the supervisor cycle, not a re-stamp.\\n\\\n 279|         === brain stderr ===\\n{brain_stderr}\"\n 280|    );\n...\n 282|        broker_still_running,\n*283|        \"REQ-DAEMON-REFRESH: the broker process must survive the refresh untouched \\\n 284|         (pid {broker_pid}).\\n=== brain stderr ===\\n{brain_stderr}\"\n 285|    );\n 286|    assert!(\n 287|        harness_alive_after,\n*288|        \"REQ-DAEMON-REFRESH: the hosted harness session must STILL be ALIVE across the \\\n 289|         refresh — only the brain cycles, never the hosted PTYs. harness_pid={harness_pid:?}.\\n\\\n 290|         === brain stderr ===\\n{brain_stderr}\"\n 291|    );\n...\n 293|        still_online,\n*294|        \"REQ-DAEMON-REFRESH: the endpoint perch must remain ONLINE across the refresh \\\n 295|         (the session was never torn down).\\n=== brain stderr ===\\n{brain_stderr}\"\n 296|    );\n 297|}\n#### resume_no_control_steal_e2e.rs\n 12|//! retake. ROOT (SME docs/UPDATE-WEDGE-2-SME-todlando.md): `run_brain` calls\n*13|//! `resume_sessions()` at boot, which re-attached EVERY session `KIND_SESSIONS` returns as\n 14|//! `subscribe_with(AttachIntent::Control, by:None)`. The docstring's \"a None identity never\n 15|//! displaces (falls back to viewer)\" is a MYTH for FREE / SAME-LOCAL-IDENTITY slots:\n 16|//! `resolve_subscribe` (broker.rs) stall-evicts first, then `become_controller` when the\n...\n 41|//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` no longer\n*42|//! calls `resume_sessions` at all — it uses the cursor-only `resume_session_cursors` (NO\n 43|//! subscribe), which supersedes the Viewer re-attach as the no-steal cure (no attach ⇒ no\n 44|//! steal, a fortiori) AND fixes the SharedSend self-deadlock. This rig's no-steal\n 45|//! discriminator (the operator's `A_i` controllers keep ticking across the swap) therefore\n...\n 57|//!   `A_i` counts `KIND_OUTPUT` frames from a child that ticks forever (~150ms).\n*58|//! - The real `spt daemon brain` candidate boots and `resume_sessions()` re-attaches all N.\n 59|//! - **RED (Control re-attach):** `resolve_subscribe` `same_identity(None==None)` →\n 60|//!   `become_controller` DROPS `A_i`'s sink → `A_i` is displaced → its tick counter FREEZES.\n 61|//! - **GREEN (Viewer re-attach):** the brain joins as a viewer → `A_i` stays controller →\n...\n 353|    // ── Establish N broker PTY sessions BEFORE the brain boots, so the candidate's\n*354|    //    boot-once resume_sessions() COVERS them (the field condition: live PTYs present at\n 355|    //    the respawn). Each conn A_i is the session's local (by:None) controller. ──\n 356|    let mut sessions: Vec<(u64, Arc<AtomicU64>)> = Vec::with_capacity(N_SESSIONS);\n 357|    for i in 0..N_SESSIONS {\n#### trial_drain_drive_e2e.rs\n 28|//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` now calls the\n*29|//! cursor-only `resume_session_cursors()` (seed cursors, NO subscribe/attach), not\n*30|//! `resume_sessions()`. So resume never attaches to (or displaces) any controller — the\n 31|//! \"resume paradox\" below is now moot (a wedge present at resume-time is untouched), which\n 32|//! only makes this rig MORE faithful; it still establishes the wedge after boot for\n*33|//! continuity. The historical `resume_sessions()` framing is retained below for context.\n 34|//!\n*35|//! `run_brain` (historically) called `resume_sessions()` ONCE at boot, which re-Control-attaches\n 36|//! (by:None) EVERY session `KIND_SESSIONS` returns — so a wedge present at resume-time\n 37|//! would be DISPLACED (cleared) before the trial ever gates, and the rig could never\n 38|//! bite. In the field `resume_sessions` returned EMPTY (the supervised daemon brain\n\n## spt-daemon/\n\n### src/\n#### applyhost.rs\n 28|//!    half-swapped seat.\n*29|//! 4. **Trigger a brain-PROCESS restart** ([`Brain::request_brain_restart`],\n 30|//!    ADR-0018 D3-3): the broker signals its supervisor to cycle the brain\n 31|//!    child onto the just-swapped binary with `start-reason=update`. No hosted\n 32|//!    endpoint terminates or suspends (REQ-UPD-3, absolute for this class — the\n...\n 338|    // promotion + auto-rollback on a brain that fails to come up is D6.\n*339|    match brain.request_brain_restart() {\n 340|        Ok(_honored) => Ok(ApplyStagedOutcome::AppliedPending {\n 341|            version,\n 342|            product_version,\n...\n 352|/// Cycle the brain of a RUNNING daemon with NO binary swap and NO staging\n*353|/// preconditions (REQ-DAEMON-REFRESH, `spt daemon refresh`): connect to the\n 354|/// broker and raise the same supervisor brain-restart signal the post-swap\n*355|/// apply rides ([`Brain::request_brain_restart`], ADR-0018 D3-3) — brain stop →\n 356|/// respawn → readiness trial → promote, broker + every hosted PTY untouched by\n 357|/// construction. The recovery verb for wedged brain-held state that previously\n 358|/// needed a full daemon bounce. Nothing is verified, staged, or renamed: the\n...\n 362|/// (accepted, not acted on).\n*363|// [impl->REQ-DAEMON-REFRESH]\n 364|pub fn refresh_brain(broker_name: &str) -> Result<bool, String> {\n 365|    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n 366|    brain\n*367|        .request_brain_restart()\n 368|        .map_err(|e| format!(\"brain-restart trigger: {e}\"))\n 369|}\n 370|\n...\n 792|    // brain-restart verb (D3-3): a broker WITH a supervisor signal wired raises\n*793|    // it on `KIND_BRAIN_RESTART` and acks `honored: true`; a bare broker (no\n 794|    // supervisor — the test/no-daemon shape) accepts the verb but acks\n 795|    // `honored: false`. This is the broker half of the apply trigger that\n 796|    // replaces the in-process handoff.\n 797|    #[test]\n*798|    fn brain_restart_verb_raises_the_supervisor_signal() {\n 799|        let name = unique_name();\n 800|        let broker = served_broker(&name);\n 801|\n...\n 804|        assert!(\n*805|            !b.request_brain_restart().expect(\"verb round-trips\"),\n 806|            \"a broker with no supervisor wired must ack honored=false\"\n 807|        );\n 808|\n...\n 810|        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n*811|        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n 812|        let mut b2 = cold_connect_retry(&name);\n 813|        assert!(\n*814|            b2.request_brain_restart().expect(\"verb round-trips\"),\n 815|            \"a broker with a supervisor wired must ack honored=true\"\n 816|        );\n 817|        assert!(\n...\n 822|\n*823|    // [unit->REQ-DAEMON-REFRESH] `refresh_brain` routes the SAME supervisor\n 824|    // brain-cycle signal as the post-swap apply — against a bare broker with NO\n 825|    // staged release, NO release cache, NO binary swap. The verb has no staging\n 826|    // preconditions: the signal raises and `honored` reports the supervisor\n...\n 841|        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n*842|        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n 843|        assert!(\n 844|            refresh_brain(&name).expect(\"refresh round-trips\"),\n 845|            \"a wired supervisor must report honored=true\"\n#### brain.rs\n 45|    ViewerEvictedEvent,\n*46|    KIND_ADAPTER_APPLY, KIND_APPLIED, KIND_BRAIN_RESTART, KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_VIEWER_EVICTED,\n*47|    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,\n 48|    KIND_NET_DIALED, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT, KIND_NET_DIAL_SUBMITTED,\n 49|    KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE,\n 50|    KIND_NET_SENT,\n...\n 1038|    /// session — brainproc.rs:184 — and nothing on its side consumes the output, §3\n*1039|    /// guard), so it MUST call [`Self::resume_session_cursors`] (cursor-only, NO\n 1040|    /// subscribe) instead. This subscribing method stays for a DRAINING caller (the\n 1041|    /// resume int tests today; the forward live-agent-adapter / Option-B drained carrier).\n 1042|    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n...\n 1045|    // [doc->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n*1046|    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {\n 1047|        let reply = self.sessions()?;\n 1048|        let mut resumed = Vec::with_capacity(reply.sessions.len());\n 1049|        for info in reply.sessions {\n...\n 1076|    // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n*1077|    pub fn resume_session_cursors(&mut self) -> io::Result<Vec<u64>> {\n 1078|        let reply = self.sessions()?;\n 1079|        let mut resumed = Vec::with_capacity(reply.sessions.len());\n 1080|        for info in reply.sessions {\n...\n 1458|    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*1459|    pub fn request_brain_restart(&mut self) -> io::Result<bool> {\n*1460|        self.send(KIND_BRAIN_RESTART, serde_json::Value::Null)?;\n 1461|        loop {\n 1462|            match self.read_event()? {\n*1463|                BrokerEvent::Other(env) if env.kind == KIND_BRAIN_RESTARTED => {\n 1464|                    let reply: BrainRestarted = serde_json::from_value(env.payload)\n 1465|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 1466|                    return Ok(reply.honored);\n#### brainproc.rs\n 106|/// The broker→supervisor **planned-restart signal** (ADR-0018 D3-3, Q3). The\n*107|/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n 108|/// its next wait tick, kills the current brain child, and respawns it with\n 109|/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n 110|/// between the broker serve path and the supervisor — both run in the broker\n...\n 196|    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n*197|    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n 198|    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n 199|    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n 200|    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n 201|    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n*202|    match brain.resume_session_cursors() {\n 203|        Ok(ids) if !ids.is_empty() => {\n 204|            eprintln!(\n 205|                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n...\n 920|            backoff = base;\n*921|            eprintln!(\"BRAIN_RESTART: planned update respawn (generation {generation})\");\n 922|            continue;\n 923|        }\n 924|        reason = StartReason::Crash;\n 925|        backoff = next_backoff(backoff, started.elapsed(), base);\n*926|        eprintln!(\"BRAIN_RESTART: supervised respawn in {}s\", backoff.as_secs());\n 927|        sleep_backoff(backoff, stop);\n 928|    }\n 929|}\n...\n 1092|/// Returns the shared [`BrainRestart`] signal so the caller can hand a clone to\n*1093|/// the broker, wiring the `KIND_BRAIN_RESTART` verb to this supervisor (D3-3).\n 1094|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 1095|pub fn spawn_brain_supervisor(\n 1096|    stop: Arc<AtomicBool>,\n...\n 1205|    /// The planned-restart path (D3-3, Q3): a `BrainRestart::request` (the\n*1206|    /// broker's `KIND_BRAIN_RESTART` handler) makes the supervisor cycle the\n 1207|    /// brain and stamp the next spawn `Update` — distinguishing an intentional\n 1208|    /// update cycle from a crash (V2, broker-authoritative). A long-lived child\n 1209|    /// is used so the restart, not an exit, is what ends the cycle.\n#### broker.rs\n 59|    NetStreamsReply, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n*60|    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n 61|    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n 62|    KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,\n 63|    KIND_NET_SENT,\n...\n 2541|    /// The broker→supervisor planned-restart signal (ADR-0018 D3-3): a\n*2542|    /// `KIND_BRAIN_RESTART` handler raises it so the brain supervisor cycles the\n 2543|    /// child onto a freshly-swapped binary. Empty on a bare broker (a test broker\n 2544|    /// with no supervisor) — the verb is then accepted but unhonored. Set once by\n 2545|    /// `Daemon::run` after it spawns the supervisor.\n*2546|    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,\n 2547|    /// Held meet results for the two-phase join (REQ-JOIN-TWO-PHASE): a\n 2548|    /// `session_id → (subnet, real address, 5-min deadline)` between\n 2549|    /// [`KIND_PAIR_MEET`] and [`KIND_PAIR_CODE_SUBMIT`]. The code phase dials the\n...\n 2696|            net: net_cell,\n*2697|            brain_restart: std::sync::OnceLock::new(),\n 2698|            pair_holds: Mutex::new(HashMap::new()),\n 2699|            wake_inflight: Mutex::new(HashSet::new()),\n 2700|            conn_handlers: AtomicUsize::new(0),\n...\n 2704|    /// Wire the brain supervisor's planned-restart signal (ADR-0018 D3-3) so the\n*2705|    /// `KIND_BRAIN_RESTART` verb can drive a real brain-process restart. Called\n 2706|    /// once by `Daemon::run` after spawning the supervisor; idempotent by\n 2707|    /// `OnceLock` (the first wins). A broker without it accepts the verb but\n 2708|    /// reports `honored: false` (no supervisor to respawn the child).\n 2709|    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*2710|    pub fn set_brain_restart(&self, signal: Arc<crate::brainproc::BrainRestart>) -> bool {\n*2711|        self.brain_restart.set(signal).is_ok()\n 2712|    }\n 2713|\n 2714|    /// Attach a network host to a broker that was bound net-less (REQ-DAEMON-9):\n...\n 3190|                }\n*3191|                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n 3192|                // An unknown kind is tolerated (forward-compat): report, don't die.\n 3193|                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n 3194|            }\n...\n 3884|    // [impl->REQ-UPD-3]\n*3885|    fn dispatch_brain_restart(&self, send: &SharedSend) {\n*3886|        let honored = match self.brain_restart.get() {\n 3887|            Some(signal) => {\n 3888|                signal.request();\n 3889|                true\n...\n 3893|        let frame = Envelope::new(\n*3894|            KIND_BRAIN_RESTARTED,\n 3895|            serde_json::to_value(BrainRestarted { honored })\n 3896|                .expect(\"BrainRestarted serializes\"),\n 3897|        );\n#### daemon.rs\n 187|        // planned-restart signal can be wired into it below (ADR-0018 D3-3): the\n*188|        // `KIND_BRAIN_RESTART` verb the broker serves must reach the supervisor.\n 189|        let broker_handle: Option<Arc<Broker>> =\n 190|            match Broker::bind_in_with_net(&broker_socket_name(), default_journal_path(), net) {\n 191|                Ok(broker) => {\n...\n 315|        // makes an update a *planned* restart on this same path: the supervisor's\n*316|        // `BrainRestart` signal is wired into the broker so the `KIND_BRAIN_RESTART`\n 317|        // verb (sent by `spt update apply` after the binary swap) cycles the brain\n 318|        // onto the new binary with `start-reason=update`. The stop flag is held\n 319|        // for symmetry, never raised (the daemon runs until killed). The brain\n...\n 343|            if let Some(broker) = &broker_handle {\n*344|                broker.set_brain_restart(restart);\n 345|            }\n 346|        }\n 347|\n#### lib.rs\n 180|};\n*181|pub use applyhost::{apply_staged, apply_staged_daemonless, refresh_brain, ApplyStagedOutcome};\n 182|pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};\n 183|pub use brain::{Brain, BrainState, BrokerEvent};\n 184|pub use broker::Broker;\n#### msg.rs\n 231|/// which the caller surfaces — it never silently no-ops a swapped binary.\n*232|pub const KIND_BRAIN_RESTART: &str = \"brain-restart\";\n*233|/// Broker→client ack for [`KIND_BRAIN_RESTART`]: the restart was *accepted*\n 234|/// (the supervisor was signalled). Readiness/rollback gating is D6 — this ack is\n 235|/// acceptance, not completion. `honored` is false when the broker has no\n 236|/// supervisor wired (e.g. a bare test broker), so the caller knows the trigger\n 237|/// reached a broker that cannot itself respawn.\n*238|pub const KIND_BRAIN_RESTARTED: &str = \"brain-restarted\";\n 239|\n 240|/// `spawn` payload — the program + args + initial geometry. The broker spawns\n 241|/// it under a PTY (no shell interpretation; argv is passed through).\n...\n 871|\n*872|/// `brain-restarted` payload — the broker's ack for [`KIND_BRAIN_RESTART`]\n 873|/// (ADR-0018 D3-3). `honored` is true when a brain supervisor was signalled to\n 874|/// cycle the child; false when the broker has none wired (a bare test broker),\n 875|/// so the caller can tell a real planned restart from a mere accept.\n#### seedmap.rs\n 37|\n*38|/// Client→daemon: store (or refresh) a seed in the in-memory map.\n 39|pub const KIND_SEED_PUT: &str = \"seed_put\";\n 40|/// Client→daemon: consume the seed for a `parent_pid` (take-once).\n 41|pub const KIND_SEED_TAKE: &str = \"seed_take\";\n\n### tests/\n#### attach.rs\n 778|#[test]\n*779|fn attach_survives_target_brain_restart_exactly_once() {\n 780|    init_home();\n 781|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 782|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 1375|// keystone (`controller_writer_reorder_*`) and the restart carrier\n*1376|// (`attach_survives_target_brain_restart_exactly_once`); HERE we lock the\n 1377|// load-bearing post-conditions of `handoff` itself, deterministically.\n 1378|#[test]\n 1379|fn handoff_seeds_resume_cursor_and_resubscribes_for_resume() {\n#### brain_resume_conn_deadlock.rs\n 133|/// How the probe brain resumes before running the heartbeat: the SHIPPING daemon\n*134|/// path — `CursorOnly` → `resume_session_cursors`, no subscribe. (The hazard path,\n 135|/// `resume_sessions`, is exercised directly by the deterministic zero-subscriber guard\n 136|/// `daemon_resume_leaves_zero_brain_subscribers` below, not through this probe.)\n 137|#[derive(Clone, Copy)]\n...\n 162|            ResumeMode::CursorOnly => {\n*163|                let _ = brain.resume_session_cursors();\n 164|            }\n 165|        }\n 166|        while !stop.load(Ordering::Relaxed) {\n...\n 216|/// STEADY-STATE severity — the SHIPPING path: the daemon brain resumes CURSOR-ONLY\n*217|/// (`resume_session_cursors`, no subscribe) while N sessions actively stream, so its\n 218|/// heartbeat stays live. NO dead peer anywhere. RED-first: revert run_brain to the\n 219|/// subscribing `resume_sessions` (or see the regression-guard test below) and this\n 220|/// starves to a standstill.\n...\n 265|/// Two settled arms, no timing window:\n*266|///   POSITIVE — the shipping daemon path (`resume_session_cursors`) must leave EVERY\n 267|///   hosted session with ZERO viewers. A resume that instead subscribed the undrained\n 268|///   heartbeat conn re-opens the round-3 SharedSend self-deadlock class; post-r4 it\n 269|///   would instead get that heartbeat conn poisoned/retired by the write watchdog —\n...\n 315|    let mut brain = Brain::cold_start(&name, 0).expect(\"cold_start probe brain\");\n*316|    brain.resume_session_cursors().expect(\"cursor-only resume\");\n 317|    for &sid in &sids {\n 318|        assert_eq!(\n 319|            broker.test_session_viewer_count(sid),\n...\n 333|    let mut hazard_brain = Brain::cold_start(&name, 0).expect(\"cold_start hazard brain\");\n*334|    hazard_brain.resume_sessions().expect(\"hazard subscribe resume\");\n 335|    // Deterministic barrier: subscribe_with is fire-and-forget and resume_sessions'\n 336|    // internal round-trip precedes the subscribes, so flush with ONE more same-conn\n 337|    // sessions() round-trip. Per-conn in-order dispatch means all 3 KIND_SUBSCRIBE\n#### daemon_e2e.rs\n 129|#[test]\n*130|fn daemon_hosts_lifecycle_and_survives_brain_restart() {\n 131|    // Isolate all on-disk perch state (the Self perch, the nested Psyche perch,\n 132|    // the commune drops) to a temp home.\n 133|    let home = tempfile::tempdir().expect(\"tempdir\");\n#### handoff.rs\n 78|#[test]\n*79|fn brain_restart_survives_gaplessly_with_fresh_gen_start() {\n 80|    const RESTARTS: u64 = 12; // >= 10 (B2 acceptance)\n 81|    let name = unique_name();\n 82|    let broker = Broker::bind(&name).expect(\"bind broker\");\n#### netbroker.rs\n 154|#[test]\n*155|fn replayed_dial_op_is_deduped_across_brain_restart() {\n 156|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 157|    let (name_a, name_b) = (unique_name(), unique_name());\n 158|    let broker_a = net_broker(&name_a, Identity::generate(), &dir.path().join(\"a\"));\n#### netstream.rs\n 144|#[test]\n*145|fn receiver_brain_restart_is_gapless_and_exactly_once() {\n 146|    const TOTAL: u64 = 20;\n 147|    const KILL_AT: u64 = 5; // receiver dies after reading this many chunks\n 148|\n...\n 220|#[test]\n*221|fn sender_brain_restart_redrive_is_exactly_once() {\n 222|    const TOTAL: u64 = 8;\n 223|    const CRASH_AFTER: u64 = 3; // acked sends before the crash\n 224|\n#### presence.rs\n 82|#[test]\n*83|fn presence_survives_brain_restart_with_cursor_resume() {\n 84|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 85|    let name_b = unique_name();\n 86|    let _broker_b = net_broker(&name_b, dir.path());\n#### resume.rs\n 188|    let mut r = connect_retry(&name);\n*189|    let mut resumed = r.resume_sessions().expect(\"resume all hosted sessions\");\n 190|    resumed.sort_unstable();\n 191|    let mut want = vec![sid_a, sid_b];\n 192|    want.sort_unstable();\n...\n 259|    let resumed = r\n*260|        .resume_sessions()\n 261|        .expect(\"resume succeeds despite eviction\");\n 262|    assert_eq!(resumed, vec![sid], \"the session re-attached\");\n 263|\n...\n 329|    let mut r = connect_retry(&name);\n*330|    let resumed = r.resume_sessions().expect(\"resume\");\n 331|    assert_eq!(resumed, vec![seed_sid], \"resumed the seed session\");\n 332|\n 333|    // First fresh spawn: delivers under both old and new code (it is the one\n\nShowing files 1-20 of 21. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"crates/spt/src, crates/spt-daemon/src, crates/spt/tests, crates/spt-daemon/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":102,"fileCount":20,"files":["crates/spt/src/cli.rs","crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/src/seedmap.rs","crates/spt/tests/daemon_refresh_e2e.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt/tests/trial_drain_drive_e2e.rs","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","crates/spt-daemon/tests/daemon_e2e.rs","crates/spt-daemon/tests/handoff.rs","crates/spt-daemon/tests/netbroker.rs","crates/spt-daemon/tests/netstream.rs","crates/spt-daemon/tests/presence.rs","crates/spt-daemon/tests/resume.rs"],"fileMatches":[{"path":"crates/spt/src/cli.rs","count":16},{"path":"crates/spt-daemon/src/applyhost.rs","count":13},{"path":"crates/spt-daemon/src/brain.rs","count":8},{"path":"crates/spt-daemon/src/brainproc.rs","count":7},{"path":"crates/spt-daemon/src/broker.rs","count":11},{"path":"crates/spt-daemon/src/daemon.rs","count":3},{"path":"crates/spt-daemon/src/lib.rs","count":1},{"path":"crates/spt-daemon/src/msg.rs","count":4},{"path":"crates/spt-daemon/src/seedmap.rs","count":1},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","count":13},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","count":4},{"path":"crates/spt/tests/trial_drain_drive_e2e.rs","count":4},{"path":"crates/spt-daemon/tests/attach.rs","count":2},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","count":6},{"path":"crates/spt-daemon/tests/daemon_e2e.rs","count":1},{"path":"crates/spt-daemon/tests/handoff.rs","count":1},{"path":"crates/spt-daemon/tests/netbroker.rs","count":1},{"path":"crates/spt-daemon/tests/netstream.rs","count":2},{"path":"crates/spt-daemon/tests/presence.rs","count":1},{"path":"crates/spt-daemon/tests/resume.rs","count":3}],"truncated":true,"fileLimitReached":20,"displayContent":"# crates/\n\n## spt/\n\n### src/\n#### cli.rs\n  1475│            DaemonCmd::Status => cmd_daemon_status(json),\n *1476│            // [impl->REQ-DAEMON-REFRESH]\n *1477│            DaemonCmd::Refresh => cmd_daemon_refresh(),\n  1478│            DaemonCmd::Brain {\n  1479│                generation,\n  1480│                start_reason,\n      │...\n  4057│\n *4058│/// `spt daemon refresh` — cycle the daemon's brain in place with NO binary swap\n *4059│/// (REQ-DAEMON-REFRESH): the apply_staged brain-cycle path minus the swap, so the\n  4060│/// broker and every hosted PTY survive by construction. Requires a running\n *4061│/// daemon (nothing to refresh otherwise — `daemon start` is the cold path). A\n  4062│/// broker without a supervisor wired accepts but cannot act; that is loud, not\n  4063│/// silent — the operator asked for a cycle that did not happen.\n *4064│// [impl->REQ-DAEMON-REFRESH]\n *4065│fn cmd_daemon_refresh() -> i32 {\n  4066│    if !spt_daemon::is_running() {\n *4067│        eprintln!(\"DAEMON_NOT_RUNNING: nothing to refresh — `spt daemon start` brings it up fresh\");\n  4068│        return 1;\n  4069│    }\n *4070│    match spt_daemon::refresh_brain(&spt_daemon::broker_socket_name()) {\n  4071│        Ok(true) => {\n  4072│            println!(\n  4073│                \"Daemon coordinator restarted in place — hosted sessions and connections were \\\n      │...\n  4079│            eprintln!(\n *4080│                \"DAEMON_REFRESH_NOT_HONORED: the daemon accepted the request but has no \\\n  4081│                 supervisor to cycle its coordinator (an older or unmanaged daemon) — use \\\n  4082│                 `spt daemon stop` then `spt daemon start` instead (hosted sessions come back \\\n  4083│                 automatically).\"\n      │...\n  4087│        Err(e) => {\n *4088│            eprintln!(\"DAEMON_REFRESH_FAIL:{e}\");\n  4089│            1\n  4090│        }\n  4091│    }\n      │...\n 12719│    // REQ-UPDATE-DEFAULT-COMPOSITE, REQ-UPDATE-RESTART-SAFE-SWAP,\n*12720│    // REQ-DAEMON-REFRESH). ──\n 12721│\n 12722│    // [unit->REQ-UPDATE-ADAPTERS-VERB] comma-list parsing shared by both verb\n 12723│    // forms: None sweeps; items trim; duplicates drop first-wins; an empty item\n      │...\n 12926│\n*12927│    // [unit->REQ-DAEMON-REFRESH] the verb parses; the brain-cycle routing (no\n 12928│    // staging/swap preconditions) is unit-gated in spt-daemon applyhost.\n 12929│    #[test]\n*12930│    fn daemon_refresh_verb_parses() {\n 12931│        assert!(matches!(\n*12932│            parse(&[\"spt\", \"daemon\", \"refresh\"]).unwrap().cmd,\n 12933│            Some(Cmd::Daemon {\n*12934│                action: Some(DaemonCmd::Refresh)\n 12935│            })\n 12936│        ));\n 12937│    }\n\n### tests/\n#### daemon_refresh_e2e.rs\n  *1│//! THE-FORKENING W4 — `spt daemon refresh` on a LIVE daemon with a hosted\n  *2│//! endpoint (REQ-DAEMON-REFRESH): the brain generation cycles (new brain pid,\n   3│//! generation bumped) with NO binary swap, while the broker process, the hosted\n   4│//! harness session, and the endpoint's ONLINE perch all survive untouched — the\n   5│//! apply_staged brain-cycle path minus the swap, driven through the REAL CLI\n    │...\n  13│//\n *14│// [int->REQ-DAEMON-REFRESH]\n  15│\n  16│use std::path::{Path, PathBuf};\n  17│use std::process::{Child, Command, Output, Stdio};\n    │...\n  75│#[test]\n *76│fn daemon_refresh_cycles_brain_while_hosted_endpoint_survives() {\n  77│    let now_ms = || {\n  78│        std::time::SystemTime::now()\n  79│            .duration_since(std::time::UNIX_EPOCH)\n    │...\n 182│\n*183│    // ── (5) THE VERB: `spt daemon refresh` — the production CLI path. ──\n 184│    let refresh = {\n 185│        let mut cmd = Command::new(&spt_bin);\n 186│        cmd.no_window()\n*187│            .args([\"daemon\", \"refresh\"])\n 188│            .env(\"SPT_HOME\", home.path());\n 189│        output_bounded(cmd, Duration::from_secs(30))\n 190│    };\n    │...\n 216│    eprintln!(\n*217│        \"=== W4 daemon refresh: run_ok={} online={online} harness_pid={harness_pid:?} \\\n 218│         refresh_ok={} brain_before={brain_pid_before}/g{gen_before} cycled={cycled:?} \\\n 219│         broker_alive={broker_still_running} harness_alive_after={harness_alive_after} \\\n 220│         still_online={still_online} ===\\n--- refresh stdout ---\\n{refresh_stdout}\\n\\\n    │...\n 260│        refresh.status.success(),\n*261│        \"REQ-DAEMON-REFRESH: `spt daemon refresh` must exit 0 against a live daemon \\\n 262│         (stdout: {refresh_stdout}; stderr: {refresh_stderr}).\\n\\\n 263│         === brain stderr ===\\n{brain_stderr}\"\n 264│    );\n    │...\n 266│        panic!(\n*267│            \"REQ-DAEMON-REFRESH: the brain must CYCLE (a new pid must stamp brain.ready) \\\n 268│             after refresh — pid stayed {brain_pid_before}.\\n=== brain stderr ===\\n{brain_stderr}\"\n 269│        )\n 270│    });\n    │...\n 276│        gen_after > gen_before,\n*277│        \"REQ-DAEMON-REFRESH: the brain GENERATION must advance across a refresh \\\n 278│         (before g{gen_before}, after g{gen_after}) — the supervisor cycle, not a re-stamp.\\n\\\n 279│         === brain stderr ===\\n{brain_stderr}\"\n 280│    );\n    │...\n 282│        broker_still_running,\n*283│        \"REQ-DAEMON-REFRESH: the broker process must survive the refresh untouched \\\n 284│         (pid {broker_pid}).\\n=== brain stderr ===\\n{brain_stderr}\"\n 285│    );\n 286│    assert!(\n 287│        harness_alive_after,\n*288│        \"REQ-DAEMON-REFRESH: the hosted harness session must STILL be ALIVE across the \\\n 289│         refresh — only the brain cycles, never the hosted PTYs. harness_pid={harness_pid:?}.\\n\\\n 290│         === brain stderr ===\\n{brain_stderr}\"\n 291│    );\n    │...\n 293│        still_online,\n*294│        \"REQ-DAEMON-REFRESH: the endpoint perch must remain ONLINE across the refresh \\\n 295│         (the session was never torn down).\\n=== brain stderr ===\\n{brain_stderr}\"\n 296│    );\n 297│}\n#### resume_no_control_steal_e2e.rs\n  12│//! retake. ROOT (SME docs/UPDATE-WEDGE-2-SME-todlando.md): `run_brain` calls\n *13│//! `resume_sessions()` at boot, which re-attached EVERY session `KIND_SESSIONS` returns as\n  14│//! `subscribe_with(AttachIntent::Control, by:None)`. The docstring's \"a None identity never\n  15│//! displaces (falls back to viewer)\" is a MYTH for FREE / SAME-LOCAL-IDENTITY slots:\n  16│//! `resolve_subscribe` (broker.rs) stall-evicts first, then `become_controller` when the\n    │...\n  41│//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` no longer\n *42│//! calls `resume_sessions` at all — it uses the cursor-only `resume_session_cursors` (NO\n  43│//! subscribe), which supersedes the Viewer re-attach as the no-steal cure (no attach ⇒ no\n  44│//! steal, a fortiori) AND fixes the SharedSend self-deadlock. This rig's no-steal\n  45│//! discriminator (the operator's `A_i` controllers keep ticking across the swap) therefore\n    │...\n  57│//!   `A_i` counts `KIND_OUTPUT` frames from a child that ticks forever (~150ms).\n *58│//! - The real `spt daemon brain` candidate boots and `resume_sessions()` re-attaches all N.\n  59│//! - **RED (Control re-attach):** `resolve_subscribe` `same_identity(None==None)` →\n  60│//!   `become_controller` DROPS `A_i`'s sink → `A_i` is displaced → its tick counter FREEZES.\n  61│//! - **GREEN (Viewer re-attach):** the brain joins as a viewer → `A_i` stays controller →\n    │...\n 353│    // ── Establish N broker PTY sessions BEFORE the brain boots, so the candidate's\n*354│    //    boot-once resume_sessions() COVERS them (the field condition: live PTYs present at\n 355│    //    the respawn). Each conn A_i is the session's local (by:None) controller. ──\n 356│    let mut sessions: Vec<(u64, Arc<AtomicU64>)> = Vec::with_capacity(N_SESSIONS);\n 357│    for i in 0..N_SESSIONS {\n#### trial_drain_drive_e2e.rs\n 28│//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` now calls the\n*29│//! cursor-only `resume_session_cursors()` (seed cursors, NO subscribe/attach), not\n*30│//! `resume_sessions()`. So resume never attaches to (or displaces) any controller — the\n 31│//! \"resume paradox\" below is now moot (a wedge present at resume-time is untouched), which\n 32│//! only makes this rig MORE faithful; it still establishes the wedge after boot for\n*33│//! continuity. The historical `resume_sessions()` framing is retained below for context.\n 34│//!\n*35│//! `run_brain` (historically) called `resume_sessions()` ONCE at boot, which re-Control-attaches\n 36│//! (by:None) EVERY session `KIND_SESSIONS` returns — so a wedge present at resume-time\n 37│//! would be DISPLACED (cleared) before the trial ever gates, and the rig could never\n 38│//! bite. In the field `resume_sessions` returned EMPTY (the supervised daemon brain\n\n## spt-daemon/\n\n### src/\n#### applyhost.rs\n  28│//!    half-swapped seat.\n *29│//! 4. **Trigger a brain-PROCESS restart** ([`Brain::request_brain_restart`],\n  30│//!    ADR-0018 D3-3): the broker signals its supervisor to cycle the brain\n  31│//!    child onto the just-swapped binary with `start-reason=update`. No hosted\n  32│//!    endpoint terminates or suspends (REQ-UPD-3, absolute for this class — the\n    │...\n 338│    // promotion + auto-rollback on a brain that fails to come up is D6.\n*339│    match brain.request_brain_restart() {\n 340│        Ok(_honored) => Ok(ApplyStagedOutcome::AppliedPending {\n 341│            version,\n 342│            product_version,\n    │...\n 352│/// Cycle the brain of a RUNNING daemon with NO binary swap and NO staging\n*353│/// preconditions (REQ-DAEMON-REFRESH, `spt daemon refresh`): connect to the\n 354│/// broker and raise the same supervisor brain-restart signal the post-swap\n*355│/// apply rides ([`Brain::request_brain_restart`], ADR-0018 D3-3) — brain stop →\n 356│/// respawn → readiness trial → promote, broker + every hosted PTY untouched by\n 357│/// construction. The recovery verb for wedged brain-held state that previously\n 358│/// needed a full daemon bounce. Nothing is verified, staged, or renamed: the\n    │...\n 362│/// (accepted, not acted on).\n*363│// [impl->REQ-DAEMON-REFRESH]\n 364│pub fn refresh_brain(broker_name: &str) -> Result<bool, String> {\n 365│    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n 366│    brain\n*367│        .request_brain_restart()\n 368│        .map_err(|e| format!(\"brain-restart trigger: {e}\"))\n 369│}\n 370│\n    │...\n 792│    // brain-restart verb (D3-3): a broker WITH a supervisor signal wired raises\n*793│    // it on `KIND_BRAIN_RESTART` and acks `honored: true`; a bare broker (no\n 794│    // supervisor — the test/no-daemon shape) accepts the verb but acks\n 795│    // `honored: false`. This is the broker half of the apply trigger that\n 796│    // replaces the in-process handoff.\n 797│    #[test]\n*798│    fn brain_restart_verb_raises_the_supervisor_signal() {\n 799│        let name = unique_name();\n 800│        let broker = served_broker(&name);\n 801│\n    │...\n 804│        assert!(\n*805│            !b.request_brain_restart().expect(\"verb round-trips\"),\n 806│            \"a broker with no supervisor wired must ack honored=false\"\n 807│        );\n 808│\n    │...\n 810│        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n*811│        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n 812│        let mut b2 = cold_connect_retry(&name);\n 813│        assert!(\n*814│            b2.request_brain_restart().expect(\"verb round-trips\"),\n 815│            \"a broker with a supervisor wired must ack honored=true\"\n 816│        );\n 817│        assert!(\n    │...\n 822│\n*823│    // [unit->REQ-DAEMON-REFRESH] `refresh_brain` routes the SAME supervisor\n 824│    // brain-cycle signal as the post-swap apply — against a bare broker with NO\n 825│    // staged release, NO release cache, NO binary swap. The verb has no staging\n 826│    // preconditions: the signal raises and `honored` reports the supervisor\n    │...\n 841│        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n*842│        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n 843│        assert!(\n 844│            refresh_brain(&name).expect(\"refresh round-trips\"),\n 845│            \"a wired supervisor must report honored=true\"\n#### brain.rs\n   45│    ViewerEvictedEvent,\n  *46│    KIND_ADAPTER_APPLY, KIND_APPLIED, KIND_BRAIN_RESTART, KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_VIEWER_EVICTED,\n  *47│    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,\n   48│    KIND_NET_DIALED, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT, KIND_NET_DIAL_SUBMITTED,\n   49│    KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE,\n   50│    KIND_NET_SENT,\n     │...\n 1038│    /// session — brainproc.rs:184 — and nothing on its side consumes the output, §3\n*1039│    /// guard), so it MUST call [`Self::resume_session_cursors`] (cursor-only, NO\n 1040│    /// subscribe) instead. This subscribing method stays for a DRAINING caller (the\n 1041│    /// resume int tests today; the forward live-agent-adapter / Option-B drained carrier).\n 1042│    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n     │...\n 1045│    // [doc->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n*1046│    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {\n 1047│        let reply = self.sessions()?;\n 1048│        let mut resumed = Vec::with_capacity(reply.sessions.len());\n 1049│        for info in reply.sessions {\n     │...\n 1076│    // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n*1077│    pub fn resume_session_cursors(&mut self) -> io::Result<Vec<u64>> {\n 1078│        let reply = self.sessions()?;\n 1079│        let mut resumed = Vec::with_capacity(reply.sessions.len());\n 1080│        for info in reply.sessions {\n     │...\n 1458│    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*1459│    pub fn request_brain_restart(&mut self) -> io::Result<bool> {\n*1460│        self.send(KIND_BRAIN_RESTART, serde_json::Value::Null)?;\n 1461│        loop {\n 1462│            match self.read_event()? {\n*1463│                BrokerEvent::Other(env) if env.kind == KIND_BRAIN_RESTARTED => {\n 1464│                    let reply: BrainRestarted = serde_json::from_value(env.payload)\n 1465│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 1466│                    return Ok(reply.honored);\n#### brainproc.rs\n  106│/// The broker→supervisor **planned-restart signal** (ADR-0018 D3-3, Q3). The\n *107│/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n  108│/// its next wait tick, kills the current brain child, and respawns it with\n  109│/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n  110│/// between the broker serve path and the supervisor — both run in the broker\n     │...\n  196│    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n *197│    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n  198│    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n  199│    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n  200│    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n  201│    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n *202│    match brain.resume_session_cursors() {\n  203│        Ok(ids) if !ids.is_empty() => {\n  204│            eprintln!(\n  205│                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n     │...\n  920│            backoff = base;\n *921│            eprintln!(\"BRAIN_RESTART: planned update respawn (generation {generation})\");\n  922│            continue;\n  923│        }\n  924│        reason = StartReason::Crash;\n  925│        backoff = next_backoff(backoff, started.elapsed(), base);\n *926│        eprintln!(\"BRAIN_RESTART: supervised respawn in {}s\", backoff.as_secs());\n  927│        sleep_backoff(backoff, stop);\n  928│    }\n  929│}\n     │...\n 1092│/// Returns the shared [`BrainRestart`] signal so the caller can hand a clone to\n*1093│/// the broker, wiring the `KIND_BRAIN_RESTART` verb to this supervisor (D3-3).\n 1094│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 1095│pub fn spawn_brain_supervisor(\n 1096│    stop: Arc<AtomicBool>,\n     │...\n 1205│    /// The planned-restart path (D3-3, Q3): a `BrainRestart::request` (the\n*1206│    /// broker's `KIND_BRAIN_RESTART` handler) makes the supervisor cycle the\n 1207│    /// brain and stamp the next spawn `Update` — distinguishing an intentional\n 1208│    /// update cycle from a crash (V2, broker-authoritative). A long-lived child\n 1209│    /// is used so the restart, not an exit, is what ends the cycle.\n#### broker.rs\n   59│    NetStreamsReply, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n  *60│    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n   61│    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n   62│    KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,\n   63│    KIND_NET_SENT,\n     │...\n 2541│    /// The broker→supervisor planned-restart signal (ADR-0018 D3-3): a\n*2542│    /// `KIND_BRAIN_RESTART` handler raises it so the brain supervisor cycles the\n 2543│    /// child onto a freshly-swapped binary. Empty on a bare broker (a test broker\n 2544│    /// with no supervisor) — the verb is then accepted but unhonored. Set once by\n 2545│    /// `Daemon::run` after it spawns the supervisor.\n*2546│    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,\n 2547│    /// Held meet results for the two-phase join (REQ-JOIN-TWO-PHASE): a\n 2548│    /// `session_id → (subnet, real address, 5-min deadline)` between\n 2549│    /// [`KIND_PAIR_MEET`] and [`KIND_PAIR_CODE_SUBMIT`]. The code phase dials the\n     │...\n 2696│            net: net_cell,\n*2697│            brain_restart: std::sync::OnceLock::new(),\n 2698│            pair_holds: Mutex::new(HashMap::new()),\n 2699│            wake_inflight: Mutex::new(HashSet::new()),\n 2700│            conn_handlers: AtomicUsize::new(0),\n     │...\n 2704│    /// Wire the brain supervisor's planned-restart signal (ADR-0018 D3-3) so the\n*2705│    /// `KIND_BRAIN_RESTART` verb can drive a real brain-process restart. Called\n 2706│    /// once by `Daemon::run` after spawning the supervisor; idempotent by\n 2707│    /// `OnceLock` (the first wins). A broker without it accepts the verb but\n 2708│    /// reports `honored: false` (no supervisor to respawn the child).\n 2709│    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*2710│    pub fn set_brain_restart(&self, signal: Arc<crate::brainproc::BrainRestart>) -> bool {\n*2711│        self.brain_restart.set(signal).is_ok()\n 2712│    }\n 2713│\n 2714│    /// Attach a network host to a broker that was bound net-less (REQ-DAEMON-9):\n     │...\n 3190│                }\n*3191│                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n 3192│                // An unknown kind is tolerated (forward-compat): report, don't die.\n 3193│                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n 3194│            }\n     │...\n 3884│    // [impl->REQ-UPD-3]\n*3885│    fn dispatch_brain_restart(&self, send: &SharedSend) {\n*3886│        let honored = match self.brain_restart.get() {\n 3887│            Some(signal) => {\n 3888│                signal.request();\n 3889│                true\n     │...\n 3893│        let frame = Envelope::new(\n*3894│            KIND_BRAIN_RESTARTED,\n 3895│            serde_json::to_value(BrainRestarted { honored })\n 3896│                .expect(\"BrainRestarted serializes\"),\n 3897│        );\n#### daemon.rs\n 187│        // planned-restart signal can be wired into it below (ADR-0018 D3-3): the\n*188│        // `KIND_BRAIN_RESTART` verb the broker serves must reach the supervisor.\n 189│        let broker_handle: Option<Arc<Broker>> =\n 190│            match Broker::bind_in_with_net(&broker_socket_name(), default_journal_path(), net) {\n 191│                Ok(broker) => {\n    │...\n 315│        // makes an update a *planned* restart on this same path: the supervisor's\n*316│        // `BrainRestart` signal is wired into the broker so the `KIND_BRAIN_RESTART`\n 317│        // verb (sent by `spt update apply` after the binary swap) cycles the brain\n 318│        // onto the new binary with `start-reason=update`. The stop flag is held\n 319│        // for symmetry, never raised (the daemon runs until killed). The brain\n    │...\n 343│            if let Some(broker) = &broker_handle {\n*344│                broker.set_brain_restart(restart);\n 345│            }\n 346│        }\n 347│\n#### lib.rs\n 180│};\n*181│pub use applyhost::{apply_staged, apply_staged_daemonless, refresh_brain, ApplyStagedOutcome};\n 182│pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};\n 183│pub use brain::{Brain, BrainState, BrokerEvent};\n 184│pub use broker::Broker;\n#### msg.rs\n 231│/// which the caller surfaces — it never silently no-ops a swapped binary.\n*232│pub const KIND_BRAIN_RESTART: &str = \"brain-restart\";\n*233│/// Broker→client ack for [`KIND_BRAIN_RESTART`]: the restart was *accepted*\n 234│/// (the supervisor was signalled). Readiness/rollback gating is D6 — this ack is\n 235│/// acceptance, not completion. `honored` is false when the broker has no\n 236│/// supervisor wired (e.g. a bare test broker), so the caller knows the trigger\n 237│/// reached a broker that cannot itself respawn.\n*238│pub const KIND_BRAIN_RESTARTED: &str = \"brain-restarted\";\n 239│\n 240│/// `spawn` payload — the program + args + initial geometry. The broker spawns\n 241│/// it under a PTY (no shell interpretation; argv is passed through).\n    │...\n 871│\n*872│/// `brain-restarted` payload — the broker's ack for [`KIND_BRAIN_RESTART`]\n 873│/// (ADR-0018 D3-3). `honored` is true when a brain supervisor was signalled to\n 874│/// cycle the child; false when the broker has none wired (a bare test broker),\n 875│/// so the caller can tell a real planned restart from a mere accept.\n#### seedmap.rs\n 37│\n*38│/// Client→daemon: store (or refresh) a seed in the in-memory map.\n 39│pub const KIND_SEED_PUT: &str = \"seed_put\";\n 40│/// Client→daemon: consume the seed for a `parent_pid` (take-once).\n 41│pub const KIND_SEED_TAKE: &str = \"seed_take\";\n\n### tests/\n#### attach.rs\n  778│#[test]\n *779│fn attach_survives_target_brain_restart_exactly_once() {\n  780│    init_home();\n  781│    let dir = tempfile::tempdir().expect(\"tempdir\");\n  782│    let (name_a, name_b) = (unique_name(), unique_name());\n     │...\n 1375│// keystone (`controller_writer_reorder_*`) and the restart carrier\n*1376│// (`attach_survives_target_brain_restart_exactly_once`); HERE we lock the\n 1377│// load-bearing post-conditions of `handoff` itself, deterministically.\n 1378│#[test]\n 1379│fn handoff_seeds_resume_cursor_and_resubscribes_for_resume() {\n#### brain_resume_conn_deadlock.rs\n 133│/// How the probe brain resumes before running the heartbeat: the SHIPPING daemon\n*134│/// path — `CursorOnly` → `resume_session_cursors`, no subscribe. (The hazard path,\n 135│/// `resume_sessions`, is exercised directly by the deterministic zero-subscriber guard\n 136│/// `daemon_resume_leaves_zero_brain_subscribers` below, not through this probe.)\n 137│#[derive(Clone, Copy)]\n    │...\n 162│            ResumeMode::CursorOnly => {\n*163│                let _ = brain.resume_session_cursors();\n 164│            }\n 165│        }\n 166│        while !stop.load(Ordering::Relaxed) {\n    │...\n 216│/// STEADY-STATE severity — the SHIPPING path: the daemon brain resumes CURSOR-ONLY\n*217│/// (`resume_session_cursors`, no subscribe) while N sessions actively stream, so its\n 218│/// heartbeat stays live. NO dead peer anywhere. RED-first: revert run_brain to the\n 219│/// subscribing `resume_sessions` (or see the regression-guard test below) and this\n 220│/// starves to a standstill.\n    │...\n 265│/// Two settled arms, no timing window:\n*266│///   POSITIVE — the shipping daemon path (`resume_session_cursors`) must leave EVERY\n 267│///   hosted session with ZERO viewers. A resume that instead subscribed the undrained\n 268│///   heartbeat conn re-opens the round-3 SharedSend self-deadlock class; post-r4 it\n 269│///   would instead get that heartbeat conn poisoned/retired by the write watchdog —\n    │...\n 315│    let mut brain = Brain::cold_start(&name, 0).expect(\"cold_start probe brain\");\n*316│    brain.resume_session_cursors().expect(\"cursor-only resume\");\n 317│    for &sid in &sids {\n 318│        assert_eq!(\n 319│            broker.test_session_viewer_count(sid),\n    │...\n 333│    let mut hazard_brain = Brain::cold_start(&name, 0).expect(\"cold_start hazard brain\");\n*334│    hazard_brain.resume_sessions().expect(\"hazard subscribe resume\");\n 335│    // Deterministic barrier: subscribe_with is fire-and-forget and resume_sessions'\n 336│    // internal round-trip precedes the subscribes, so flush with ONE more same-conn\n 337│    // sessions() round-trip. Per-conn in-order dispatch means all 3 KIND_SUBSCRIBE\n#### daemon_e2e.rs\n 129│#[test]\n*130│fn daemon_hosts_lifecycle_and_survives_brain_restart() {\n 131│    // Isolate all on-disk perch state (the Self perch, the nested Psyche perch,\n 132│    // the commune drops) to a temp home.\n 133│    let home = tempfile::tempdir().expect(\"tempdir\");\n#### handoff.rs\n 78│#[test]\n*79│fn brain_restart_survives_gaplessly_with_fresh_gen_start() {\n 80│    const RESTARTS: u64 = 12; // >= 10 (B2 acceptance)\n 81│    let name = unique_name();\n 82│    let broker = Broker::bind(&name).expect(\"bind broker\");\n#### netbroker.rs\n 154│#[test]\n*155│fn replayed_dial_op_is_deduped_across_brain_restart() {\n 156│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 157│    let (name_a, name_b) = (unique_name(), unique_name());\n 158│    let broker_a = net_broker(&name_a, Identity::generate(), &dir.path().join(\"a\"));\n#### netstream.rs\n 144│#[test]\n*145│fn receiver_brain_restart_is_gapless_and_exactly_once() {\n 146│    const TOTAL: u64 = 20;\n 147│    const KILL_AT: u64 = 5; // receiver dies after reading this many chunks\n 148│\n    │...\n 220│#[test]\n*221│fn sender_brain_restart_redrive_is_exactly_once() {\n 222│    const TOTAL: u64 = 8;\n 223│    const CRASH_AFTER: u64 = 3; // acked sends before the crash\n 224│\n#### presence.rs\n 82│#[test]\n*83│fn presence_survives_brain_restart_with_cursor_resume() {\n 84│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 85│    let name_b = unique_name();\n 86│    let _broker_b = net_broker(&name_b, dir.path());\n#### resume.rs\n 188│    let mut r = connect_retry(&name);\n*189│    let mut resumed = r.resume_sessions().expect(\"resume all hosted sessions\");\n 190│    resumed.sort_unstable();\n 191│    let mut want = vec![sid_a, sid_b];\n 192│    want.sort_unstable();\n    │...\n 259│    let resumed = r\n*260│        .resume_sessions()\n 261│        .expect(\"resume succeeds despite eviction\");\n 262│    assert_eq!(resumed, vec![sid], \"the session re-attached\");\n 263│\n    │...\n 329│    let mut r = connect_retry(&name);\n*330│    let resumed = r.resume_sessions().expect(\"resume\");\n 331│    assert_eq!(resumed, vec![seed_sid], \"resumed the seed session\");\n 332│\n 333│    // First fresh spawn: delivers under both old and new code (it is the one"},"isError":false,"timestamp":1784195824177}}
{"type":"message","id":"312b743c","parentId":"aa2921f9","timestamp":"2026-07-16T09:57:13.908Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning extended daemon refresh tests**"},{"type":"toolCall","id":"call_BFc23r7TOyMAelNgelV9Qp7D|fc_008390f508d1d09b016a58aaf9cb58819aa7e8eb88dc1701be","name":"read","arguments":{"i":"Reading wedge code diagnosis","path":"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","selector":"1-260"}},{"type":"toolCall","id":"call_rVavpqB1FqxWCJZhAXQly51Y|fc_008390f508d1d09b016a58aaf9cb78819a8e1888ecc4e9f7c7","name":"read","arguments":{"i":"Reading wedge rig verdict","path":"docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md","selector":"1-260"}},{"type":"toolCall","id":"call_rBdt3GQKTh3bYgo4VashfB9E|fc_008390f508d1d09b016a58aaf9cb88819a836d30d248a658a9","name":"read","arguments":{"i":"Reading refresh E2E coverage","path":"crates/spt/tests/daemon_refresh_e2e.rs","selector":"1-305"}},{"type":"toolCall","id":"call_vao3ExBNo7JiwTDhf4qgebRQ|fc_008390f508d1d09b016a58aaf9cb90819a97cd3a9a4e5b82c2","name":"read","arguments":{"i":"Reading zero subscriber guard","path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","selector":"260-355"}},{"type":"toolCall","id":"call_lBfBBIzqkNmxwv1IvF4id131|fc_008390f508d1d09b016a58aaf9cb98819ab7c335d5ed1f5d18","name":"read","arguments":{"i":"Reading blackhole lifecycle assertions","path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","selector":"1-80,240-470"}},{"type":"toolCall","id":"call_VWCHWIdpi8IWddgi5sN5zNia|fc_008390f508d1d09b016a58aaf9cb9c819a831500a6be0a92a6","name":"read","arguments":{"i":"Reading controller decouple assertions","path":"crates/spt-daemon/tests/brain_decouple.rs","selector":"1-50,215-375,470-650"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":23567,"output":356,"cacheRead":195072,"cacheWrite":0,"totalTokens":218995,"reasoningTokens":85,"cost":{"input":0.11783500000000001,"output":0.01068,"cacheRead":0.097536,"cacheWrite":0,"total":0.226051}},"stopReason":"toolUse","timestamp":1784195824223,"responseId":"resp_008390f508d1d09b016a58aaf086c8819a95cece0f1a13cd01","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_008390f508d1d09b016a58aaf23f90819a995a9858d9e55adc","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKr5TM-mRdxQNKBDzzJoTMZNNd3X_ZeixExj6RRlARbEnu9awB2NhV10baNB-G_LVNVkGFLRG7JWs6Z1ow-8zsKttw8m-c_0CV2bx1-xksA1l30ZIMS2NaqRkDulmvv-BWmwzUDsdaX6eCGLuOv1bZUuNsnWoC591plQEYM2lA5EoSuElbSGXBR2N8vBRz8DvhH_HQr6xsXXj4SySx9_u61n89xxqOk7QKbdVY1ffsoLpZecw2YIY3ySkzu6vzvJpHTvrd_CEXC_fv-nJoDXj81H5AUE1CCUSG-8WZfjuY0EZJnkRG_iGRlhsnOIowvetSwxt9doEm_uBM1W1cLeIbk-7dBC6NkQVra2f9ZnuLCet2CdrPPEW4TOv44-QKqaXXMwBmUPuPNB-B6k-KySYrNBpBoom6Daw46DPHBCyKnH-YCAB_HLckAQa2pZAhRCp41YjxtCNIkRy1jWJZc2YY5hVfrPAe3nn2h68W4B58MTbT95BA92bFJtlL6APjlqYkuJ2PAO4ubW_SLOAs4xPvVjJKJI43blywiWIOK3_2o6_45TwUCL-DkBnhLdR8BPeUQBwkSLmlCkmpO58wRvW8kEySxpny0xpyO6GuXS_L18dYSzE3EvXNzOzgtQLYe2TvPehQezxeXewhVpHhZ8G_izOSNor1IygSq1aVMY_g6uvGturTTV2qr0CypyYkCu5Gv466Wc9a1Il1_aAeZoPSsRWFQFs0LWMe1eCj5bKxyO1GUb21WVqZB9mIhMimI0n6RC30Gp5RraNWS8cCZtd5YR_BtNhJ5NIcVFPN-Utxp22YQ8ZpHzmXh-w1h-gVS_qcZ-XjSDhnJ6aP-pHQob0ueN6uQS8viUTlaDlAm3i2bFx7GUsF5Vl7IIoclNx2igpRTzREUZBKSUU5VolZ9UYFY-El9gUqJk-jp1Iym9witb9HAWymCCNOubmxuY4fvmazeZVc1pIKu8z28P79a0fZom_BEkXGSx7RhkvI-W7w7s2On5RS1-R_aKx5emp-KjLRgHs5LNvM7UDH27OwyN4Sx402tIOr_Lai41754sgQm74WQ3JFpwU38KePFlwmIf8bs7Dve8k6Yj_HvvYQf7JKB-I6CREYonHcAF8YfPKzI8mVaYeDKMm9JOaHqEFoNaQHkWzuaIfEDx_W4ofMhzliDdxZ-iiOb-vJH_q5qJUvpUVKfmBKaCP9KUr0FWBPtjt-RDeHQ8-RKu3d9O1mHSeUvH8JqUnRAb91M3t5SXoYN0DpQMoOfjiPwFR6bkAhcTPgx76Q6ly4r_WAqIh_R8ZObMbCwytsPR0ERgHb4FboI0U1q2cEmjr1Ldo8SQufjPRjlreZ1tBmeOmcX0-nfzTkFBzyLuTOMcHvWnHaBE65vsDmAETXZX4QQM4OxzNGe0cgy8Li1YMlHYrzQ9d2FPnbhVzwxNC1YBCMh6gFM8kLSBfrb4hmCCRkdzFvsUt2xO8R2LLes8RfyMD5CSGA4w6Wj5OA==","summary":[{"type":"summary_text","text":"**Planning extended daemon refresh tests**"}]},{"id":"fc_008390f508d1d09b016a58aaf9cb58819aa7e8eb88dc1701be","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading wedge code diagnosis\",\"path\":\"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md\",\"selector\":\"1-260\"}","call_id":"call_BFc23r7TOyMAelNgelV9Qp7D","name":"read"},{"id":"fc_008390f508d1d09b016a58aaf9cb78819a8e1888ecc4e9f7c7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading wedge rig verdict\",\"path\":\"docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md\",\"selector\":\"1-260\"}","call_id":"call_rVavpqB1FqxWCJZhAXQly51Y","name":"read"},{"id":"fc_008390f508d1d09b016a58aaf9cb88819a836d30d248a658a9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading refresh E2E coverage\",\"path\":\"crates/spt/tests/daemon_refresh_e2e.rs\",\"selector\":\"1-305\"}","call_id":"call_rBdt3GQKTh3bYgo4VashfB9E","name":"read"},{"id":"fc_008390f508d1d09b016a58aaf9cb90819a97cd3a9a4e5b82c2","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading zero subscriber guard\",\"path\":\"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs\",\"selector\":\"260-355\"}","call_id":"call_vao3ExBNo7JiwTDhf4qgebRQ","name":"read"},{"id":"fc_008390f508d1d09b016a58aaf9cb98819ab7c335d5ed1f5d18","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading blackhole lifecycle assertions\",\"path\":\"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs\",\"selector\":\"1-80,240-470\"}","call_id":"call_lBfBBIzqkNmxwv1IvF4id131","name":"read"},{"id":"fc_008390f508d1d09b016a58aaf9cb9c819a831500a6be0a92a6","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading controller decouple assertions\",\"path\":\"crates/spt-daemon/tests/brain_decouple.rs\",\"selector\":\"1-50,215-375,470-650\"}","call_id":"call_VWCHWIdpi8IWddgi5sN5zNia","name":"read"}]},"duration":9666.727099999785,"ttft":2001.9972000000998,"contextSnapshot":{"promptTokens":218639,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BFc23r7TOyMAelNgelV9Qp7D|fc_008390f508d1d09b016a58aaf9cb58819aa7e8eb88dc1701be","toolName":"read","startedAt":"2026-07-16T09:57:13.908Z","args":{"path":"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md"},"intent":"Reading wedge code diagnosis"},"id":"0a71fcb5","parentId":"312b743c","timestamp":"2026-07-16T09:57:13.908Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_rVavpqB1FqxWCJZhAXQly51Y|fc_008390f508d1d09b016a58aaf9cb78819a8e1888ecc4e9f7c7","toolName":"read","startedAt":"2026-07-16T09:57:13.908Z","args":{"path":"docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md"},"intent":"Reading wedge rig verdict"},"id":"786743e4","parentId":"0a71fcb5","timestamp":"2026-07-16T09:57:13.908Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_rBdt3GQKTh3bYgo4VashfB9E|fc_008390f508d1d09b016a58aaf9cb88819a836d30d248a658a9","toolName":"read","startedAt":"2026-07-16T09:57:13.908Z","args":{"path":"crates/spt/tests/daemon_refresh_e2e.rs"},"intent":"Reading refresh E2E coverage"},"id":"57150bfa","parentId":"786743e4","timestamp":"2026-07-16T09:57:13.908Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_vao3ExBNo7JiwTDhf4qgebRQ|fc_008390f508d1d09b016a58aaf9cb90819a97cd3a9a4e5b82c2","toolName":"read","startedAt":"2026-07-16T09:57:13.908Z","args":{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs"},"intent":"Reading zero subscriber guard"},"id":"ab484329","parentId":"57150bfa","timestamp":"2026-07-16T09:57:13.908Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_lBfBBIzqkNmxwv1IvF4id131|fc_008390f508d1d09b016a58aaf9cb98819ab7c335d5ed1f5d18","toolName":"read","startedAt":"2026-07-16T09:57:13.908Z","args":{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs"},"intent":"Reading blackhole lifecycle assertions"},"id":"e2007c09","parentId":"ab484329","timestamp":"2026-07-16T09:57:13.908Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_VWCHWIdpi8IWddgi5sN5zNia|fc_008390f508d1d09b016a58aaf9cb9c819a831500a6be0a92a6","toolName":"read","startedAt":"2026-07-16T09:57:13.908Z","args":{"path":"crates/spt-daemon/tests/brain_decouple.rs"},"intent":"Reading controller decouple assertions"},"id":"57c3c1bf","parentId":"e2007c09","timestamp":"2026-07-16T09:57:13.908Z"}
{"type":"message","id":"6e8de6d4","parentId":"57c3c1bf","timestamp":"2026-07-16T09:57:13.922Z","message":{"role":"toolResult","toolCallId":"call_lBfBBIzqkNmxwv1IvF4id131|fc_008390f508d1d09b016a58aaf9cb98819ab7c335d5ed1f5d18","toolName":"read","content":[{"type":"text","text":"//! MSG-IDENTITY W6 / F-039 leg (e) — the DETERMINISTIC BLACK-HOLED-CONTROLLER\n//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker\n//! hosting REAL PTY children over the real local IPC.\n//!\n//! ## What this is\n//!\n//! This is hertz's RCA fix-shape item 5 made executable (`.claude/\n//! hertz-pty-freeze-rca.md` §Proposed fix shape, item 5): the standing\n//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the\n//! EXACT Windows named-pipe black-hole (a controller that stops draining while a\n//! finite burst floods its socket, so the broker's writer parks inside\n//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` — no supported\n//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/\n//! src/conn.rs` bounded/cancelable/poison-on-failure writes, landed fc048bb),\n//! asserts hertz's FIVE invariants for ONE incident:\n//!\n//!   1. UNRELATED SESSIONS CONTINUE — a second, independent session's viewer keeps\n//!      receiving output throughout the black-hole incident (the fan-out never\n//!      blocks the whole broker on the one wedged conn);\n//!   2. THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE BOUND — a write\n//!      on the retired old-client connection FAILS, and the conn's `handle_conn`\n//!      thread finishes within the write bound of logical release (the conn was\n//!      physically retired, not merely logically stall-evicted);\n//!   3. ITS WRITER EXITS — `broker.conn_handler_count()` falls to the\n//!      unrelated-viewer floor (<= 1; the persistent invariant-1 viewer holds one\n//!      handler) WITHOUT the black-holed client ever dropping its end (the parked\n//!      writer was aborted + the reader EOF-cleaned, so the handler thread ran to\n//!      completion), and the viewer flows output AFTER the sample latches — proving\n//!      the survivor is the viewer's handler, not a leaked one;\n//!   4. A FRESH VIEWER CAN ATTACH — a brand-new Viewer subscribe on the retired\n//!      session COMPLETES and RECEIVES replayed output from the frozen cursor\n//!      (the fan-out accepts + serves a new subscriber after retirement);\n//!   5. NO LOCK OR TASK REMAINS OWNED BY THE RETIRED CONNECTION — asserted via\n//!      PROXIES (this invariant has no direct seam): after retirement the broker\n//!      still answers a control-plane `KIND_SESSIONS` request PROMPTLY (no lock\n//!      is held by the retired writer), `conn_handler_count()` returns to the\n//!      unrelated-viewer floor without the client dropping its end (no task\n//!      lingers), AND both the\n//!      unrelated session's viewer and the fresh viewer flow output (the shared\n//!      fan-out lock is free). The conjunction of these three is the strongest\n//!      deterministic proxy available for \"nothing is still owned.\"\n//!\n//! ## Relationship to `brain_decouple.rs`\n//!\n//! This harness GENERALIZES `brain_decouple.rs`'s r4 gate\n//! (`non_draining_controller_stall_evict_releases_writer_and_connection`, the\n//! Windows-mandatory physical-retirement leg for\n//! REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, which stays the primary\n//! gate for that hazard). It reuses the same SEED/trigger/burst choreography,\n//! the hand-back-the-unread-Stream pattern, the `conn_handler_count` and\n//! `stall_evict_count` seams, the old-client-write-fails probe, and the\n//! teardown-before-assert discipline. It ADDS the two pieces the r4 gate does\n//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n//! UNRELATED session whose viewer keeps ticking across the incident, and a fresh\n//! VIEWER attach (not just a controller take) proven to receive output.\n//!\n//! ## Why Windows-mandatory (`#![cfg(windows)]` — the whole file)\n//!\n//! Invariants 2, 3, and the task-ownership proxy of 5 are the PHYSICAL-retirement\n//! asserts, and the deterministic black-hole only reproduces on the Windows\n//! named-pipe path (`interprocess 2.4.2` → `WriteFileEx`+`SleepEx(INFINITE)`,\n//! `set_timeout` → `Unsupported`) — the exact field failure. On Unix the\n//! poison/retire path is a `shutdown(SHUT_RDWR)` abort of a buffer-fill block: a\n//! genuinely DIFFERENT mechanism, not this incident. Faking a Unix companion\n//! would test a different thing, so — mirroring the r4 gate's own\n//! `#[cfg(windows)]` leg — the entire harness is Windows-only. On other targets\n//! this test binary compiles to zero tests (no fakery).\n//!\n//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n//!\n//! Shares ONE process-global `SPT_HOME` + `SPT_BRAIN_WRITE_DEADLINE_MS`, spins a\n//! REAL broker + TWO PTY children. HEAVY class. Run isolated with\n//! `cargo nextest run -p spt-daemon --test conn_blackhole_lifecycle` (CI's path)\n//! or `cargo test --test conn_blackhole_lifecycle -- --test-threads=1`. NEVER a\n//! bare `cargo test --test conn_blackhole_lifecycle` on Windows. Every read in\n//! this test rides a bounded thread+channel or a polled deadline; the test\n//! captures plain values as it goes and only asserts AFTER teardown, so it can\n//! never hang even on broken code.\n#![cfg(windows)]\n\n…\nfn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n…\n    thread::spawn(move || {\n…\n        loop {\n            match read_frame(&mut c) {\n…\n                Err(_) => return,\n            }\n        }\n    });\n    rx.recv_timeout(deadline).ok()\n}\n\n// [int->REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS]\n#[test]\nfn blackholed_controller_lifecycle_five_invariants() {\n    init_home();\n    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n    // instead of 15 s — the structural outcome, fast + deterministic (the knob\n    // grants wall-clock only; it never relaxes an assertion).\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n\n    // ── UNRELATED session U (invariant 1): a second, independent flood child on\n    //    its OWN endpoint. Drop its spawner conn (so U has no controller to wedge\n    //    — the PTY keeps producing headless), and attach a SEPARATE DRAINING\n    //    viewer whose frame count must keep advancing ACROSS the whole incident. ──\n    let (u_spawn, sid_u) = spawn_session(&name, flood_spawn_req(\"blackhole-unrelated-ep\"));\n    drop(u_spawn);\n    let u_name = name.clone();\n    let u_frames = Arc::new(AtomicU64::new(0));\n    let u_frames_w = Arc::clone(&u_frames);\n    thread::spawn(move || {\n        let mut v = connect(&u_name);\n        send(\n            &mut v,\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id: sid_u,\n                from_seq: 0,\n                intent: AttachIntent::Viewer,\n                by: Some(\"unrelated-viewer\".to_string()),\n            })\n            .unwrap(),\n        );\n        while let Ok(f) = read_frame(&mut v) {\n            if f.kind == KIND_OUTPUT {\n                u_frames_w.fetch_add(1, Ordering::Relaxed);\n            }\n        }\n    });\n\n    // ── BLACK-HOLED session B: the real SEED+burst PTY child from conn A, then\n    //    DROP A so the ONLY persistent brain conn is the black-holed controller R. ──\n    let (a, sid_b) = spawn_session(&name, seed_burst_spawn_req(\"blackhole-victim-ep\"));\n    drop(a);\n\n    // ── Remote controller R: subscribes Control from_seq 0, drains until it has\n    //    seen `SEED` (a real controller frame flowed), signals main so main can\n    //    freeze the PRE-burst high-water, then on main's go fires the burst trigger,\n    //    drains a few burst frames (a real seq>0 frame delivered), and STOPS\n    //    reading — handing the Stream back to main UNREAD. Main holds it, never\n    //    draining, so the broker's R-writer parks in `write_frame` exactly as\n    //    against a suspended brain. R's Stream is NEVER dropped until the very end. ──\n    let r_name = name.clone();\n    let (seed_tx, seed_rx) = std::sync::mpsc::channel::<(bool, u64)>();\n    let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();\n    let (hand_tx, hand_rx) = std::sync::mpsc::channel::<(Stream, u64)>();\n    thread::spawn(move || {\n        let mut r = connect(&r_name);\n        send(\n            &mut r,\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id: sid_b,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                by: Some(\"operator-one\".to_string()),\n            })\n            .unwrap(),\n        );\n        loop {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == KIND_SUBSCRIBED => break,\n                Ok(_) => continue,\n                Err(_) => {\n                    let _ = seed_tx.send((false, 0));\n                    return;\n                }\n            }\n        }\n        // Drain live output until the CONCATENATED decoded bytes contain `SEED`\n        // (ConPTY echoes input + emits VT noise, so per-frame matching is\n        // unreliable). Track the max seq seen.\n        let mut acc: Vec<u8> = Vec::new();\n        let mut max_seq: u64 = 0;\n        let mut saw_seed = false;\n        let seed_deadline = Instant::now() + Duration::from_secs(30);\n        while Instant::now() < seed_deadline {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == KIND_OUTPUT => {\n                    let ev: OutputEvent = serde_json::from_value(f.payload).unwrap();\n                    max_seq = max_seq.max(ev.seq);\n                    if let Ok(bytes) = decode_bytes(&ev.data_b64) {\n                        acc.extend_from_slice(&bytes);\n                    }\n                    if acc.windows(4).any(|w| w == b\"SEED\") {\n                        saw_seed = true;\n                        break;\n                    }\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n        let _ = seed_tx.send((saw_seed, max_seq));\n        // Wait for main's go, then fire the burst trigger (`\\r\\n` satisfies the\n        // first Read-Host; ack=false = the rc/operator fire-and-forward path).\n        let _ = go_rx.recv_timeout(Duration::from_secs(15));\n        send(\n            &mut r,\n            KIND_INPUT,\n            serde_json::to_value(InputReq {\n                session_id: sid_b,\n                data_b64: encode_bytes(b\"\\r\\n\"),\n                op_id: None,\n                minter: None,\n                ack: false,\n            })\n            .unwrap(),\n        );\n        // Drain a bounded handful of burst frames so a REAL seq>0 controller frame\n        // is proven delivered, then STOP reading forever (the black-hole). The\n        // remaining ~1300 frames flood the now-unread socket → R-writer wedges.\n        let post_deadline = Instant::now() + Duration::from_secs(5);\n        let mut post = 0u32;\n        while (max_seq == 0 || post < 8) && Instant::now() < post_deadline {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == KIND_OUTPUT => {\n                    let ev: OutputEvent = serde_json::from_value(f.payload).unwrap();\n                    max_seq = max_seq.max(ev.seq);\n                    post += 1;\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n        // Hand the Stream back to main UNREAD (main holds it = the black-holed\n        // client). The broker's R-writer stays wedged on the remaining burst.\n        let _ = hand_tx.send((r, max_seq));\n    });\n\n    // Main: recv SEED-reached, freeze the PRE-burst high-water (trigger NOT fired —\n    // R is parked on go_rx), release R.\n    let (saw_seed, _seed_max) = seed_rx\n        .recv_timeout(Duration::from_secs(35))\n        .unwrap_or((false, 0));\n    let seed_high = broker.session_output_seq(sid_b).unwrap_or(0);\n    let _ = go_tx.send(());\n    // Recv the handed-back black-holed Stream (held UNREAD for the rest of the test).\n    let handed = hand_rx.recv_timeout(Duration::from_secs(25)).ok();\n    let mut r_stream: Option<Stream> = None;\n    let mut max_drained_seq: u64 = 0;\n    if let Some((stream, mds)) = handed {\n        r_stream = Some(stream);\n        max_drained_seq = mds;\n    }\n\n    // Prove the burst is real: the victim's ring high-water advances ≥64 frames\n    // past the trigger (independent of the wedged controller).\n    let mut burst_advanced = false;\n    let step_deadline = Instant::now() + Duration::from_secs(20);\n    while Instant::now() < step_deadline {\n        if let Some(hw) = broker.session_output_seq(sid_b) {\n            if hw >= seed_high + 64 {\n                burst_advanced = true;\n                break;\n            }\n        }\n        thread::sleep(Duration::from_millis(100));\n    }\n\n    // INVARIANT 1 baseline: the unrelated viewer must ALREADY be ticking before\n    // the incident window opens (so the \"kept advancing across the incident\"\n    // assertion below is meaningful, not vacuous).\n    let mut u_baseline = 0u64;\n    let u_base_deadline = Instant::now() + Duration::from_secs(15);\n    while Instant::now() < u_base_deadline {\n        u_baseline = u_frames.load(Ordering::Relaxed);\n        if u_baseline > 0 {\n            break;\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n\n    // ── Observe exactly ONE logical stall eviction via an INDEPENDENT DRAINING\n    //    control-plane connection (the KIND_SESSIONS reap runs the 400 ms\n    //    stall-evict). Record t_release + freeze the resume cursor for the sid_b\n    //    row. Accept a tally of {1, 2}: POISON-BEATS-EVICT (F-039 rider,\n    //    doyle-ruled) — the physical watchdog can poison first and record the same\n    //    release class, so the tally can step 0→2 when watchdog races the reap. ──\n    let mut stall_evicts_seen: u64 = 0;\n    let mut t_release: Option<Instant> = None;\n    let mut frozen_cursor: Option<u64> = None;\n    let step6_deadline = Instant::now() + Duration::from_secs(10);\n    while Instant::now() < step6_deadline {\n        let sessions = query_sessions(&name, Duration::from_secs(3));\n        let count = stall_evict_count(&name, Duration::from_secs(3));\n        if count >= 1 {\n            stall_evicts_seen = count;\n            t_release = Some(Instant::now());\n            frozen_cursor = sessions\n                .as_ref()\n                .and_then(|r| r.sessions.iter().find(|s| s.session_id == sid_b))\n                .map(|s| s.resume_seq)\n                .or_else(|| {\n                    query_sessions(&name, Duration::from_secs(3)).and_then(|r| {\n                        r.sessions\n                            .iter()\n                            .find(|s| s.session_id == sid_b)\n                            .map(|s| s.resume_seq)\n                    })\n                });\n            break;\n        }\n        thread::sleep(Duration::from_millis(150));\n    }\n\n    // ── INVARIANT 3 (its writer exits): WITHOUT dropping R's Stream, poll\n    //    conn_handler_count() until it drops to the UNRELATED-VIEWER FLOOR (<= 1),\n    //    bounded at t_release + 2 s. `conn_handlers` is a GLOBAL atomic of live\n…\n}"}],"details":{"displayContent":{"text":"//! MSG-IDENTITY W6 / F-039 leg (e) — the DETERMINISTIC BLACK-HOLED-CONTROLLER\n//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker\n//! hosting REAL PTY children over the real local IPC.\n//!\n//! ## What this is\n//!\n//! This is hertz's RCA fix-shape item 5 made executable (`.claude/\n//! hertz-pty-freeze-rca.md` §Proposed fix shape, item 5): the standing\n//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the\n//! EXACT Windows named-pipe black-hole (a controller that stops draining while a\n//! finite burst floods its socket, so the broker's writer parks inside\n//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` — no supported\n//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/\n//! src/conn.rs` bounded/cancelable/poison-on-failure writes, landed fc048bb),\n//! asserts hertz's FIVE invariants for ONE incident:\n//!\n//!   1. UNRELATED SESSIONS CONTINUE — a second, independent session's viewer keeps\n//!      receiving output throughout the black-hole incident (the fan-out never\n//!      blocks the whole broker on the one wedged conn);\n//!   2. THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE BOUND — a write\n//!      on the retired old-client connection FAILS, and the conn's `handle_conn`\n//!      thread finishes within the write bound of logical release (the conn was\n//!      physically retired, not merely logically stall-evicted);\n//!   3. ITS WRITER EXITS — `broker.conn_handler_count()` falls to the\n//!      unrelated-viewer floor (<= 1; the persistent invariant-1 viewer holds one\n//!      handler) WITHOUT the black-holed client ever dropping its end (the parked\n//!      writer was aborted + the reader EOF-cleaned, so the handler thread ran to\n//!      completion), and the viewer flows output AFTER the sample latches — proving\n//!      the survivor is the viewer's handler, not a leaked one;\n//!   4. A FRESH VIEWER CAN ATTACH — a brand-new Viewer subscribe on the retired\n//!      session COMPLETES and RECEIVES replayed output from the frozen cursor\n//!      (the fan-out accepts + serves a new subscriber after retirement);\n//!   5. NO LOCK OR TASK REMAINS OWNED BY THE RETIRED CONNECTION — asserted via\n//!      PROXIES (this invariant has no direct seam): after retirement the broker\n//!      still answers a control-plane `KIND_SESSIONS` request PROMPTLY (no lock\n//!      is held by the retired writer), `conn_handler_count()` returns to the\n//!      unrelated-viewer floor without the client dropping its end (no task\n//!      lingers), AND both the\n//!      unrelated session's viewer and the fresh viewer flow output (the shared\n//!      fan-out lock is free). The conjunction of these three is the strongest\n//!      deterministic proxy available for \"nothing is still owned.\"\n//!\n//! ## Relationship to `brain_decouple.rs`\n//!\n//! This harness GENERALIZES `brain_decouple.rs`'s r4 gate\n//! (`non_draining_controller_stall_evict_releases_writer_and_connection`, the\n//! Windows-mandatory physical-retirement leg for\n//! REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, which stays the primary\n//! gate for that hazard). It reuses the same SEED/trigger/burst choreography,\n//! the hand-back-the-unread-Stream pattern, the `conn_handler_count` and\n//! `stall_evict_count` seams, the old-client-write-fails probe, and the\n//! teardown-before-assert discipline. It ADDS the two pieces the r4 gate does\n//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n//! UNRELATED session whose viewer keeps ticking across the incident, and a fresh\n//! VIEWER attach (not just a controller take) proven to receive output.\n//!\n//! ## Why Windows-mandatory (`#![cfg(windows)]` — the whole file)\n//!\n//! Invariants 2, 3, and the task-ownership proxy of 5 are the PHYSICAL-retirement\n//! asserts, and the deterministic black-hole only reproduces on the Windows\n//! named-pipe path (`interprocess 2.4.2` → `WriteFileEx`+`SleepEx(INFINITE)`,\n//! `set_timeout` → `Unsupported`) — the exact field failure. On Unix the\n//! poison/retire path is a `shutdown(SHUT_RDWR)` abort of a buffer-fill block: a\n//! genuinely DIFFERENT mechanism, not this incident. Faking a Unix companion\n//! would test a different thing, so — mirroring the r4 gate's own\n//! `#[cfg(windows)]` leg — the entire harness is Windows-only. On other targets\n//! this test binary compiles to zero tests (no fakery).\n//!\n//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n//!\n//! Shares ONE process-global `SPT_HOME` + `SPT_BRAIN_WRITE_DEADLINE_MS`, spins a\n//! REAL broker + TWO PTY children. HEAVY class. Run isolated with\n//! `cargo nextest run -p spt-daemon --test conn_blackhole_lifecycle` (CI's path)\n//! or `cargo test --test conn_blackhole_lifecycle -- --test-threads=1`. NEVER a\n//! bare `cargo test --test conn_blackhole_lifecycle` on Windows. Every read in\n//! this test rides a bounded thread+channel or a polled deadline; the test\n//! captures plain values as it goes and only asserts AFTER teardown, so it can\n//! never hang even on broken code.\n#![cfg(windows)]\n\n…\nfn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n…\n    thread::spawn(move || {\n…\n        loop {\n            match read_frame(&mut c) {\n…\n                Err(_) => return,\n            }\n        }\n    });\n    rx.recv_timeout(deadline).ok()\n}\n\n// [int->REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS]\n#[test]\nfn blackholed_controller_lifecycle_five_invariants() {\n    init_home();\n    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n    // instead of 15 s — the structural outcome, fast + deterministic (the knob\n    // grants wall-clock only; it never relaxes an assertion).\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n\n    // ── UNRELATED session U (invariant 1): a second, independent flood child on\n    //    its OWN endpoint. Drop its spawner conn (so U has no controller to wedge\n    //    — the PTY keeps producing headless), and attach a SEPARATE DRAINING\n    //    viewer whose frame count must keep advancing ACROSS the whole incident. ──\n    let (u_spawn, sid_u) = spawn_session(&name, flood_spawn_req(\"blackhole-unrelated-ep\"));\n    drop(u_spawn);\n    let u_name = name.clone();\n    let u_frames = Arc::new(AtomicU64::new(0));\n    let u_frames_w = Arc::clone(&u_frames);\n    thread::spawn(move || {\n        let mut v = connect(&u_name);\n        send(\n            &mut v,\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id: sid_u,\n                from_seq: 0,\n                intent: AttachIntent::Viewer,\n                by: Some(\"unrelated-viewer\".to_string()),\n            })\n            .unwrap(),\n        );\n        while let Ok(f) = read_frame(&mut v) {\n            if f.kind == KIND_OUTPUT {\n                u_frames_w.fetch_add(1, Ordering::Relaxed);\n            }\n        }\n    });\n\n    // ── BLACK-HOLED session B: the real SEED+burst PTY child from conn A, then\n    //    DROP A so the ONLY persistent brain conn is the black-holed controller R. ──\n    let (a, sid_b) = spawn_session(&name, seed_burst_spawn_req(\"blackhole-victim-ep\"));\n    drop(a);\n\n    // ── Remote controller R: subscribes Control from_seq 0, drains until it has\n    //    seen `SEED` (a real controller frame flowed), signals main so main can\n    //    freeze the PRE-burst high-water, then on main's go fires the burst trigger,\n    //    drains a few burst frames (a real seq>0 frame delivered), and STOPS\n    //    reading — handing the Stream back to main UNREAD. Main holds it, never\n    //    draining, so the broker's R-writer parks in `write_frame` exactly as\n    //    against a suspended brain. R's Stream is NEVER dropped until the very end. ──\n    let r_name = name.clone();\n    let (seed_tx, seed_rx) = std::sync::mpsc::channel::<(bool, u64)>();\n    let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();\n    let (hand_tx, hand_rx) = std::sync::mpsc::channel::<(Stream, u64)>();\n    thread::spawn(move || {\n        let mut r = connect(&r_name);\n        send(\n            &mut r,\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id: sid_b,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                by: Some(\"operator-one\".to_string()),\n            })\n            .unwrap(),\n        );\n        loop {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == KIND_SUBSCRIBED => break,\n                Ok(_) => continue,\n                Err(_) => {\n                    let _ = seed_tx.send((false, 0));\n                    return;\n                }\n            }\n        }\n        // Drain live output until the CONCATENATED decoded bytes contain `SEED`\n        // (ConPTY echoes input + emits VT noise, so per-frame matching is\n        // unreliable). Track the max seq seen.\n        let mut acc: Vec<u8> = Vec::new();\n        let mut max_seq: u64 = 0;\n        let mut saw_seed = false;\n        let seed_deadline = Instant::now() + Duration::from_secs(30);\n        while Instant::now() < seed_deadline {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == KIND_OUTPUT => {\n                    let ev: OutputEvent = serde_json::from_value(f.payload).unwrap();\n                    max_seq = max_seq.max(ev.seq);\n                    if let Ok(bytes) = decode_bytes(&ev.data_b64) {\n                        acc.extend_from_slice(&bytes);\n                    }\n                    if acc.windows(4).any(|w| w == b\"SEED\") {\n                        saw_seed = true;\n                        break;\n                    }\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n        let _ = seed_tx.send((saw_seed, max_seq));\n        // Wait for main's go, then fire the burst trigger (`\\r\\n` satisfies the\n        // first Read-Host; ack=false = the rc/operator fire-and-forward path).\n        let _ = go_rx.recv_timeout(Duration::from_secs(15));\n        send(\n            &mut r,\n            KIND_INPUT,\n            serde_json::to_value(InputReq {\n                session_id: sid_b,\n                data_b64: encode_bytes(b\"\\r\\n\"),\n                op_id: None,\n                minter: None,\n                ack: false,\n            })\n            .unwrap(),\n        );\n        // Drain a bounded handful of burst frames so a REAL seq>0 controller frame\n        // is proven delivered, then STOP reading forever (the black-hole). The\n        // remaining ~1300 frames flood the now-unread socket → R-writer wedges.\n        let post_deadline = Instant::now() + Duration::from_secs(5);\n        let mut post = 0u32;\n        while (max_seq == 0 || post < 8) && Instant::now() < post_deadline {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == KIND_OUTPUT => {\n                    let ev: OutputEvent = serde_json::from_value(f.payload).unwrap();\n                    max_seq = max_seq.max(ev.seq);\n                    post += 1;\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n        // Hand the Stream back to main UNREAD (main holds it = the black-holed\n        // client). The broker's R-writer stays wedged on the remaining burst.\n        let _ = hand_tx.send((r, max_seq));\n    });\n\n    // Main: recv SEED-reached, freeze the PRE-burst high-water (trigger NOT fired —\n    // R is parked on go_rx), release R.\n    let (saw_seed, _seed_max) = seed_rx\n        .recv_timeout(Duration::from_secs(35))\n        .unwrap_or((false, 0));\n    let seed_high = broker.session_output_seq(sid_b).unwrap_or(0);\n    let _ = go_tx.send(());\n    // Recv the handed-back black-holed Stream (held UNREAD for the rest of the test).\n    let handed = hand_rx.recv_timeout(Duration::from_secs(25)).ok();\n    let mut r_stream: Option<Stream> = None;\n    let mut max_drained_seq: u64 = 0;\n    if let Some((stream, mds)) = handed {\n        r_stream = Some(stream);\n        max_drained_seq = mds;\n    }\n\n    // Prove the burst is real: the victim's ring high-water advances ≥64 frames\n    // past the trigger (independent of the wedged controller).\n    let mut burst_advanced = false;\n    let step_deadline = Instant::now() + Duration::from_secs(20);\n    while Instant::now() < step_deadline {\n        if let Some(hw) = broker.session_output_seq(sid_b) {\n            if hw >= seed_high + 64 {\n                burst_advanced = true;\n                break;\n            }\n        }\n        thread::sleep(Duration::from_millis(100));\n    }\n\n    // INVARIANT 1 baseline: the unrelated viewer must ALREADY be ticking before\n    // the incident window opens (so the \"kept advancing across the incident\"\n    // assertion below is meaningful, not vacuous).\n    let mut u_baseline = 0u64;\n    let u_base_deadline = Instant::now() + Duration::from_secs(15);\n    while Instant::now() < u_base_deadline {\n        u_baseline = u_frames.load(Ordering::Relaxed);\n        if u_baseline > 0 {\n            break;\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n\n    // ── Observe exactly ONE logical stall eviction via an INDEPENDENT DRAINING\n    //    control-plane connection (the KIND_SESSIONS reap runs the 400 ms\n    //    stall-evict). Record t_release + freeze the resume cursor for the sid_b\n    //    row. Accept a tally of {1, 2}: POISON-BEATS-EVICT (F-039 rider,\n    //    doyle-ruled) — the physical watchdog can poison first and record the same\n    //    release class, so the tally can step 0→2 when watchdog races the reap. ──\n    let mut stall_evicts_seen: u64 = 0;\n    let mut t_release: Option<Instant> = None;\n    let mut frozen_cursor: Option<u64> = None;\n    let step6_deadline = Instant::now() + Duration::from_secs(10);\n    while Instant::now() < step6_deadline {\n        let sessions = query_sessions(&name, Duration::from_secs(3));\n        let count = stall_evict_count(&name, Duration::from_secs(3));\n        if count >= 1 {\n            stall_evicts_seen = count;\n            t_release = Some(Instant::now());\n            frozen_cursor = sessions\n                .as_ref()\n                .and_then(|r| r.sessions.iter().find(|s| s.session_id == sid_b))\n                .map(|s| s.resume_seq)\n                .or_else(|| {\n                    query_sessions(&name, Duration::from_secs(3)).and_then(|r| {\n                        r.sessions\n                            .iter()\n                            .find(|s| s.session_id == sid_b)\n                            .map(|s| s.resume_seq)\n                    })\n                });\n            break;\n        }\n        thread::sleep(Duration::from_millis(150));\n    }\n\n    // ── INVARIANT 3 (its writer exits): WITHOUT dropping R's Stream, poll\n    //    conn_handler_count() until it drops to the UNRELATED-VIEWER FLOOR (<= 1),\n    //    bounded at t_release + 2 s. `conn_handlers` is a GLOBAL atomic of live\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,null,226,null,229,null,232,233,null,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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,462,463,464,465,466,467,468,469,470,null,709]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\conn_blackhole_lifecycle.rs"}}},"isError":false,"timestamp":1784195833921}}
{"type":"message","id":"bc897559","parentId":"6e8de6d4","timestamp":"2026-07-16T09:57:13.922Z","message":{"role":"toolResult","toolCallId":"call_VWCHWIdpi8IWddgi5sN5zNia|fc_008390f508d1d09b016a58aaf9cb9c819a831500a6be0a92a6","toolName":"read","content":[{"type":"text","text":"//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\n//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\n//!\n//! ## The bug this gate locks down (the update-wedge root)\n//!\n//! A controller's writer thread does a BLOCKING socket write to its brain\n//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\n//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\n//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\n//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\n//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\n//! evicted by neither: control latched, reattach was refused, `rc --take` hung,\n//! `daemon status` still read healthy.\n//!\n//! ## Faithful in-process analog of a suspended brain\n//!\n//! A test process cannot `NtSuspendProcess` itself, so the rig reproduces the exact\n//! MECHANISM: controller A stops reading its socket. The flood child fills A's recv\n//! buffer, the controller channel fills, and A's writer thread parks INSIDE\n//! `write_frame` — precisely the state a suspended brain leaves its broker conn in\n//! (a peer that stopped draining). This is the same analog the 7.12 wedge gate uses.\n//! `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15 s production deadline so the\n//! structural outcome is provable in ~1 s (the deadline knob grants wall-clock only;\n//! it never relaxes an assertion).\n//!\n//! ## What this rig asserts (exactly four — the decouple invariant)\n//!\n//! With the wedged controller's writer parked past the brain-write deadline:\n//! 1. an attached VIEWER keeps ticking (the fan-out never blocks on the wedged conn);\n//! 2. a second operator's `Control` subscribe COMPLETES — the wedged incumbent is\n//!    stall-evicted broker-side and the newcomer takes the freed slot (pre-W2 this\n//!    busy-refused forever);\n//! 3. that take resolves PROMPTLY (well under the watchdog — a stall-evict, not a hang);\n//! 4. the stall-evict is OBSERVABLE (the broker-global tally increments, the datum\n//!    `daemon status` surfaces).\n//!\n//! What this rig does NOT assert (covered elsewhere / manual): resume + cursor-replay\n//! byte-identity is NOT re-proven here — W2's delta does not touch the replay path, so\n//! output integrity stays covered by the pre-existing ring/cursor units (the effect-\n//! journal replay dedup, the `delivered_through` no-rewind CAS `advanced_cursor`, and\n//! the clean-repaint cold-attach batch). A live `NtSuspendProcess` / `SIGSTOP` run on a\n//! real brain child remains the MANUAL field rig from the incident night — this CI test\n//! is the deterministic in-process ANALOG, not that run.\n//!\n//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n//!\n//! Shares ONE process-global `SPT_HOME` + the `SPT_BRAIN_WRITE_DEADLINE_MS` env, and\n//! spins a REAL broker + PTY child. Run isolated with\n//! `cargo nextest run --test brain_decouple` (process-per-test — CI's path) or\n//! `cargo test --test brain_decouple -- --test-threads=1`. Never a bare\n…\nfn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n…\n    thread::spawn(move || {\n…\n        loop {\n…\n        }\n    });\n    rx.recv_timeout(deadline).unwrap_or(0)\n}\n\n// [int->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n#[test]\nfn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\n    init_home();\n    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\n    // 5 s output-driven CONTROLLER_WRITE_DEADLINE, so ONLY the W2 time-based\n    // stall-evict can free the slot in the window this test acts in.\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n\n    // ── Spawner A: spawn the flood child (dispatch_spawn pre-attaches A as the\n    //    LOCAL controller, by=None). ──\n    let mut a = connect(&name);\n    send(\n        &mut a,\n        KIND_SPAWN,\n        serde_json::to_value(flood_spawn_req(\"decouple-ep\")).unwrap(),\n    );\n    let sid = loop {\n        let f = read_frame(&mut a).expect(\"frame before spawned\");\n        if f.kind == KIND_SPAWNED {\n            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;\n        }\n    };\n\n    // ── Remote controller R: a REMOTE operator (by=Some) takes control of the\n    //    (locally-spawned, undriven) session, then STOPS reading — the suspended-\n    //    brain analog. R MUST be a remote controller: a second remote's Control\n    //    subscribe against a DIFFERENT-remote-driven session busy-refuses PRE-W2\n    //    (the wedge), so only the stall-evict can free the slot — this is what\n    //    isolates the fix from the pre-existing \"a remote takes an UNDRIVEN\n    //    (local-only) session\" silent-control path. R's conn is held open (never\n    //    read from) on its own thread so the broker's writer for R parks in\n    //    write_frame exactly as against a suspended brain. ──\n    let rname = name.clone();\n    thread::spawn(move || {\n        let mut r = connect(&rname);\n        send(\n            &mut r,\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id: sid,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                by: Some(\"operator-one\".to_string()),\n            })\n            .unwrap(),\n        );\n        // Read ONLY the subscribed reply, then go silent forever holding the conn.\n        loop {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == KIND_SUBSCRIBED => break,\n                Ok(_) => continue,\n                Err(_) => return,\n            }\n        }\n        // Hold the conn open, never draining — the broker's R-writer wedges on the\n        // flood. Park long past the test; the process-per-test exit reaps it.\n        thread::sleep(Duration::from_secs(120));\n        drop(r);\n    });\n    // Give R time to become the controller and its writer to wedge on the flood.\n    thread::sleep(Duration::from_millis(300));\n    // The local spawner is displaced by R's take; drop it (no longer the controller).\n    drop(a);\n\n    // ── Viewer V: a SEPARATE conn that DOES drain. Prove the fan-out keeps ticking\n    //    to V while A's writer is wedged (the drain never blocks on the wedged conn). ──\n    let vname = name.clone();\n    let v_frames = Arc::new(std::sync::atomic::AtomicU64::new(0));\n    let v_frames_w = Arc::clone(&v_frames);\n    thread::spawn(move || {\n        let mut v = connect(&vname);\n        send(\n            &mut v,\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id: sid,\n                from_seq: 0,\n                intent: AttachIntent::Viewer,\n                by: Some(\"viewer-node\".to_string()),\n            })\n            .unwrap(),\n        );\n        while let Ok(f) = read_frame(&mut v) {\n            if f.kind != KIND_SUBSCRIBED {\n                v_frames_w.fetch_add(1, Ordering::Relaxed);\n            }\n        }\n    });\n\n    // Let A's writer wedge past the (shrunk) brain-write deadline, and let the viewer\n    // accumulate frames during the wedge.\n    thread::sleep(Duration::from_millis(900));\n    let ticks_during_wedge = v_frames.load(Ordering::Relaxed);\n\n    // ── The take: a DIFFERENT operator's Control subscribe. Pre-W2 the wedged\n    //    incumbent (A) is present → this busy-refuses forever. With the W2\n    //    stall-evict, A is dropped broker-side and B takes the freed slot. ──\n    let start = Instant::now();\n    let outcome = subscribe(\n        &name,\n        sid,\n        AttachIntent::Control,\n        Some(\"operator-two\"),\n        Duration::from_secs(10),\n    );\n    let took = start.elapsed();\n\n    let evicts = stall_evict_count(&name, Duration::from_secs(5));\n\n    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\n    if let Some(pid) = broker.session_pid(sid) {\n        kill_pid(pid);\n    }\n\n    eprintln!(\n        \"=== W2 GATE: viewer_ticks_during_wedge={ticks_during_wedge} take_outcome={outcome:?} \\\n         take_elapsed={took:?} stall_evicts={evicts} ===\"\n    );\n\n    // The viewer kept ticking while the controller conn was wedged.\n    assert!(\n        ticks_during_wedge > 0,\n        \"an attached viewer must keep receiving output while the controller conn is \\\n         wedged — the fan-out must never block on the stalled brain conn\"\n    );\n    // The take COMPLETED and took control (the wedged incumbent was stall-evicted).\n    assert!(\n        matches!(\n            outcome,\n            Some(SubscribeOutcome::Controller) | Some(SubscribeOutcome::TookControl)\n        ),\n        \"a second operator's Control subscribe must COMPLETE by stall-evicting the \\\n         wedged incumbent (pre-W2: busy-refused forever). got {outcome:?}\"\n    );\n    // It resolved WELL under the 10 s watchdog (the stall-evict is prompt, not a hang).\n    assert!(\n        took < Duration::from_secs(9),\n        \"the take must self-heal promptly, not ride the watchdog: {took:?}\"\n    );\n    // The stall-evict is observable (the broker-side release the daemon-status\n    // surface reports).\n    assert!(\n        evicts >= 1,\n        \"the stall-evict must be recorded in the broker-global tally (daemon-status \\\n         observability rider); got {evicts}\"\n    );\n}\n…\n\n// [int->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n#[cfg(windows)]\n#[test]\nfn non_draining_controller_stall_evict_releases_writer_and_connection() {\n    init_home();\n    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms.\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n\n    // ── Step 2: spawn the real SEED+burst PTY child from conn A, then DROP A so the\n    //    ONLY persistent brain conn later is the black-holed controller R. ──\n    let mut a = connect(&name);\n    send(\n        &mut a,\n        KIND_SPAWN,\n        serde_json::to_value(seed_burst_spawn_req(\"uwedge-r4-ep\")).unwrap(),\n    );\n    let sid = loop {\n        let f = read_frame(&mut a).expect(\"frame before spawned\");\n        if f.kind == KIND_SPAWNED {\n            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;\n        }\n    };\n    drop(a);\n\n    // ── Steps 3+4: remote controller R. On a dedicated thread it subscribes Control\n    //    from_seq 0, drains output until it has seen `SEED` (proving a real controller\n    //    frame flowed), signals main so main can freeze the PRE-burst high-water, then\n    //    on main's go it fires the burst trigger and drains a few burst frames (a real\n    //    seq>0 frame delivered to the controller) and STOPS reading. It hands the\n    //    Stream back to main UNREAD — main holds it, never draining, so the broker's\n    //    R-writer parks in `write_frame` exactly as against a suspended brain. R's\n    //    Stream is NEVER dropped until the very end of the test. ──\n    let r_name = name.clone();\n    let (seed_tx, seed_rx) = std::sync::mpsc::channel::<(bool, u64)>();\n    let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();\n    let (hand_tx, hand_rx) = std::sync::mpsc::channel::<(Stream, u64)>();\n    thread::spawn(move || {\n        let mut r = connect(&r_name);\n        send(\n            &mut r,\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id: sid,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                by: Some(\"operator-one\".to_string()),\n            })\n            .unwrap(),\n        );\n        // Read until SUBSCRIBED.\n        loop {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == KIND_SUBSCRIBED => break,\n                Ok(_) => continue,\n                Err(_) => {\n                    let _ = seed_tx.send((false, 0));\n                    return;\n                }\n            }\n        }\n        // Drain live output until the concatenated decoded bytes contain `SEED`\n        // (search the CONCATENATED stream — ConPTY echoes input and emits VT noise,\n        // so per-frame matching is unreliable). Track the max seq seen.\n        let mut acc: Vec<u8> = Vec::new();\n        let mut max_seq: u64 = 0;\n        let mut saw_seed = false;\n        let seed_deadline = Instant::now() + Duration::from_secs(30);\n        while Instant::now() < seed_deadline {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == spt_daemon::msg::KIND_OUTPUT => {\n                    let ev: spt_daemon::msg::OutputEvent =\n                        serde_json::from_value(f.payload).unwrap();\n                    max_seq = max_seq.max(ev.seq);\n                    if let Ok(bytes) = spt_daemon::msg::decode_bytes(&ev.data_b64) {\n                        acc.extend_from_slice(&bytes);\n                    }\n                    if acc.windows(4).any(|w| w == b\"SEED\") {\n                        saw_seed = true;\n                        break;\n                    }\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n        // Signal SEED reached (PRE-trigger: the child is parked on the first\n        // Read-Host, so main can freeze the pre-burst high-water now).\n        let _ = seed_tx.send((saw_seed, max_seq));\n        // Wait for main's go, then fire the burst trigger. `\\r\\n` satisfies the first\n        // Read-Host; ack=false (the rc/operator fire-and-forward path).\n        let _ = go_rx.recv_timeout(Duration::from_secs(15));\n        send(\n            &mut r,\n            spt_daemon::msg::KIND_INPUT,\n            serde_json::to_value(spt_daemon::msg::InputReq {\n                session_id: sid,\n                data_b64: spt_daemon::msg::encode_bytes(b\"\\r\\n\"),\n                op_id: None,\n                minter: None,\n                ack: false,\n            })\n            .unwrap(),\n        );\n        // Drain a bounded handful of burst frames so a REAL seq>0 controller frame is\n        // proven delivered, then STOP reading forever (the black-hole). The remaining\n        // ~1300 frames flood the now-unread socket → the R-writer wedges in write_frame.\n        let post_deadline = Instant::now() + Duration::from_secs(5);\n        let mut post = 0u32;\n        while (max_seq == 0 || post < 8) && Instant::now() < post_deadline {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == spt_daemon::msg::KIND_OUTPUT => {\n                    let ev: spt_daemon::msg::OutputEvent =\n                        serde_json::from_value(f.payload).unwrap();\n                    max_seq = max_seq.max(ev.seq);\n                    post += 1;\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n        // Hand the Stream back to main UNREAD (main holds it = the black-holed client).\n        let _ = hand_tx.send((r, max_seq));\n        // Thread exits; the broker's R-writer stays wedged on the remaining burst.\n    });\n\n    // Main: recv SEED-reached, then freeze the PRE-burst high-water (the trigger has\n    // NOT fired — R is parked on go_rx).\n    let (saw_seed, _seed_max) = seed_rx\n        .recv_timeout(Duration::from_secs(35))\n        .unwrap_or((false, 0));\n    let seed_high = broker.session_output_seq(sid).unwrap_or(0);\n    let _ = go_tx.send(());\n    // Recv the handed-back black-holed Stream (held UNREAD for the rest of the test).\n    let handed = hand_rx.recv_timeout(Duration::from_secs(25)).ok();\n    let mut r_stream: Option<Stream> = None;\n    let mut max_drained_seq: u64 = 0;\n    if let Some((stream, mds)) = handed {\n        r_stream = Some(stream);\n        max_drained_seq = mds;\n    }\n\n    // ── Step 5: prove the PTY/ring high-water advances ≥ 64 frames past the trigger\n    //    (the burst is real and flowing into the ring, independent of the wedged\n    //    controller). ──\n    let mut burst_advanced = false;\n    let step5_deadline = Instant::now() + Duration::from_secs(20);\n    while Instant::now() < step5_deadline {\n        if let Some(hw) = broker.session_output_seq(sid) {\n            if hw >= seed_high + 64 {\n                burst_advanced = true;\n                break;\n            }\n        }\n        thread::sleep(Duration::from_millis(100));\n    }\n\n    // ── Step 6: exactly ONE logical stall eviction, observed via an INDEPENDENT\n    //    DRAINING control-plane connection (the KIND_SESSIONS reap runs the 400 ms\n    //    stall-evict). Record t_release at first observation and freeze the resume\n    //    cursor for the sid row. ──\n    // POISON-BEATS-EVICT (F-039 rider, doyle-ruled 2026-07-10, 3rd CI trip): the\n    // ONE wedged controller's release can be tallied by the LOGICAL reap\n    // (stall_evict_controller), by the PHYSICAL bound (the conn watchdog poisons\n    // first and note_controller_write_retired records the same event class —\n    // the field-accepted v0.30.6 behavior), or by BOTH when the watchdog races\n    // the reap's blocked-window sample. So the tally can step 0→2 and an\n    // `== 1` break-condition misses it forever (the evicts_seen=0 flake). The\n    // loop breaks on ≥1; the assert below accepts {1, 2} — anything more is a\n    // real second eviction and still fails.\n    let mut stall_evicts_seen: u64 = 0;\n    let mut t_release: Option<Instant> = None;\n    let mut frozen_cursor: Option<u64> = None;\n    let step6_deadline = Instant::now() + Duration::from_secs(10);\n…\n}"}],"details":{"displayContent":{"text":"//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\n//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\n//!\n//! ## The bug this gate locks down (the update-wedge root)\n//!\n//! A controller's writer thread does a BLOCKING socket write to its brain\n//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\n//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\n//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\n//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\n//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\n//! evicted by neither: control latched, reattach was refused, `rc --take` hung,\n//! `daemon status` still read healthy.\n//!\n//! ## Faithful in-process analog of a suspended brain\n//!\n//! A test process cannot `NtSuspendProcess` itself, so the rig reproduces the exact\n//! MECHANISM: controller A stops reading its socket. The flood child fills A's recv\n//! buffer, the controller channel fills, and A's writer thread parks INSIDE\n//! `write_frame` — precisely the state a suspended brain leaves its broker conn in\n//! (a peer that stopped draining). This is the same analog the 7.12 wedge gate uses.\n//! `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15 s production deadline so the\n//! structural outcome is provable in ~1 s (the deadline knob grants wall-clock only;\n//! it never relaxes an assertion).\n//!\n//! ## What this rig asserts (exactly four — the decouple invariant)\n//!\n//! With the wedged controller's writer parked past the brain-write deadline:\n//! 1. an attached VIEWER keeps ticking (the fan-out never blocks on the wedged conn);\n//! 2. a second operator's `Control` subscribe COMPLETES — the wedged incumbent is\n//!    stall-evicted broker-side and the newcomer takes the freed slot (pre-W2 this\n//!    busy-refused forever);\n//! 3. that take resolves PROMPTLY (well under the watchdog — a stall-evict, not a hang);\n//! 4. the stall-evict is OBSERVABLE (the broker-global tally increments, the datum\n//!    `daemon status` surfaces).\n//!\n//! What this rig does NOT assert (covered elsewhere / manual): resume + cursor-replay\n//! byte-identity is NOT re-proven here — W2's delta does not touch the replay path, so\n//! output integrity stays covered by the pre-existing ring/cursor units (the effect-\n//! journal replay dedup, the `delivered_through` no-rewind CAS `advanced_cursor`, and\n//! the clean-repaint cold-attach batch). A live `NtSuspendProcess` / `SIGSTOP` run on a\n//! real brain child remains the MANUAL field rig from the incident night — this CI test\n//! is the deterministic in-process ANALOG, not that run.\n//!\n//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n//!\n//! Shares ONE process-global `SPT_HOME` + the `SPT_BRAIN_WRITE_DEADLINE_MS` env, and\n//! spins a REAL broker + PTY child. Run isolated with\n//! `cargo nextest run --test brain_decouple` (process-per-test — CI's path) or\n//! `cargo test --test brain_decouple -- --test-threads=1`. Never a bare\n…\nfn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n…\n    thread::spawn(move || {\n…\n        loop {\n…\n        }\n    });\n    rx.recv_timeout(deadline).unwrap_or(0)\n}\n\n// [int->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n#[test]\nfn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\n    init_home();\n    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\n    // 5 s output-driven CONTROLLER_WRITE_DEADLINE, so ONLY the W2 time-based\n    // stall-evict can free the slot in the window this test acts in.\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n\n    // ── Spawner A: spawn the flood child (dispatch_spawn pre-attaches A as the\n    //    LOCAL controller, by=None). ──\n    let mut a = connect(&name);\n    send(\n        &mut a,\n        KIND_SPAWN,\n        serde_json::to_value(flood_spawn_req(\"decouple-ep\")).unwrap(),\n    );\n    let sid = loop {\n        let f = read_frame(&mut a).expect(\"frame before spawned\");\n        if f.kind == KIND_SPAWNED {\n            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;\n        }\n    };\n\n    // ── Remote controller R: a REMOTE operator (by=Some) takes control of the\n    //    (locally-spawned, undriven) session, then STOPS reading — the suspended-\n    //    brain analog. R MUST be a remote controller: a second remote's Control\n    //    subscribe against a DIFFERENT-remote-driven session busy-refuses PRE-W2\n    //    (the wedge), so only the stall-evict can free the slot — this is what\n    //    isolates the fix from the pre-existing \"a remote takes an UNDRIVEN\n    //    (local-only) session\" silent-control path. R's conn is held open (never\n    //    read from) on its own thread so the broker's writer for R parks in\n    //    write_frame exactly as against a suspended brain. ──\n    let rname = name.clone();\n    thread::spawn(move || {\n        let mut r = connect(&rname);\n        send(\n            &mut r,\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id: sid,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                by: Some(\"operator-one\".to_string()),\n            })\n            .unwrap(),\n        );\n        // Read ONLY the subscribed reply, then go silent forever holding the conn.\n        loop {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == KIND_SUBSCRIBED => break,\n                Ok(_) => continue,\n                Err(_) => return,\n            }\n        }\n        // Hold the conn open, never draining — the broker's R-writer wedges on the\n        // flood. Park long past the test; the process-per-test exit reaps it.\n        thread::sleep(Duration::from_secs(120));\n        drop(r);\n    });\n    // Give R time to become the controller and its writer to wedge on the flood.\n    thread::sleep(Duration::from_millis(300));\n    // The local spawner is displaced by R's take; drop it (no longer the controller).\n    drop(a);\n\n    // ── Viewer V: a SEPARATE conn that DOES drain. Prove the fan-out keeps ticking\n    //    to V while A's writer is wedged (the drain never blocks on the wedged conn). ──\n    let vname = name.clone();\n    let v_frames = Arc::new(std::sync::atomic::AtomicU64::new(0));\n    let v_frames_w = Arc::clone(&v_frames);\n    thread::spawn(move || {\n        let mut v = connect(&vname);\n        send(\n            &mut v,\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id: sid,\n                from_seq: 0,\n                intent: AttachIntent::Viewer,\n                by: Some(\"viewer-node\".to_string()),\n            })\n            .unwrap(),\n        );\n        while let Ok(f) = read_frame(&mut v) {\n            if f.kind != KIND_SUBSCRIBED {\n                v_frames_w.fetch_add(1, Ordering::Relaxed);\n            }\n        }\n    });\n\n    // Let A's writer wedge past the (shrunk) brain-write deadline, and let the viewer\n    // accumulate frames during the wedge.\n    thread::sleep(Duration::from_millis(900));\n    let ticks_during_wedge = v_frames.load(Ordering::Relaxed);\n\n    // ── The take: a DIFFERENT operator's Control subscribe. Pre-W2 the wedged\n    //    incumbent (A) is present → this busy-refuses forever. With the W2\n    //    stall-evict, A is dropped broker-side and B takes the freed slot. ──\n    let start = Instant::now();\n    let outcome = subscribe(\n        &name,\n        sid,\n        AttachIntent::Control,\n        Some(\"operator-two\"),\n        Duration::from_secs(10),\n    );\n    let took = start.elapsed();\n\n    let evicts = stall_evict_count(&name, Duration::from_secs(5));\n\n    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\n    if let Some(pid) = broker.session_pid(sid) {\n        kill_pid(pid);\n    }\n\n    eprintln!(\n        \"=== W2 GATE: viewer_ticks_during_wedge={ticks_during_wedge} take_outcome={outcome:?} \\\n         take_elapsed={took:?} stall_evicts={evicts} ===\"\n    );\n\n    // The viewer kept ticking while the controller conn was wedged.\n    assert!(\n        ticks_during_wedge > 0,\n        \"an attached viewer must keep receiving output while the controller conn is \\\n         wedged — the fan-out must never block on the stalled brain conn\"\n    );\n    // The take COMPLETED and took control (the wedged incumbent was stall-evicted).\n    assert!(\n        matches!(\n            outcome,\n            Some(SubscribeOutcome::Controller) | Some(SubscribeOutcome::TookControl)\n        ),\n        \"a second operator's Control subscribe must COMPLETE by stall-evicting the \\\n         wedged incumbent (pre-W2: busy-refused forever). got {outcome:?}\"\n    );\n    // It resolved WELL under the 10 s watchdog (the stall-evict is prompt, not a hang).\n    assert!(\n        took < Duration::from_secs(9),\n        \"the take must self-heal promptly, not ride the watchdog: {took:?}\"\n    );\n    // The stall-evict is observable (the broker-side release the daemon-status\n    // surface reports).\n    assert!(\n        evicts >= 1,\n        \"the stall-evict must be recorded in the broker-global tally (daemon-status \\\n         observability rider); got {evicts}\"\n    );\n}\n…\n\n// [int->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n#[cfg(windows)]\n#[test]\nfn non_draining_controller_stall_evict_releases_writer_and_connection() {\n    init_home();\n    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms.\n    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n\n    // ── Step 2: spawn the real SEED+burst PTY child from conn A, then DROP A so the\n    //    ONLY persistent brain conn later is the black-holed controller R. ──\n    let mut a = connect(&name);\n    send(\n        &mut a,\n        KIND_SPAWN,\n        serde_json::to_value(seed_burst_spawn_req(\"uwedge-r4-ep\")).unwrap(),\n    );\n    let sid = loop {\n        let f = read_frame(&mut a).expect(\"frame before spawned\");\n        if f.kind == KIND_SPAWNED {\n            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;\n        }\n    };\n    drop(a);\n\n    // ── Steps 3+4: remote controller R. On a dedicated thread it subscribes Control\n    //    from_seq 0, drains output until it has seen `SEED` (proving a real controller\n    //    frame flowed), signals main so main can freeze the PRE-burst high-water, then\n    //    on main's go it fires the burst trigger and drains a few burst frames (a real\n    //    seq>0 frame delivered to the controller) and STOPS reading. It hands the\n    //    Stream back to main UNREAD — main holds it, never draining, so the broker's\n    //    R-writer parks in `write_frame` exactly as against a suspended brain. R's\n    //    Stream is NEVER dropped until the very end of the test. ──\n    let r_name = name.clone();\n    let (seed_tx, seed_rx) = std::sync::mpsc::channel::<(bool, u64)>();\n    let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();\n    let (hand_tx, hand_rx) = std::sync::mpsc::channel::<(Stream, u64)>();\n    thread::spawn(move || {\n        let mut r = connect(&r_name);\n        send(\n            &mut r,\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id: sid,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                by: Some(\"operator-one\".to_string()),\n            })\n            .unwrap(),\n        );\n        // Read until SUBSCRIBED.\n        loop {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == KIND_SUBSCRIBED => break,\n                Ok(_) => continue,\n                Err(_) => {\n                    let _ = seed_tx.send((false, 0));\n                    return;\n                }\n            }\n        }\n        // Drain live output until the concatenated decoded bytes contain `SEED`\n        // (search the CONCATENATED stream — ConPTY echoes input and emits VT noise,\n        // so per-frame matching is unreliable). Track the max seq seen.\n        let mut acc: Vec<u8> = Vec::new();\n        let mut max_seq: u64 = 0;\n        let mut saw_seed = false;\n        let seed_deadline = Instant::now() + Duration::from_secs(30);\n        while Instant::now() < seed_deadline {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == spt_daemon::msg::KIND_OUTPUT => {\n                    let ev: spt_daemon::msg::OutputEvent =\n                        serde_json::from_value(f.payload).unwrap();\n                    max_seq = max_seq.max(ev.seq);\n                    if let Ok(bytes) = spt_daemon::msg::decode_bytes(&ev.data_b64) {\n                        acc.extend_from_slice(&bytes);\n                    }\n                    if acc.windows(4).any(|w| w == b\"SEED\") {\n                        saw_seed = true;\n                        break;\n                    }\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n        // Signal SEED reached (PRE-trigger: the child is parked on the first\n        // Read-Host, so main can freeze the pre-burst high-water now).\n        let _ = seed_tx.send((saw_seed, max_seq));\n        // Wait for main's go, then fire the burst trigger. `\\r\\n` satisfies the first\n        // Read-Host; ack=false (the rc/operator fire-and-forward path).\n        let _ = go_rx.recv_timeout(Duration::from_secs(15));\n        send(\n            &mut r,\n            spt_daemon::msg::KIND_INPUT,\n            serde_json::to_value(spt_daemon::msg::InputReq {\n                session_id: sid,\n                data_b64: spt_daemon::msg::encode_bytes(b\"\\r\\n\"),\n                op_id: None,\n                minter: None,\n                ack: false,\n            })\n            .unwrap(),\n        );\n        // Drain a bounded handful of burst frames so a REAL seq>0 controller frame is\n        // proven delivered, then STOP reading forever (the black-hole). The remaining\n        // ~1300 frames flood the now-unread socket → the R-writer wedges in write_frame.\n        let post_deadline = Instant::now() + Duration::from_secs(5);\n        let mut post = 0u32;\n        while (max_seq == 0 || post < 8) && Instant::now() < post_deadline {\n            match read_frame(&mut r) {\n                Ok(f) if f.kind == spt_daemon::msg::KIND_OUTPUT => {\n                    let ev: spt_daemon::msg::OutputEvent =\n                        serde_json::from_value(f.payload).unwrap();\n                    max_seq = max_seq.max(ev.seq);\n                    post += 1;\n                }\n                Ok(_) => continue,\n                Err(_) => break,\n            }\n        }\n        // Hand the Stream back to main UNREAD (main holds it = the black-holed client).\n        let _ = hand_tx.send((r, max_seq));\n        // Thread exits; the broker's R-writer stays wedged on the remaining burst.\n    });\n\n    // Main: recv SEED-reached, then freeze the PRE-burst high-water (the trigger has\n    // NOT fired — R is parked on go_rx).\n    let (saw_seed, _seed_max) = seed_rx\n        .recv_timeout(Duration::from_secs(35))\n        .unwrap_or((false, 0));\n    let seed_high = broker.session_output_seq(sid).unwrap_or(0);\n    let _ = go_tx.send(());\n    // Recv the handed-back black-holed Stream (held UNREAD for the rest of the test).\n    let handed = hand_rx.recv_timeout(Duration::from_secs(25)).ok();\n    let mut r_stream: Option<Stream> = None;\n    let mut max_drained_seq: u64 = 0;\n    if let Some((stream, mds)) = handed {\n        r_stream = Some(stream);\n        max_drained_seq = mds;\n    }\n\n    // ── Step 5: prove the PTY/ring high-water advances ≥ 64 frames past the trigger\n    //    (the burst is real and flowing into the ring, independent of the wedged\n    //    controller). ──\n    let mut burst_advanced = false;\n    let step5_deadline = Instant::now() + Duration::from_secs(20);\n    while Instant::now() < step5_deadline {\n        if let Some(hw) = broker.session_output_seq(sid) {\n            if hw >= seed_high + 64 {\n                burst_advanced = true;\n                break;\n            }\n        }\n        thread::sleep(Duration::from_millis(100));\n    }\n\n    // ── Step 6: exactly ONE logical stall eviction, observed via an INDEPENDENT\n    //    DRAINING control-plane connection (the KIND_SESSIONS reap runs the 400 ms\n    //    stall-evict). Record t_release at first observation and freeze the resume\n    //    cursor for the sid row. ──\n    // POISON-BEATS-EVICT (F-039 rider, doyle-ruled 2026-07-10, 3rd CI trip): the\n    // ONE wedged controller's release can be tallied by the LOGICAL reap\n    // (stall_evict_controller), by the PHYSICAL bound (the conn watchdog poisons\n    // first and note_controller_write_retired records the same event class —\n    // the field-accepted v0.30.6 behavior), or by BOTH when the watchdog races\n    // the reap's blocked-window sample. So the tally can step 0→2 and an\n    // `== 1` break-condition misses it forever (the evicts_seen=0 flake). The\n    // loop breaks on ≥1; the assert below accepts {1, 2} — anything more is a\n    // real second eviction and still fails.\n    let mut stall_evicts_seen: u64 = 0;\n    let mut t_release: Option<Instant> = None;\n    let mut frozen_cursor: Option<u64> = None;\n    let step6_deadline = Instant::now() + Duration::from_secs(10);\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,null,196,null,199,null,202,null,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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,null,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,null,833]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\brain_decouple.rs"}}},"isError":false,"timestamp":1784195833921}}
{"type":"message","id":"996a8bf7","parentId":"bc897559","timestamp":"2026-07-16T09:57:13.923Z","message":{"role":"toolResult","toolCallId":"call_rVavpqB1FqxWCJZhAXQly51Y|fc_008390f508d1d09b016a58aaf9cb78819a8e1888ecc4e9f7c7","toolName":"read","content":[{"type":"text","text":"# UPDATE-WEDGE round-3 RIG VERDICT — net-runtime starvation seam FALSIFIED (todlando, 2026-07-09)\n\nReport-before-fix per `docs/UPDATE-WEDGE-2-ROUND3-RULING.md` §7. Rig lives in the\n`uwedge2` worktree: `crates/spt-daemon/tests/net_worker_starve.rs` + a throwaway\nDIAGNOSTIC probe on `crates/spt-daemon/src/nethost.rs` (`active_dial_tasks()`,\n`net_canary_age_ms()`, `dial_permits_available()` — no REQ, marked throwaway).\n\n## (a) Mechanism confirmed? — NO. FALSIFIED, robustly, across both dead-peer shapes.\n\nThe ruling's decider (§5): do dead-peer `submit_dial`s HOLD/serialize the broker\nnet runtime's 2 worker threads (starving the reactor that services LOCAL loopback\n`rc` I/O), or do the QUIC awaits YIELD? **They yield.**\n\nRig: dialer with a 3s QUIC-op bound; fire K=12 concurrent `submit_dial`s straight\nonto the broker's own `NetHost` (the exact pump path); read the reactor-liveness\ncanary (a runtime task stamping epoch-ms every 25ms — a stale stamp == workers\nmonopolized).\n\n| dead-peer shape | K in-flight | peak canary age | dial permits free |\n|---|---|---|---|\n| proof-hang (accepts QUIC handshake, black-holes the seed-proof — netbroker.rs dead peer) | 12 | **32 ms** | 12 |\n| unreachable (id-only `addr_for_node_hex`, no path — the field's \"peer unresponsive\" ecb39e40/cfd9fc55 shape) | 12 | **33 ms** | 12 |\n\n12 in-flight dead-peer dials (≫ the 2 workers) held the canary at **32–33 ms** vs\nthe **15 000 ms** stall-evict threshold — ~450× headroom. Both connect shapes\n(proof-read-hang AND relay-less/discovery-off unreachable-connect) yield cleanly;\n2 workers multiplex 12 dead dials without perturbation. The 24-permit dial\nsemaphore is never the limiter (12 free throughout). Locked as passing asserts so\na future non-yielding regression re-fires the investigation.\n\n## (b) Boundary map\n\n- **Loopback `rc` I/O** (`open_stream` attach @nethost.rs:1226, `send_stream`\n  controller-output @1301) is driven by `bounded_block_on` → `runtime.handle()\n  .block_on(fut)` on a **broker DISPATCH/reply thread** (NOT a net worker). The\n  future's `tokio::io::duplex` readiness is serviced by the 2-worker net reactor —\n  **PROVEN healthy under a 12-deep dead-peer dial burst** (canary fresh).\n- **Dead-peer connects** (`submit_dial` @nethost.rs:1036) run as `runtime.spawn`\n  tasks on the 2 workers, bounded by `quic_op_timeout`. They are cooperative\n  (yield at every await); 12 coexist with zero reactor impact.\n- **The field stall-evict** is *\"controller writer blocked >15000ms on a\n  suspended/black-holed **brain conn**\"*. The blocked writer is the broker → BRAIN\n  IPC path; it unblocks only when the **brain READS/drains** its side. That drain\n  is the brain main loop's job — it is **not** on the broker net runtime at all.\n\nSo the net runtime is EXONERATED as the wedge seam. The `submit-dial exceeded the\n10s bound` field lines are the *expected, benign* dead-peer timeout — not evidence\nof runtime saturation. The wedge coupling lives at the **brain main-loop drain**.\n\n## (c) Does shape C suffice? — MOOT. C's (and A's) premise is false.\n\nBoth ruled shapes target net-worker starvation by a dial burst:\n- **C** (brain bounds/staggers dead-peer dial *requests*) — there is no burst-driven\n  worker starvation to relieve; staggering dials changes nothing about the brain's\n  drain.\n- **A** (isolate loopback I/O onto its own runtime) — the loopback lane is already\n  proven un-starved by peer work; isolating it fixes a starvation that isn't there.\n\n**Do NOT mint `REQ-BRAIN-DIAL-BOUND-NO-STARVE` or `REQ-NET-LOOPBACK-NO-PEER-STARVE`.**\nNeither closes the field wedge.\n\n## Re-open — where the wedge actually is (HYPOTHESIS for doyle's next ruling)\n\nThe stall is the **brain main loop failing to DRAIN controller output sinks for\n>15s**. The net runtime is clean, so a peer op stalls the drain only if it runs ON\n/ blocks the brain's main (drain) loop. Two sub-candidates, both inside the\nv0.30.3 (counter-54) regression window the field pinned:\n\n1. **counter-54 reap-drive (ce053dc) — PRIME suspect for the STEADY-STATE\n   doyle-only wedge.** v0.30.3 added \"brain self-drives the controller reap\" in the\n   main loop (every ~500ms). If that drive does a blocking net/IPC op or contends a\n   lock with the peer path, it stalls the very drain it shares a thread with — with\n   NO respawn — which is exactly the never-before-v0.30.3 steady-state wedge.\n2. **Startup sequencing (respawn severity) — the round-2-deferred stagger.** The new\n   brain runs net-consumer setup + initial peer dials interleaved with entering its\n   drain loop; resumed controllers go undrained across the dead-peer-dial window →\n   stall-evict on respawn.\n\nNext rig (needs doyle's authorization — brain+broker+PTY stack, `brain_decouple`\ntemplate + a real pump round with a dead peer): assert whether (1) the counter-54\nreap-drive and/or (2) startup sequencing blocks the brain from draining an\nactively-streaming controller >15s. That is the real decider now. The pure-NetHost\nStage-1 rig has done its job: it removed the net runtime from suspicion.\n\n## Runner\n\n`cargo nextest run -p spt-daemon --test net_worker_starve` (or `--test-threads=1`).\nHermetic (relays disabled, no discovery, loopback QUIC); no SPT_HOME/PTY needed.\n"}],"details":{"displayContent":{"text":"# UPDATE-WEDGE round-3 RIG VERDICT — net-runtime starvation seam FALSIFIED (todlando, 2026-07-09)\r\n\r\nReport-before-fix per `docs/UPDATE-WEDGE-2-ROUND3-RULING.md` §7. Rig lives in the\r\n`uwedge2` worktree: `crates/spt-daemon/tests/net_worker_starve.rs` + a throwaway\r\nDIAGNOSTIC probe on `crates/spt-daemon/src/nethost.rs` (`active_dial_tasks()`,\r\n`net_canary_age_ms()`, `dial_permits_available()` — no REQ, marked throwaway).\r\n\r\n## (a) Mechanism confirmed? — NO. FALSIFIED, robustly, across both dead-peer shapes.\r\n\r\nThe ruling's decider (§5): do dead-peer `submit_dial`s HOLD/serialize the broker\r\nnet runtime's 2 worker threads (starving the reactor that services LOCAL loopback\r\n`rc` I/O), or do the QUIC awaits YIELD? **They yield.**\r\n\r\nRig: dialer with a 3s QUIC-op bound; fire K=12 concurrent `submit_dial`s straight\r\nonto the broker's own `NetHost` (the exact pump path); read the reactor-liveness\r\ncanary (a runtime task stamping epoch-ms every 25ms — a stale stamp == workers\r\nmonopolized).\r\n\r\n| dead-peer shape | K in-flight | peak canary age | dial permits free |\r\n|---|---|---|---|\r\n| proof-hang (accepts QUIC handshake, black-holes the seed-proof — netbroker.rs dead peer) | 12 | **32 ms** | 12 |\r\n| unreachable (id-only `addr_for_node_hex`, no path — the field's \"peer unresponsive\" ecb39e40/cfd9fc55 shape) | 12 | **33 ms** | 12 |\r\n\r\n12 in-flight dead-peer dials (≫ the 2 workers) held the canary at **32–33 ms** vs\r\nthe **15 000 ms** stall-evict threshold — ~450× headroom. Both connect shapes\r\n(proof-read-hang AND relay-less/discovery-off unreachable-connect) yield cleanly;\r\n2 workers multiplex 12 dead dials without perturbation. The 24-permit dial\r\nsemaphore is never the limiter (12 free throughout). Locked as passing asserts so\r\na future non-yielding regression re-fires the investigation.\r\n\r\n## (b) Boundary map\r\n\r\n- **Loopback `rc` I/O** (`open_stream` attach @nethost.rs:1226, `send_stream`\r\n  controller-output @1301) is driven by `bounded_block_on` → `runtime.handle()\r\n  .block_on(fut)` on a **broker DISPATCH/reply thread** (NOT a net worker). The\r\n  future's `tokio::io::duplex` readiness is serviced by the 2-worker net reactor —\r\n  **PROVEN healthy under a 12-deep dead-peer dial burst** (canary fresh).\r\n- **Dead-peer connects** (`submit_dial` @nethost.rs:1036) run as `runtime.spawn`\r\n  tasks on the 2 workers, bounded by `quic_op_timeout`. They are cooperative\r\n  (yield at every await); 12 coexist with zero reactor impact.\r\n- **The field stall-evict** is *\"controller writer blocked >15000ms on a\r\n  suspended/black-holed **brain conn**\"*. The blocked writer is the broker → BRAIN\r\n  IPC path; it unblocks only when the **brain READS/drains** its side. That drain\r\n  is the brain main loop's job — it is **not** on the broker net runtime at all.\r\n\r\nSo the net runtime is EXONERATED as the wedge seam. The `submit-dial exceeded the\r\n10s bound` field lines are the *expected, benign* dead-peer timeout — not evidence\r\nof runtime saturation. The wedge coupling lives at the **brain main-loop drain**.\r\n\r\n## (c) Does shape C suffice? — MOOT. C's (and A's) premise is false.\r\n\r\nBoth ruled shapes target net-worker starvation by a dial burst:\r\n- **C** (brain bounds/staggers dead-peer dial *requests*) — there is no burst-driven\r\n  worker starvation to relieve; staggering dials changes nothing about the brain's\r\n  drain.\r\n- **A** (isolate loopback I/O onto its own runtime) — the loopback lane is already\r\n  proven un-starved by peer work; isolating it fixes a starvation that isn't there.\r\n\r\n**Do NOT mint `REQ-BRAIN-DIAL-BOUND-NO-STARVE` or `REQ-NET-LOOPBACK-NO-PEER-STARVE`.**\r\nNeither closes the field wedge.\r\n\r\n## Re-open — where the wedge actually is (HYPOTHESIS for doyle's next ruling)\r\n\r\nThe stall is the **brain main loop failing to DRAIN controller output sinks for\r\n>15s**. The net runtime is clean, so a peer op stalls the drain only if it runs ON\r\n/ blocks the brain's main (drain) loop. Two sub-candidates, both inside the\r\nv0.30.3 (counter-54) regression window the field pinned:\r\n\r\n1. **counter-54 reap-drive (ce053dc) — PRIME suspect for the STEADY-STATE\r\n   doyle-only wedge.** v0.30.3 added \"brain self-drives the controller reap\" in the\r\n   main loop (every ~500ms). If that drive does a blocking net/IPC op or contends a\r\n   lock with the peer path, it stalls the very drain it shares a thread with — with\r\n   NO respawn — which is exactly the never-before-v0.30.3 steady-state wedge.\r\n2. **Startup sequencing (respawn severity) — the round-2-deferred stagger.** The new\r\n   brain runs net-consumer setup + initial peer dials interleaved with entering its\r\n   drain loop; resumed controllers go undrained across the dead-peer-dial window →\r\n   stall-evict on respawn.\r\n\r\nNext rig (needs doyle's authorization — brain+broker+PTY stack, `brain_decouple`\r\ntemplate + a real pump round with a dead peer): assert whether (1) the counter-54\r\nreap-drive and/or (2) startup sequencing blocks the brain from draining an\r\nactively-streaming controller >15s. That is the real decider now. The pure-NetHost\r\nStage-1 rig has done its job: it removed the net runtime from suspicion.\r\n\r\n## Runner\r\n\r\n`cargo nextest run -p spt-daemon --test net_worker_starve` (or `--test-threads=1`).\r\nHermetic (relays disabled, no discovery, loopback QUIC); no SPT_HOME/PTY needed.\r\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,86,87,88,89]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md"}}},"isError":false,"timestamp":1784195833922}}
{"type":"message","id":"cfede808","parentId":"996a8bf7","timestamp":"2026-07-16T09:57:13.923Z","message":{"role":"toolResult","toolCallId":"call_BFc23r7TOyMAelNgelV9Qp7D|fc_008390f508d1d09b016a58aaf9cb58819aa7e8eb88dc1701be","toolName":"read","content":[{"type":"text","text":"# UPDATE-WEDGE round-3 CODE-READ — reap-drive exonerated; the seam is a SharedSend self-deadlock on the brain conn (todlando, 2026-07-09)\n\nReport-before-fix per RULING 2 §5/§8. doyle asked: pin what the counter-54 reap-drive\n(ce053dc) blocks on each tick, especially whether an UNRESPONSIVE peer lengthens the tick\n>15s. Answer: **it doesn't block on anything — and the real wedge is a different seam the\nreap-drive only widened.**\n\n## 1. The reap-drive does NOT block (doyle's dead-peer-tick hypothesis — FALSIFIED by code)\n\n`run_brain`'s heartbeat loop (brainproc.rs:239-278) per tick: `thread::sleep(500ms)` →\n`brain.net_status()` → spawn-consumers-once → `brain.sessions()` (the counter-54 reap-drive,\nbrainproc.rs:276) → `write_ready`. It hosts NO PTY sessions, so it does not itself drain PTY\noutput. Neither IPC call blocks on a dead peer:\n\n- **`net_status()` is a cheap field read.** `dispatch_net_status` (broker.rs:3573) returns\n  `node_id_hex()` / `addr_json()` / `conn_count()` — no `bounded_block_on`, no peer wait.\n  `conn_count` takes `shared.conns.lock()` only briefly; a dead-peer connect never reaches\n  `register_conn`, so it never holds that lock.\n- **`sessions()` reap takes each OutputLog lock only briefly.** The `KIND_SESSIONS` handler\n  (broker.rs:2853-2926) snapshots each session under its OWN log lock, calls\n  `reap_dead_controller` (968), reads the cursor, releases; `converge_perch_stamps` runs\n  OFF-lock. No peer op anywhere.\n- **`append` is non-blocking under the log lock.** The producer fan-out (broker.rs:790-849)\n  is `try_send` to controller (827) and viewers (802) — never a blocking send — so it holds\n  the log lock only momentarily. The reap's `recover_log` never queues behind it.\n- **The controller writer holds the SOCKET mutex, never the log lock, across its blocking\n  write** (`controller_writer`, broker.rs:1418; docstring 1409-1411: *\"a backed-up controller\n  blocks ONLY this thread, never the session drain which used to write inline under the log\n  lock\"*). So a WEDGED writer cannot block the reap either.\n- **Viewers drop, never block the producer** (`try_send` + eviction, broker.rs:1086/802).\n\nConstants: `BRAIN_HEARTBEAT` = 500ms, `BRAIN_WRITE_DEADLINE` = 15s. Net: the reap-drive tick\nis ~ms even with dead peers present. There is NO code path by which an unresponsive peer\nlengthens it. **`REQ`-shaping the fix as \"make the reap-drive non-blocking\" targets a block\nthat does not exist.**\n\n## 2. The REAL seam — a SharedSend self-deadlock on the brain conn\n\n`SharedSend = Arc<Mutex<SendHalf>>` (broker.rs:78): ONE mutex guards each conn's send half.\nOn the brain conn, that one mutex is contended by TWO writer classes:\n\n- **Subscriber writers** — `viewer_writer` (broker.rs:1333/1342) and `controller_writer`\n  (1451) — take `send.lock()` and hold it ACROSS the blocking `write_frame` to the conn.\n- **The dispatch reply path** — `send_frame` (broker.rs:4221) — locks the SAME `send.lock()`\n  to write `KIND_SESSIONS_REPLY` (2926) / `KIND_NET_STATUS_REPLY` (3592).\n\nThe brain conn is BOTH a subscriber sink (resume re-subscribes N sessions onto it) AND the\nbrain's request/reply channel. So:\n\n> when the brain conn backs up (the brain isn't draining fast enough — it only reads during\n> the 500ms heartbeat's `net_status`/`sessions` calls), a subscriber writer BLOCKS inside\n> `write_frame` while HOLDING `send.lock()` → the dispatch thread cannot acquire `send.lock()`\n> to send the heartbeat reply → `net_status()`/`sessions()` never return → the heartbeat loop\n> is stuck → the brain never drains its conn → the subscriber writer stays blocked. **A\n> self-reinforcing deadlock on the brain conn's send mutex.**\n\nOnce the heartbeat is stuck, the brain drains nothing; every subscriber writer on the brain\nconn wedges; the broker-side controller writers whose sink is the brain conn cross\n`BRAIN_WRITE_DEADLINE` → `BRAIN_SUBSCRIBER_STALL_EVICT`. Matches the field exactly.\n\n### Why counter-54 is the regression window (but not the root)\nce053dc added a SECOND per-heartbeat reply round-trip (`brain.sessions()`) through the\ncontended brain-conn send mutex. It did not create the deadlock — it made the heartbeat\ndepend on TWO reply round-trips per tick surviving the mutex, widening the stall window and\ntightening the coupling. Regression-window-consistent; the underlying send-mutex sharing is\nolder.\n\n### Why the Viewer fix (v0.30.4) did NOT heal it\nThe round-2 Viewer fix removed the resume-STEAL (no more `become_controller`), but the brain\nis STILL a subscriber — `viewer_writer` has the SAME `send.lock()`-across-blocking-write\npattern (1333/1342) AND viewers have NO stall-evict safety valve (only controllers are\nreaped). So a blocked brain-viewer writer holds the send mutex indefinitely → the deadlock\npersists. This is why v0.30.4 field-verify wedged again.\n\n### The dead peer is NOT causal\nThe trigger is active-session OUTPUT backing up the brain conn faster than the 500ms\nheartbeat drains it — on respawn (resume subscribes N + replay burst floods the conn before\nthe loop drains) and in steady state (an active streamer's output between heartbeats fills the\nsocket buffer; on v0.30.3 additionally compounded by the steal). The field always had dead\npeers, but they are coincidental load, not the mechanism.\n\n## 3. Fix shape (proposed — doyle to rule) — BRAIN-SIDE, seamless\n\nThe daemon brain hosts NO PTY sessions (brainproc.rs:184), so it has NO consumer for the\nsubscriptions `resume_sessions` re-establishes — it floods its own request/reply conn with\noutput it never uses. Options, preference order:\n\n- **(A) resume_sessions does NOT re-subscribe.** Drop the undrained continuity subscription\n  entirely — the daemon brain has no consumer for it today. The brain conn then carries only\n  request/reply → no subscriber backpressure → no deadlock. Smallest, brain-side, seamless.\n  Forward-seam: when genuinely daemon-driven sessions land (the live-agent adapter), THOSE\n  attach on a drained carrier (option B).\n- **(B) Drain the brain conn on a DEDICATED reader thread** (`BrainConn::Split` — the exact\n  carrier the pump already uses, brain.rs:230) so the heartbeat reply is never blocked by\n  subscriber backpressure, and subscriptions can persist. Brain-side, seamless.\n- **(C) [broker-side — deployment risk, FLAG] subscriber writers use a bounded/non-blocking\n  write** instead of holding `send.lock()` across a blocking `write_frame`. Durable root but\n  broker-side ⇒ needs a coordinated daemon restart to land (the daemon.rs:368 constraint).\n\nPrefer **A or B** (brain-side ⇒ rides the brain-swap ⇒ seamless; the deployment risk lifts).\nBoth touch ONLY the brain (brain.rs / brainproc.rs). C touches the broker — flagged.\n\n## 4. Confirmer rig (RULING 2 §6, still authorized)\nThe mechanism is a code-proven deadlock, so the rig CONFIRMS + GUARDS rather than discovers.\nbrain+broker+PTY (`brain_decouple` template): N sessions actively producing output, the brain\nresume-subscribed onto its request/reply conn; RED-first — the heartbeat stalls and an\nactive-streaming controller is stall-evicted >15s WITHOUT a dead peer needing to be present\n(proving dead-peer-independence). Assert BOTH severities (respawn interleave + steady-state\noutput backup). Fix A/B must turn both green. Dead-peer arm optional (to show it is not the\ntrigger).\n\n## 5. Sequence\nReported. doyle to rule the shape (A vs B) → mint the REQ (candidate\n`REQ-BRAIN-CONN-NO-SUBSCRIBER-DEADLOCK` or similar) → build the confirmer rig RED-first → fix →\nboth severities green. No REQ / no fix until doyle rules.\n"}],"details":{"displayContent":{"text":"# UPDATE-WEDGE round-3 CODE-READ — reap-drive exonerated; the seam is a SharedSend self-deadlock on the brain conn (todlando, 2026-07-09)\r\n\r\nReport-before-fix per RULING 2 §5/§8. doyle asked: pin what the counter-54 reap-drive\r\n(ce053dc) blocks on each tick, especially whether an UNRESPONSIVE peer lengthens the tick\r\n>15s. Answer: **it doesn't block on anything — and the real wedge is a different seam the\r\nreap-drive only widened.**\r\n\r\n## 1. The reap-drive does NOT block (doyle's dead-peer-tick hypothesis — FALSIFIED by code)\r\n\r\n`run_brain`'s heartbeat loop (brainproc.rs:239-278) per tick: `thread::sleep(500ms)` →\r\n`brain.net_status()` → spawn-consumers-once → `brain.sessions()` (the counter-54 reap-drive,\r\nbrainproc.rs:276) → `write_ready`. It hosts NO PTY sessions, so it does not itself drain PTY\r\noutput. Neither IPC call blocks on a dead peer:\r\n\r\n- **`net_status()` is a cheap field read.** `dispatch_net_status` (broker.rs:3573) returns\r\n  `node_id_hex()` / `addr_json()` / `conn_count()` — no `bounded_block_on`, no peer wait.\r\n  `conn_count` takes `shared.conns.lock()` only briefly; a dead-peer connect never reaches\r\n  `register_conn`, so it never holds that lock.\r\n- **`sessions()` reap takes each OutputLog lock only briefly.** The `KIND_SESSIONS` handler\r\n  (broker.rs:2853-2926) snapshots each session under its OWN log lock, calls\r\n  `reap_dead_controller` (968), reads the cursor, releases; `converge_perch_stamps` runs\r\n  OFF-lock. No peer op anywhere.\r\n- **`append` is non-blocking under the log lock.** The producer fan-out (broker.rs:790-849)\r\n  is `try_send` to controller (827) and viewers (802) — never a blocking send — so it holds\r\n  the log lock only momentarily. The reap's `recover_log` never queues behind it.\r\n- **The controller writer holds the SOCKET mutex, never the log lock, across its blocking\r\n  write** (`controller_writer`, broker.rs:1418; docstring 1409-1411: *\"a backed-up controller\r\n  blocks ONLY this thread, never the session drain which used to write inline under the log\r\n  lock\"*). So a WEDGED writer cannot block the reap either.\r\n- **Viewers drop, never block the producer** (`try_send` + eviction, broker.rs:1086/802).\r\n\r\nConstants: `BRAIN_HEARTBEAT` = 500ms, `BRAIN_WRITE_DEADLINE` = 15s. Net: the reap-drive tick\r\nis ~ms even with dead peers present. There is NO code path by which an unresponsive peer\r\nlengthens it. **`REQ`-shaping the fix as \"make the reap-drive non-blocking\" targets a block\r\nthat does not exist.**\r\n\r\n## 2. The REAL seam — a SharedSend self-deadlock on the brain conn\r\n\r\n`SharedSend = Arc<Mutex<SendHalf>>` (broker.rs:78): ONE mutex guards each conn's send half.\r\nOn the brain conn, that one mutex is contended by TWO writer classes:\r\n\r\n- **Subscriber writers** — `viewer_writer` (broker.rs:1333/1342) and `controller_writer`\r\n  (1451) — take `send.lock()` and hold it ACROSS the blocking `write_frame` to the conn.\r\n- **The dispatch reply path** — `send_frame` (broker.rs:4221) — locks the SAME `send.lock()`\r\n  to write `KIND_SESSIONS_REPLY` (2926) / `KIND_NET_STATUS_REPLY` (3592).\r\n\r\nThe brain conn is BOTH a subscriber sink (resume re-subscribes N sessions onto it) AND the\r\nbrain's request/reply channel. So:\r\n\r\n> when the brain conn backs up (the brain isn't draining fast enough — it only reads during\r\n> the 500ms heartbeat's `net_status`/`sessions` calls), a subscriber writer BLOCKS inside\r\n> `write_frame` while HOLDING `send.lock()` → the dispatch thread cannot acquire `send.lock()`\r\n> to send the heartbeat reply → `net_status()`/`sessions()` never return → the heartbeat loop\r\n> is stuck → the brain never drains its conn → the subscriber writer stays blocked. **A\r\n> self-reinforcing deadlock on the brain conn's send mutex.**\r\n\r\nOnce the heartbeat is stuck, the brain drains nothing; every subscriber writer on the brain\r\nconn wedges; the broker-side controller writers whose sink is the brain conn cross\r\n`BRAIN_WRITE_DEADLINE` → `BRAIN_SUBSCRIBER_STALL_EVICT`. Matches the field exactly.\r\n\r\n### Why counter-54 is the regression window (but not the root)\r\nce053dc added a SECOND per-heartbeat reply round-trip (`brain.sessions()`) through the\r\ncontended brain-conn send mutex. It did not create the deadlock — it made the heartbeat\r\ndepend on TWO reply round-trips per tick surviving the mutex, widening the stall window and\r\ntightening the coupling. Regression-window-consistent; the underlying send-mutex sharing is\r\nolder.\r\n\r\n### Why the Viewer fix (v0.30.4) did NOT heal it\r\nThe round-2 Viewer fix removed the resume-STEAL (no more `become_controller`), but the brain\r\nis STILL a subscriber — `viewer_writer` has the SAME `send.lock()`-across-blocking-write\r\npattern (1333/1342) AND viewers have NO stall-evict safety valve (only controllers are\r\nreaped). So a blocked brain-viewer writer holds the send mutex indefinitely → the deadlock\r\npersists. This is why v0.30.4 field-verify wedged again.\r\n\r\n### The dead peer is NOT causal\r\nThe trigger is active-session OUTPUT backing up the brain conn faster than the 500ms\r\nheartbeat drains it — on respawn (resume subscribes N + replay burst floods the conn before\r\nthe loop drains) and in steady state (an active streamer's output between heartbeats fills the\r\nsocket buffer; on v0.30.3 additionally compounded by the steal). The field always had dead\r\npeers, but they are coincidental load, not the mechanism.\r\n\r\n## 3. Fix shape (proposed — doyle to rule) — BRAIN-SIDE, seamless\r\n\r\nThe daemon brain hosts NO PTY sessions (brainproc.rs:184), so it has NO consumer for the\r\nsubscriptions `resume_sessions` re-establishes — it floods its own request/reply conn with\r\noutput it never uses. Options, preference order:\r\n\r\n- **(A) resume_sessions does NOT re-subscribe.** Drop the undrained continuity subscription\r\n  entirely — the daemon brain has no consumer for it today. The brain conn then carries only\r\n  request/reply → no subscriber backpressure → no deadlock. Smallest, brain-side, seamless.\r\n  Forward-seam: when genuinely daemon-driven sessions land (the live-agent adapter), THOSE\r\n  attach on a drained carrier (option B).\r\n- **(B) Drain the brain conn on a DEDICATED reader thread** (`BrainConn::Split` — the exact\r\n  carrier the pump already uses, brain.rs:230) so the heartbeat reply is never blocked by\r\n  subscriber backpressure, and subscriptions can persist. Brain-side, seamless.\r\n- **(C) [broker-side — deployment risk, FLAG] subscriber writers use a bounded/non-blocking\r\n  write** instead of holding `send.lock()` across a blocking `write_frame`. Durable root but\r\n  broker-side ⇒ needs a coordinated daemon restart to land (the daemon.rs:368 constraint).\r\n\r\nPrefer **A or B** (brain-side ⇒ rides the brain-swap ⇒ seamless; the deployment risk lifts).\r\nBoth touch ONLY the brain (brain.rs / brainproc.rs). C touches the broker — flagged.\r\n\r\n## 4. Confirmer rig (RULING 2 §6, still authorized)\r\nThe mechanism is a code-proven deadlock, so the rig CONFIRMS + GUARDS rather than discovers.\r\nbrain+broker+PTY (`brain_decouple` template): N sessions actively producing output, the brain\r\nresume-subscribed onto its request/reply conn; RED-first — the heartbeat stalls and an\r\nactive-streaming controller is stall-evicted >15s WITHOUT a dead peer needing to be present\r\n(proving dead-peer-independence). Assert BOTH severities (respawn interleave + steady-state\r\noutput backup). Fix A/B must turn both green. Dead-peer arm optional (to show it is not the\r\ntrigger).\r\n\r\n## 5. Sequence\r\nReported. doyle to rule the shape (A vs B) → mint the REQ (candidate\r\n`REQ-BRAIN-CONN-NO-SUBSCRIBER-DEADLOCK` or similar) → build the confirmer rig RED-first → fix →\r\nboth severities green. No REQ / no fix until doyle rules.\r\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,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\UPDATE-WEDGE-2-ROUND3-CODEREAD.md"}}},"isError":false,"timestamp":1784195833922}}
{"type":"message","id":"d406a0b6","parentId":"cfede808","timestamp":"2026-07-16T09:57:13.923Z","message":{"role":"toolResult","toolCallId":"call_rBdt3GQKTh3bYgo4VashfB9E|fc_008390f508d1d09b016a58aaf9cb88819a836d30d248a658a9","toolName":"read","content":[{"type":"text","text":"//! THE-FORKENING W4 — `spt daemon refresh` on a LIVE daemon with a hosted\n//! endpoint (REQ-DAEMON-REFRESH): the brain generation cycles (new brain pid,\n//! generation bumped) with NO binary swap, while the broker process, the hosted\n//! harness session, and the endpoint's ONLINE perch all survive untouched — the\n//! apply_staged brain-cycle path minus the swap, driven through the REAL CLI\n//! verb. The recovery story: wedged brain-held state no longer needs a full\n//! daemon bounce that kills every PTY.\n//!\n//! Real `spt daemon run` broker + supervised brain + a REAL dummy-harness\n//! `[session.self]` session (the live_adapt_translation_swap_e2e rig, minus the\n//! translation child). Bounded everywhere; every spawned pid is reaped SCOPED\n//! at test end.\n//\n// [int->REQ-DAEMON-REFRESH]\n\nuse std::path::{Path, PathBuf};\nuse std::process::{Child, Command, Output, Stdio};\nuse std::time::{Duration, Instant};\n\nmod common;\nuse common::CommandNoWindowExt;\n\nuse spt_store::perch::{self, ParentHint};\n\n/// Scoped pid-tree kill (cleanup) — never machine-wide.\nfn kill_pid(pid: u32) {\n    #[cfg(windows)]\n    let _ = Command::new(\"taskkill\")\n        .no_window()\n        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n        .output();\n    #[cfg(unix)]\n    let _ = Command::new(\"kill\").args([\"-9\", &pid.to_string()]).output();\n}\n\n/// `Command::output()` with a deadline, off-thread — bounds a hang into a failure.\nfn output_bounded(mut cmd: Command, deadline: Duration) -> Output {\n    let (tx, rx) = std::sync::mpsc::channel();\n    std::thread::spawn(move || {\n        let _ = tx.send(cmd.output());\n    });\n    rx.recv_timeout(deadline)\n        .expect(\"captured spt call must complete within its deadline\")\n        .expect(\"run spt\")\n}\n\n/// `(pid, generation)` out of `brain.ready`; `None` until it parses.\nfn read_ready(path: &Path) -> Option<(u32, u64)> {\n    let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).ok()?).ok()?;\n    Some((\n        v.get(\"pid\")?.as_u64()? as u32,\n        v.get(\"generation\")?.as_u64()?,\n    ))\n}\n\n/// Poll `brain.ready` until it holds a pid different from `was`, up to `budget`.\nfn wait_ready_not(path: &Path, was: Option<u32>, budget: Duration) -> Option<(u32, u64)> {\n    let deadline = Instant::now() + budget;\n    while Instant::now() < deadline {\n        if let Some((pid, generation)) = read_ready(path) {\n            if was != Some(pid) {\n                return Some((pid, generation));\n            }\n        }\n        std::thread::sleep(Duration::from_millis(50));\n    }\n    None\n}\n\nfn sibling_bin(name: &str) -> PathBuf {\n    PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"))\n        .with_file_name(format!(\"{name}{}\", std::env::consts::EXE_SUFFIX))\n}\n\n#[test]\nfn daemon_refresh_cycles_brain_while_hosted_endpoint_survives() {\n    let now_ms = || {\n        std::time::SystemTime::now()\n            .duration_since(std::time::UNIX_EPOCH)\n            .map(|d| d.as_millis() as u64)\n            .unwrap_or(0)\n    };\n    let _ = now_ms; // rig parity; the CLI verb needs no in-proc Brain here.\n\n    // ── (1) Isolated, NET-LESS SPT_HOME. ──\n    let home = tempfile::tempdir().unwrap();\n    std::env::set_var(\"SPT_HOME\", home.path());\n    let identity_dir = home.path().join(\"identity\");\n    std::fs::create_dir_all(&identity_dir).unwrap();\n    std::fs::write(identity_dir.join(\"node.key\"), \"not-a-valid-seed\").unwrap();\n\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n    let exe_suffix = spt_bin\n        .extension()\n        .map(|e| format!(\".{}\", e.to_string_lossy()))\n        .unwrap_or_default();\n    let mock_session = sibling_bin(\"mock-session\");\n    assert!(\n        mock_session.exists(),\n        \"the dummy-harness program must be built: {}\",\n        mock_session.display()\n    );\n\n    // ── (2) Register adapter `cc` with a long-lived dummy-harness session\n    //    (the live_adapt rig's manifest, minus the translation binary). ──\n    let install_dir = perch::spt_home().join(\"srcs\").join(\"cc\");\n    std::fs::create_dir_all(&install_dir).unwrap();\n    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\\\', \"/\");\n    let mock_for_cmd = mock_session.to_string_lossy().replace('\\\\', \"/\");\n    let self_cmd = format!(\n        \"{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \\\n         --spt {spt_for_cmd} --mock-adapter cc --type live_agent --heartbeat-ms 200\"\n    );\n    let psyche_bin = install_dir.join(format!(\"psychebin{exe_suffix}\"));\n    std::fs::copy(&spt_bin, &psyche_bin).expect(\"stage the psyche binary fixture\");\n    let drops = home.path().join(\"drops\");\n    std::fs::create_dir_all(&drops).unwrap();\n    let dir = drops.to_string_lossy().replace('\\\\', \"/\");\n    let manifest_toml = format!(\n        \"[adapter]\\nname=\\\"cc\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\n         min_spt_core_version=\\\"0\\\"\\n\\n[session.self]\\ncommand='{self_cmd}'\\n\\n\\\n         [session]\\ncommune_dir=\\\"{dir}\\\"\\nsignoff_dir=\\\"{dir}\\\"\\n\\n\\\n         [session.psyche_init]\\ncommand='psychebin ready {{id}}'\\ncwd=\\\"{{psyche_dir}}\\\"\\nkeys=[]\\n\"\n    );\n    std::fs::write(install_dir.join(\"manifest.toml\"), &manifest_toml).unwrap();\n    spt_runtime::registry::register(&perch::adapters_dir(), &install_dir, 1000).unwrap();\n\n    // ── (3) The REAL daemon broker process. ──\n    let brain_log = home.path().join(\"brain.stderr.log\");\n    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n    let mut broker: Child = Command::new(&spt_bin)\n        .no_window()\n        .args([\"daemon\", \"run\"])\n        .env(\"SPT_HOME\", home.path())\n        .stdout(Stdio::null())\n        .stderr(Stdio::from(brain_log_file))\n        .spawn()\n        .expect(\"spawn spt daemon run (broker process)\");\n    let ready_path = home.path().join(\"brain.ready\");\n    let (brain_pid_before, gen_before) =\n        match wait_ready_not(&ready_path, None, Duration::from_secs(30)) {\n            Some(r) => r,\n            None => {\n                let _ = broker.kill();\n                let _ = broker.wait();\n                panic!(\n                    \"PRECONDITION: brain never came up.\\n{}\",\n                    std::fs::read_to_string(&brain_log).unwrap_or_default()\n                );\n            }\n        };\n    let broker_pid = broker.id();\n\n    // ── (4) Host a real endpoint: `endpoint run --adapter cc --id ccX --start`,\n    //    wait for its perch ONLINE. ──\n    let id = \"ccX\";\n    let run = {\n        let mut cmd = Command::new(&spt_bin);\n        cmd.no_window()\n            .args([\"endpoint\", \"run\", \"--adapter\", \"cc\", \"--id\", id, \"--start\"])\n            .env(\"SPT_HOME\", home.path());\n        output_bounded(cmd, Duration::from_secs(45))\n    };\n    let run_stderr = String::from_utf8_lossy(&run.stderr).to_string();\n    let harness_pid: Option<u32> = run_stderr\n        .split(\"pid=\")\n        .nth(1)\n        .and_then(|s| s.split_whitespace().next())\n        .and_then(|s| s.trim().parse().ok());\n\n    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);\n    let mut online = false;\n    let deadline = Instant::now() + Duration::from_secs(25);\n    while Instant::now() < deadline {\n        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {\n            online = true;\n            break;\n        }\n        std::thread::sleep(Duration::from_millis(150));\n    }\n\n    // ── (5) THE VERB: `spt daemon refresh` — the production CLI path. ──\n    let refresh = {\n        let mut cmd = Command::new(&spt_bin);\n        cmd.no_window()\n            .args([\"daemon\", \"refresh\"])\n            .env(\"SPT_HOME\", home.path());\n        output_bounded(cmd, Duration::from_secs(30))\n    };\n    let refresh_stdout = String::from_utf8_lossy(&refresh.stdout).to_string();\n    let refresh_stderr = String::from_utf8_lossy(&refresh.stderr).to_string();\n\n    // ── (6) The brain must CYCLE: a new pid stamps brain.ready with a bumped\n    //    generation (no binary swapped anywhere). ──\n    let cycled = wait_ready_not(&ready_path, Some(brain_pid_before), Duration::from_secs(30));\n\n    // The broker process must be the SAME OS process (never restarted).\n    let broker_still_running = broker.try_wait().map(|st| st.is_none()).unwrap_or(false);\n\n    // The hosted endpoint survives: harness alive + perch back/still ONLINE.\n    let harness_alive_after = harness_pid\n        .map(spt_store::proc::is_process_alive)\n        .unwrap_or(false);\n    let mut still_online = false;\n    let online_deadline = Instant::now() + Duration::from_secs(10);\n    while Instant::now() < online_deadline {\n        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {\n            still_online = true;\n            break;\n        }\n        std::thread::sleep(Duration::from_millis(150));\n    }\n\n    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();\n    eprintln!(\n        \"=== W4 daemon refresh: run_ok={} online={online} harness_pid={harness_pid:?} \\\n         refresh_ok={} brain_before={brain_pid_before}/g{gen_before} cycled={cycled:?} \\\n         broker_alive={broker_still_running} harness_alive_after={harness_alive_after} \\\n         still_online={still_online} ===\\n--- refresh stdout ---\\n{refresh_stdout}\\n\\\n         --- refresh stderr ---\\n{refresh_stderr}\\n=== endpoint run stderr ===\\n{run_stderr}\\n\\\n         === brain stderr ===\\n{brain_stderr}\",\n        run.status.success(),\n        refresh.status.success(),\n    );\n\n    // ── (7) Reap the ENTIRE tree SCOPED before asserting. ──\n    if let Some(p) = harness_pid {\n        kill_pid(p);\n    }\n    let psyche_perch = perch::resolve_perch_path(&format!(\"{id}-psyche\"), ParentHint::Explicit(id));\n    if let Some(p) = spt_store::info::read_pid(&psyche_perch) {\n        kill_pid(p);\n    }\n    let _ = {\n        let mut cmd = Command::new(&spt_bin);\n        cmd.no_window()\n            .args([\"daemon\", \"stop\", \"--force\"])\n            .env(\"SPT_HOME\", home.path());\n        output_bounded(cmd, Duration::from_secs(20))\n    };\n    if let Some((pid, _)) = cycled {\n        kill_pid(pid);\n    }\n    kill_pid(brain_pid_before);\n    let _ = broker.kill();\n    let _ = broker.wait();\n\n    // ── ASSERTIONS ──\n    assert!(\n        run.status.success(),\n        \"endpoint run --start must succeed: {run_stderr}\"\n    );\n    assert!(\n        online,\n        \"PRECONDITION: the cc endpoint must bind ONLINE before the refresh.\\n\\\n         === brain stderr ===\\n{brain_stderr}\"\n    );\n    assert!(\n        refresh.status.success(),\n        \"REQ-DAEMON-REFRESH: `spt daemon refresh` must exit 0 against a live daemon \\\n         (stdout: {refresh_stdout}; stderr: {refresh_stderr}).\\n\\\n         === brain stderr ===\\n{brain_stderr}\"\n    );\n    let (brain_pid_after, gen_after) = cycled.unwrap_or_else(|| {\n        panic!(\n            \"REQ-DAEMON-REFRESH: the brain must CYCLE (a new pid must stamp brain.ready) \\\n             after refresh — pid stayed {brain_pid_before}.\\n=== brain stderr ===\\n{brain_stderr}\"\n        )\n    });\n    assert_ne!(\n        brain_pid_after, brain_pid_before,\n        \"a fresh brain process must own the ready stamp\"\n    );\n    assert!(\n        gen_after > gen_before,\n        \"REQ-DAEMON-REFRESH: the brain GENERATION must advance across a refresh \\\n         (before g{gen_before}, after g{gen_after}) — the supervisor cycle, not a re-stamp.\\n\\\n         === brain stderr ===\\n{brain_stderr}\"\n    );\n    assert!(\n        broker_still_running,\n        \"REQ-DAEMON-REFRESH: the broker process must survive the refresh untouched \\\n         (pid {broker_pid}).\\n=== brain stderr ===\\n{brain_stderr}\"\n    );\n    assert!(\n        harness_alive_after,\n        \"REQ-DAEMON-REFRESH: the hosted harness session must STILL be ALIVE across the \\\n         refresh — only the brain cycles, never the hosted PTYs. harness_pid={harness_pid:?}.\\n\\\n         === brain stderr ===\\n{brain_stderr}\"\n    );\n    assert!(\n        still_online,\n        \"REQ-DAEMON-REFRESH: the endpoint perch must remain ONLINE across the refresh \\\n         (the session was never torn down).\\n=== brain stderr ===\\n{brain_stderr}\"\n    );\n}\n"}],"details":{"displayContent":{"text":"//! THE-FORKENING W4 — `spt daemon refresh` on a LIVE daemon with a hosted\r\n//! endpoint (REQ-DAEMON-REFRESH): the brain generation cycles (new brain pid,\r\n//! generation bumped) with NO binary swap, while the broker process, the hosted\r\n//! harness session, and the endpoint's ONLINE perch all survive untouched — the\r\n//! apply_staged brain-cycle path minus the swap, driven through the REAL CLI\r\n//! verb. The recovery story: wedged brain-held state no longer needs a full\r\n//! daemon bounce that kills every PTY.\r\n//!\r\n//! Real `spt daemon run` broker + supervised brain + a REAL dummy-harness\r\n//! `[session.self]` session (the live_adapt_translation_swap_e2e rig, minus the\r\n//! translation child). Bounded everywhere; every spawned pid is reaped SCOPED\r\n//! at test end.\r\n//\r\n// [int->REQ-DAEMON-REFRESH]\r\n\r\nuse std::path::{Path, PathBuf};\r\nuse std::process::{Child, Command, Output, Stdio};\r\nuse std::time::{Duration, Instant};\r\n\r\nmod common;\r\nuse common::CommandNoWindowExt;\r\n\r\nuse spt_store::perch::{self, ParentHint};\r\n\r\n/// Scoped pid-tree kill (cleanup) — never machine-wide.\r\nfn kill_pid(pid: u32) {\r\n    #[cfg(windows)]\r\n    let _ = Command::new(\"taskkill\")\r\n        .no_window()\r\n        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\r\n        .output();\r\n    #[cfg(unix)]\r\n    let _ = Command::new(\"kill\").args([\"-9\", &pid.to_string()]).output();\r\n}\r\n\r\n/// `Command::output()` with a deadline, off-thread — bounds a hang into a failure.\r\nfn output_bounded(mut cmd: Command, deadline: Duration) -> Output {\r\n    let (tx, rx) = std::sync::mpsc::channel();\r\n    std::thread::spawn(move || {\r\n        let _ = tx.send(cmd.output());\r\n    });\r\n    rx.recv_timeout(deadline)\r\n        .expect(\"captured spt call must complete within its deadline\")\r\n        .expect(\"run spt\")\r\n}\r\n\r\n/// `(pid, generation)` out of `brain.ready`; `None` until it parses.\r\nfn read_ready(path: &Path) -> Option<(u32, u64)> {\r\n    let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).ok()?).ok()?;\r\n    Some((\r\n        v.get(\"pid\")?.as_u64()? as u32,\r\n        v.get(\"generation\")?.as_u64()?,\r\n    ))\r\n}\r\n\r\n/// Poll `brain.ready` until it holds a pid different from `was`, up to `budget`.\r\nfn wait_ready_not(path: &Path, was: Option<u32>, budget: Duration) -> Option<(u32, u64)> {\r\n    let deadline = Instant::now() + budget;\r\n    while Instant::now() < deadline {\r\n        if let Some((pid, generation)) = read_ready(path) {\r\n            if was != Some(pid) {\r\n                return Some((pid, generation));\r\n            }\r\n        }\r\n        std::thread::sleep(Duration::from_millis(50));\r\n    }\r\n    None\r\n}\r\n\r\nfn sibling_bin(name: &str) -> PathBuf {\r\n    PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"))\r\n        .with_file_name(format!(\"{name}{}\", std::env::consts::EXE_SUFFIX))\r\n}\r\n\r\n#[test]\r\nfn daemon_refresh_cycles_brain_while_hosted_endpoint_survives() {\r\n    let now_ms = || {\r\n        std::time::SystemTime::now()\r\n            .duration_since(std::time::UNIX_EPOCH)\r\n            .map(|d| d.as_millis() as u64)\r\n            .unwrap_or(0)\r\n    };\r\n    let _ = now_ms; // rig parity; the CLI verb needs no in-proc Brain here.\r\n\r\n    // ── (1) Isolated, NET-LESS SPT_HOME. ──\r\n    let home = tempfile::tempdir().unwrap();\r\n    std::env::set_var(\"SPT_HOME\", home.path());\r\n    let identity_dir = home.path().join(\"identity\");\r\n    std::fs::create_dir_all(&identity_dir).unwrap();\r\n    std::fs::write(identity_dir.join(\"node.key\"), \"not-a-valid-seed\").unwrap();\r\n\r\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\r\n    let exe_suffix = spt_bin\r\n        .extension()\r\n        .map(|e| format!(\".{}\", e.to_string_lossy()))\r\n        .unwrap_or_default();\r\n    let mock_session = sibling_bin(\"mock-session\");\r\n    assert!(\r\n        mock_session.exists(),\r\n        \"the dummy-harness program must be built: {}\",\r\n        mock_session.display()\r\n    );\r\n\r\n    // ── (2) Register adapter `cc` with a long-lived dummy-harness session\r\n    //    (the live_adapt rig's manifest, minus the translation binary). ──\r\n    let install_dir = perch::spt_home().join(\"srcs\").join(\"cc\");\r\n    std::fs::create_dir_all(&install_dir).unwrap();\r\n    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\\\', \"/\");\r\n    let mock_for_cmd = mock_session.to_string_lossy().replace('\\\\', \"/\");\r\n    let self_cmd = format!(\r\n        \"{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \\\r\n         --spt {spt_for_cmd} --mock-adapter cc --type live_agent --heartbeat-ms 200\"\r\n    );\r\n    let psyche_bin = install_dir.join(format!(\"psychebin{exe_suffix}\"));\r\n    std::fs::copy(&spt_bin, &psyche_bin).expect(\"stage the psyche binary fixture\");\r\n    let drops = home.path().join(\"drops\");\r\n    std::fs::create_dir_all(&drops).unwrap();\r\n    let dir = drops.to_string_lossy().replace('\\\\', \"/\");\r\n    let manifest_toml = format!(\r\n        \"[adapter]\\nname=\\\"cc\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\r\n         min_spt_core_version=\\\"0\\\"\\n\\n[session.self]\\ncommand='{self_cmd}'\\n\\n\\\r\n         [session]\\ncommune_dir=\\\"{dir}\\\"\\nsignoff_dir=\\\"{dir}\\\"\\n\\n\\\r\n         [session.psyche_init]\\ncommand='psychebin ready {{id}}'\\ncwd=\\\"{{psyche_dir}}\\\"\\nkeys=[]\\n\"\r\n    );\r\n    std::fs::write(install_dir.join(\"manifest.toml\"), &manifest_toml).unwrap();\r\n    spt_runtime::registry::register(&perch::adapters_dir(), &install_dir, 1000).unwrap();\r\n\r\n    // ── (3) The REAL daemon broker process. ──\r\n    let brain_log = home.path().join(\"brain.stderr.log\");\r\n    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\r\n    let mut broker: Child = Command::new(&spt_bin)\r\n        .no_window()\r\n        .args([\"daemon\", \"run\"])\r\n        .env(\"SPT_HOME\", home.path())\r\n        .stdout(Stdio::null())\r\n        .stderr(Stdio::from(brain_log_file))\r\n        .spawn()\r\n        .expect(\"spawn spt daemon run (broker process)\");\r\n    let ready_path = home.path().join(\"brain.ready\");\r\n    let (brain_pid_before, gen_before) =\r\n        match wait_ready_not(&ready_path, None, Duration::from_secs(30)) {\r\n            Some(r) => r,\r\n            None => {\r\n                let _ = broker.kill();\r\n                let _ = broker.wait();\r\n                panic!(\r\n                    \"PRECONDITION: brain never came up.\\n{}\",\r\n                    std::fs::read_to_string(&brain_log).unwrap_or_default()\r\n                );\r\n            }\r\n        };\r\n    let broker_pid = broker.id();\r\n\r\n    // ── (4) Host a real endpoint: `endpoint run --adapter cc --id ccX --start`,\r\n    //    wait for its perch ONLINE. ──\r\n    let id = \"ccX\";\r\n    let run = {\r\n        let mut cmd = Command::new(&spt_bin);\r\n        cmd.no_window()\r\n            .args([\"endpoint\", \"run\", \"--adapter\", \"cc\", \"--id\", id, \"--start\"])\r\n            .env(\"SPT_HOME\", home.path());\r\n        output_bounded(cmd, Duration::from_secs(45))\r\n    };\r\n    let run_stderr = String::from_utf8_lossy(&run.stderr).to_string();\r\n    let harness_pid: Option<u32> = run_stderr\r\n        .split(\"pid=\")\r\n        .nth(1)\r\n        .and_then(|s| s.split_whitespace().next())\r\n        .and_then(|s| s.trim().parse().ok());\r\n\r\n    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\r\n    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);\r\n    let mut online = false;\r\n    let deadline = Instant::now() + Duration::from_secs(25);\r\n    while Instant::now() < deadline {\r\n        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {\r\n            online = true;\r\n            break;\r\n        }\r\n        std::thread::sleep(Duration::from_millis(150));\r\n    }\r\n\r\n    // ── (5) THE VERB: `spt daemon refresh` — the production CLI path. ──\r\n    let refresh = {\r\n        let mut cmd = Command::new(&spt_bin);\r\n        cmd.no_window()\r\n            .args([\"daemon\", \"refresh\"])\r\n            .env(\"SPT_HOME\", home.path());\r\n        output_bounded(cmd, Duration::from_secs(30))\r\n    };\r\n    let refresh_stdout = String::from_utf8_lossy(&refresh.stdout).to_string();\r\n    let refresh_stderr = String::from_utf8_lossy(&refresh.stderr).to_string();\r\n\r\n    // ── (6) The brain must CYCLE: a new pid stamps brain.ready with a bumped\r\n    //    generation (no binary swapped anywhere). ──\r\n    let cycled = wait_ready_not(&ready_path, Some(brain_pid_before), Duration::from_secs(30));\r\n\r\n    // The broker process must be the SAME OS process (never restarted).\r\n    let broker_still_running = broker.try_wait().map(|st| st.is_none()).unwrap_or(false);\r\n\r\n    // The hosted endpoint survives: harness alive + perch back/still ONLINE.\r\n    let harness_alive_after = harness_pid\r\n        .map(spt_store::proc::is_process_alive)\r\n        .unwrap_or(false);\r\n    let mut still_online = false;\r\n    let online_deadline = Instant::now() + Duration::from_secs(10);\r\n    while Instant::now() < online_deadline {\r\n        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {\r\n            still_online = true;\r\n            break;\r\n        }\r\n        std::thread::sleep(Duration::from_millis(150));\r\n    }\r\n\r\n    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();\r\n    eprintln!(\r\n        \"=== W4 daemon refresh: run_ok={} online={online} harness_pid={harness_pid:?} \\\r\n         refresh_ok={} brain_before={brain_pid_before}/g{gen_before} cycled={cycled:?} \\\r\n         broker_alive={broker_still_running} harness_alive_after={harness_alive_after} \\\r\n         still_online={still_online} ===\\n--- refresh stdout ---\\n{refresh_stdout}\\n\\\r\n         --- refresh stderr ---\\n{refresh_stderr}\\n=== endpoint run stderr ===\\n{run_stderr}\\n\\\r\n         === brain stderr ===\\n{brain_stderr}\",\r\n        run.status.success(),\r\n        refresh.status.success(),\r\n    );\r\n\r\n    // ── (7) Reap the ENTIRE tree SCOPED before asserting. ──\r\n    if let Some(p) = harness_pid {\r\n        kill_pid(p);\r\n    }\r\n    let psyche_perch = perch::resolve_perch_path(&format!(\"{id}-psyche\"), ParentHint::Explicit(id));\r\n    if let Some(p) = spt_store::info::read_pid(&psyche_perch) {\r\n        kill_pid(p);\r\n    }\r\n    let _ = {\r\n        let mut cmd = Command::new(&spt_bin);\r\n        cmd.no_window()\r\n            .args([\"daemon\", \"stop\", \"--force\"])\r\n            .env(\"SPT_HOME\", home.path());\r\n        output_bounded(cmd, Duration::from_secs(20))\r\n    };\r\n    if let Some((pid, _)) = cycled {\r\n        kill_pid(pid);\r\n    }\r\n    kill_pid(brain_pid_before);\r\n    let _ = broker.kill();\r\n    let _ = broker.wait();\r\n\r\n    // ── ASSERTIONS ──\r\n    assert!(\r\n        run.status.success(),\r\n        \"endpoint run --start must succeed: {run_stderr}\"\r\n    );\r\n    assert!(\r\n        online,\r\n        \"PRECONDITION: the cc endpoint must bind ONLINE before the refresh.\\n\\\r\n         === brain stderr ===\\n{brain_stderr}\"\r\n    );\r\n    assert!(\r\n        refresh.status.success(),\r\n        \"REQ-DAEMON-REFRESH: `spt daemon refresh` must exit 0 against a live daemon \\\r\n         (stdout: {refresh_stdout}; stderr: {refresh_stderr}).\\n\\\r\n         === brain stderr ===\\n{brain_stderr}\"\r\n    );\r\n    let (brain_pid_after, gen_after) = cycled.unwrap_or_else(|| {\r\n        panic!(\r\n            \"REQ-DAEMON-REFRESH: the brain must CYCLE (a new pid must stamp brain.ready) \\\r\n             after refresh — pid stayed {brain_pid_before}.\\n=== brain stderr ===\\n{brain_stderr}\"\r\n        )\r\n    });\r\n    assert_ne!(\r\n        brain_pid_after, brain_pid_before,\r\n        \"a fresh brain process must own the ready stamp\"\r\n    );\r\n    assert!(\r\n        gen_after > gen_before,\r\n        \"REQ-DAEMON-REFRESH: the brain GENERATION must advance across a refresh \\\r\n         (before g{gen_before}, after g{gen_after}) — the supervisor cycle, not a re-stamp.\\n\\\r\n         === brain stderr ===\\n{brain_stderr}\"\r\n    );\r\n    assert!(\r\n        broker_still_running,\r\n        \"REQ-DAEMON-REFRESH: the broker process must survive the refresh untouched \\\r\n         (pid {broker_pid}).\\n=== brain stderr ===\\n{brain_stderr}\"\r\n    );\r\n    assert!(\r\n        harness_alive_after,\r\n        \"REQ-DAEMON-REFRESH: the hosted harness session must STILL be ALIVE across the \\\r\n         refresh — only the brain cycles, never the hosted PTYs. harness_pid={harness_pid:?}.\\n\\\r\n         === brain stderr ===\\n{brain_stderr}\"\r\n    );\r\n    assert!(\r\n        still_online,\r\n        \"REQ-DAEMON-REFRESH: the endpoint perch must remain ONLINE across the refresh \\\r\n         (the session was never torn down).\\n=== brain stderr ===\\n{brain_stderr}\"\r\n    );\r\n}\r\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,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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\daemon_refresh_e2e.rs"}}},"isError":false,"timestamp":1784195833923}}
{"type":"message","id":"ceb3f688","parentId":"d406a0b6","timestamp":"2026-07-16T09:57:13.925Z","message":{"role":"toolResult","toolCallId":"call_vao3ExBNo7JiwTDhf4qgebRQ|fc_008390f508d1d09b016a58aaf9cb90819a97cd3a9a4e5b82c2","toolName":"read","content":[{"type":"text","text":"/// proof was invalidated — it failed REAL on kitsubito Linux CI (\"worst completed 10\").\n/// doyle ruling `docs/UPDATE-WEDGE-R4-STALE-GUARD-RULING.md` (2026-07-10). The two\n/// cursor-only heartbeat tests above remain the POSITIVE liveness proof; THIS is the\n/// deterministic, timing-independent STRUCTURAL proof that the shipping resume attaches\n/// ZERO subscribers.\n///\n/// Two settled arms, no timing window:\n///   POSITIVE — the shipping daemon path (`resume_session_cursors`) must leave EVERY\n///   hosted session with ZERO viewers. A resume that instead subscribed the undrained\n///   heartbeat conn re-opens the round-3 SharedSend self-deadlock class; post-r4 it\n///   would instead get that heartbeat conn poisoned/retired by the write watchdog —\n///   still a broken resume, just a different failure. Cursor-only must attach nothing.\n///   SEAM-SENSITIVITY (keeps the proof NON-VACUOUS) — a SECOND brain on the SAME broker\n///   takes the hazard path (`resume_sessions`, still present) and MUST raise every\n///   session's viewer count to >= 1, proving `test_session_viewer_count` observes the\n///   very subscription the positive arm forbids (a bare \"assert 0\" could pass vacuously\n///   against an always-0 seam).\n///\n/// DETERMINISM of the seam arm (no timing window): `subscribe_with` is FIRE-AND-FORGET\n/// (it writes the `KIND_SUBSCRIBE` frame and does NOT await `KIND_SUBSCRIBED`), and\n/// `resume_sessions` runs its internal `sessions()` round-trip BEFORE sending those\n/// subscribes — so on return the subscribes may not yet be applied. We force a\n/// happens-before with ONE follow-up `sessions()` round-trip on the SAME hazard conn:\n/// the broker dispatches that conn's frames strictly in order and inserts each viewer\n/// synchronously (`add_viewer`) as it dispatches the `KIND_SUBSCRIBE`, so all three\n/// subscribes are applied before the `SESSIONS` reply is produced. That reply-read loop\n/// also DRAINS the interleaved replay frames, so the broker's viewer writers never block\n/// — no deadlock, and (at N=3) no write is held long enough for the r4 watchdog to\n/// retire/evict a viewer before we read the counts. State is settled the moment the\n/// barrier `sessions()` returns.\n// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n#[test]\nfn daemon_resume_leaves_zero_brain_subscribers() {\n    const N: usize = 3;\n    init_home();\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n\n    // Stand up N real PTY sessions the brain will resume. Flooding is irrelevant to\n    // this STRUCTURAL check — we reuse spawn_flood only to host real sessions and hold\n    // the spawner controller conns + pids open, exactly like heartbeat_ticks_under_flood.\n    let mut conns = Vec::new();\n    let mut pids = Vec::new();\n    let mut sids = Vec::new();\n    for i in 0..N {\n        let (sid, pid, conn) = spawn_flood(&name, &format!(\"zero-sub-ep-{i}\"));\n        sids.push(sid);\n        pids.push(pid);\n        conns.push(conn); // hold the spawner controller conns open (undrained)\n    }\n\n    // POSITIVE arm — the SHIPPING daemon resume path: cursor-only, NO subscribe.\n    let mut brain = Brain::cold_start(&name, 0).expect(\"cold_start probe brain\");\n    brain.resume_session_cursors().expect(\"cursor-only resume\");\n    for &sid in &sids {\n        assert_eq!(\n            broker.test_session_viewer_count(sid),\n            Some(0),\n            \"the daemon's cursor-only resume must attach ZERO viewers to session {sid}; a \\\n             non-zero count means the brain SUBSCRIBED its own undrained heartbeat conn — \\\n             the round-3 SharedSend self-deadlock class re-opens (post-r4 it instead \\\n             poisons/retires the heartbeat conn via the write watchdog — still a broken \\\n             resume).\"\n        );\n    }\n\n    // SEAM-SENSITIVITY arm — a SECOND brain on the SAME broker takes the hazard path\n    // (`resume_sessions` still exists) and MUST raise every count to >= 1, proving the\n    // seam actually observes the subscription the positive arm forbids (else the Some(0)\n    // above is a vacuous pass against an always-0 read).\n    let mut hazard_brain = Brain::cold_start(&name, 0).expect(\"cold_start hazard brain\");\n    hazard_brain.resume_sessions().expect(\"hazard subscribe resume\");\n    // Deterministic barrier: subscribe_with is fire-and-forget and resume_sessions'\n    // internal round-trip precedes the subscribes, so flush with ONE more same-conn\n    // sessions() round-trip. Per-conn in-order dispatch means all 3 KIND_SUBSCRIBE\n    // frames were applied (add_viewer inserts synchronously) before this reply; the\n    // reply-read loop also drains the replay so no viewer writer blocks/evicts. State\n    // is settled on return — no timing window.\n    hazard_brain\n        .sessions()\n        .expect(\"barrier round-trip flushes the fire-and-forget subscribes\");\n    for &sid in &sids {\n        let count = broker.test_session_viewer_count(sid);\n        assert!(\n            matches!(count, Some(c) if c >= 1),\n            \"seam check: a subscribing resume MUST attach >= 1 viewer to session {sid} \\\n             (got {count:?}). A 0/None here means the viewer-count seam does not observe \\\n             subscriptions, which would make the Some(0) positive assertion above vacuous.\"\n        );\n    }\n\n    for pid in pids {\n        kill_pid(pid);\n    }\n    drop(conns);\n    drop(brain);\n…\n}\n\n[Showing lines 259-358 of 361. Use :359 to continue]"}],"details":{"truncation":{"content":"/// proof was invalidated — it failed REAL on kitsubito Linux CI (\"worst completed 10\").\r\n/// doyle ruling `docs/UPDATE-WEDGE-R4-STALE-GUARD-RULING.md` (2026-07-10). The two\r\n/// cursor-only heartbeat tests above remain the POSITIVE liveness proof; THIS is the\r\n/// deterministic, timing-independent STRUCTURAL proof that the shipping resume attaches\r\n/// ZERO subscribers.\r\n///\r\n/// Two settled arms, no timing window:\r\n///   POSITIVE — the shipping daemon path (`resume_session_cursors`) must leave EVERY\r\n///   hosted session with ZERO viewers. A resume that instead subscribed the undrained\r\n///   heartbeat conn re-opens the round-3 SharedSend self-deadlock class; post-r4 it\r\n///   would instead get that heartbeat conn poisoned/retired by the write watchdog —\r\n///   still a broken resume, just a different failure. Cursor-only must attach nothing.\r\n///   SEAM-SENSITIVITY (keeps the proof NON-VACUOUS) — a SECOND brain on the SAME broker\r\n///   takes the hazard path (`resume_sessions`, still present) and MUST raise every\r\n///   session's viewer count to >= 1, proving `test_session_viewer_count` observes the\r\n///   very subscription the positive arm forbids (a bare \"assert 0\" could pass vacuously\r\n///   against an always-0 seam).\r\n///\r\n/// DETERMINISM of the seam arm (no timing window): `subscribe_with` is FIRE-AND-FORGET\r\n/// (it writes the `KIND_SUBSCRIBE` frame and does NOT await `KIND_SUBSCRIBED`), and\r\n/// `resume_sessions` runs its internal `sessions()` round-trip BEFORE sending those\r\n/// subscribes — so on return the subscribes may not yet be applied. We force a\r\n/// happens-before with ONE follow-up `sessions()` round-trip on the SAME hazard conn:\r\n/// the broker dispatches that conn's frames strictly in order and inserts each viewer\r\n/// synchronously (`add_viewer`) as it dispatches the `KIND_SUBSCRIBE`, so all three\r\n/// subscribes are applied before the `SESSIONS` reply is produced. That reply-read loop\r\n/// also DRAINS the interleaved replay frames, so the broker's viewer writers never block\r\n/// — no deadlock, and (at N=3) no write is held long enough for the r4 watchdog to\r\n/// retire/evict a viewer before we read the counts. State is settled the moment the\r\n/// barrier `sessions()` returns.\r\n// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n#[test]\r\nfn daemon_resume_leaves_zero_brain_subscribers() {\r\n    const N: usize = 3;\r\n    init_home();\r\n    let name = unique_name();\r\n    let broker = Broker::bind(&name).expect(\"bind broker\");\r\n    let serve = Arc::clone(&broker);\r\n    thread::spawn(move || {\r\n        let _ = serve.serve();\r\n    });\r\n\r\n    // Stand up N real PTY sessions the brain will resume. Flooding is irrelevant to\r\n    // this STRUCTURAL check — we reuse spawn_flood only to host real sessions and hold\r\n    // the spawner controller conns + pids open, exactly like heartbeat_ticks_under_flood.\r\n    let mut conns = Vec::new();\r\n    let mut pids = Vec::new();\r\n    let mut sids = Vec::new();\r\n    for i in 0..N {\r\n        let (sid, pid, conn) = spawn_flood(&name, &format!(\"zero-sub-ep-{i}\"));\r\n        sids.push(sid);\r\n        pids.push(pid);\r\n        conns.push(conn); // hold the spawner controller conns open (undrained)\r\n    }\r\n\r\n    // POSITIVE arm — the SHIPPING daemon resume path: cursor-only, NO subscribe.\r\n    let mut brain = Brain::cold_start(&name, 0).expect(\"cold_start probe brain\");\r\n    brain.resume_session_cursors().expect(\"cursor-only resume\");\r\n    for &sid in &sids {\r\n        assert_eq!(\r\n            broker.test_session_viewer_count(sid),\r\n            Some(0),\r\n            \"the daemon's cursor-only resume must attach ZERO viewers to session {sid}; a \\\r\n             non-zero count means the brain SUBSCRIBED its own undrained heartbeat conn — \\\r\n             the round-3 SharedSend self-deadlock class re-opens (post-r4 it instead \\\r\n             poisons/retires the heartbeat conn via the write watchdog — still a broken \\\r\n             resume).\"\r\n        );\r\n    }\r\n\r\n    // SEAM-SENSITIVITY arm — a SECOND brain on the SAME broker takes the hazard path\r\n    // (`resume_sessions` still exists) and MUST raise every count to >= 1, proving the\r\n    // seam actually observes the subscription the positive arm forbids (else the Some(0)\r\n    // above is a vacuous pass against an always-0 read).\r\n    let mut hazard_brain = Brain::cold_start(&name, 0).expect(\"cold_start hazard brain\");\r\n    hazard_brain.resume_sessions().expect(\"hazard subscribe resume\");\r\n    // Deterministic barrier: subscribe_with is fire-and-forget and resume_sessions'\r\n    // internal round-trip precedes the subscribes, so flush with ONE more same-conn\r\n    // sessions() round-trip. Per-conn in-order dispatch means all 3 KIND_SUBSCRIBE\r\n    // frames were applied (add_viewer inserts synchronously) before this reply; the\r\n    // reply-read loop also drains the replay so no viewer writer blocks/evicts. State\r\n    // is settled on return — no timing window.\r\n    hazard_brain\r\n        .sessions()\r\n        .expect(\"barrier round-trip flushes the fire-and-forget subscribes\");\r\n    for &sid in &sids {\r\n        let count = broker.test_session_viewer_count(sid);\r\n        assert!(\r\n            matches!(count, Some(c) if c >= 1),\r\n            \"seam check: a subscribing resume MUST attach >= 1 viewer to session {sid} \\\r\n             (got {count:?}). A 0/None here means the viewer-count seam does not observe \\\r\n             subscriptions, which would make the Some(0) positive assertion above vacuous.\"\r\n        );\r\n    }\r\n\r\n    for pid in pids {\r\n        kill_pid(pid);\r\n    }\r\n    drop(conns);\r\n    drop(brain);\r","truncated":true,"truncatedBy":"lines","totalLines":103,"totalBytes":5606,"outputLines":100,"outputBytes":5606,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// proof was invalidated — it failed REAL on kitsubito Linux CI (\"worst completed 10\").\r\n/// doyle ruling `docs/UPDATE-WEDGE-R4-STALE-GUARD-RULING.md` (2026-07-10). The two\r\n/// cursor-only heartbeat tests above remain the POSITIVE liveness proof; THIS is the\r\n/// deterministic, timing-independent STRUCTURAL proof that the shipping resume attaches\r\n/// ZERO subscribers.\r\n///\r\n/// Two settled arms, no timing window:\r\n///   POSITIVE — the shipping daemon path (`resume_session_cursors`) must leave EVERY\r\n///   hosted session with ZERO viewers. A resume that instead subscribed the undrained\r\n///   heartbeat conn re-opens the round-3 SharedSend self-deadlock class; post-r4 it\r\n///   would instead get that heartbeat conn poisoned/retired by the write watchdog —\r\n///   still a broken resume, just a different failure. Cursor-only must attach nothing.\r\n///   SEAM-SENSITIVITY (keeps the proof NON-VACUOUS) — a SECOND brain on the SAME broker\r\n///   takes the hazard path (`resume_sessions`, still present) and MUST raise every\r\n///   session's viewer count to >= 1, proving `test_session_viewer_count` observes the\r\n///   very subscription the positive arm forbids (a bare \"assert 0\" could pass vacuously\r\n///   against an always-0 seam).\r\n///\r\n/// DETERMINISM of the seam arm (no timing window): `subscribe_with` is FIRE-AND-FORGET\r\n/// (it writes the `KIND_SUBSCRIBE` frame and does NOT await `KIND_SUBSCRIBED`), and\r\n/// `resume_sessions` runs its internal `sessions()` round-trip BEFORE sending those\r\n/// subscribes — so on return the subscribes may not yet be applied. We force a\r\n/// happens-before with ONE follow-up `sessions()` round-trip on the SAME hazard conn:\r\n/// the broker dispatches that conn's frames strictly in order and inserts each viewer\r\n/// synchronously (`add_viewer`) as it dispatches the `KIND_SUBSCRIBE`, so all three\r\n/// subscribes are applied before the `SESSIONS` reply is produced. That reply-read loop\r\n/// also DRAINS the interleaved replay frames, so the broker's viewer writers never block\r\n/// — no deadlock, and (at N=3) no write is held long enough for the r4 watchdog to\r\n/// retire/evict a viewer before we read the counts. State is settled the moment the\r\n/// barrier `sessions()` returns.\r\n// [int->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n#[test]\r\nfn daemon_resume_leaves_zero_brain_subscribers() {\r\n    const N: usize = 3;\r\n    init_home();\r\n    let name = unique_name();\r\n    let broker = Broker::bind(&name).expect(\"bind broker\");\r\n    let serve = Arc::clone(&broker);\r\n    thread::spawn(move || {\r\n        let _ = serve.serve();\r\n    });\r\n\r\n    // Stand up N real PTY sessions the brain will resume. Flooding is irrelevant to\r\n    // this STRUCTURAL check — we reuse spawn_flood only to host real sessions and hold\r\n    // the spawner controller conns + pids open, exactly like heartbeat_ticks_under_flood.\r\n    let mut conns = Vec::new();\r\n    let mut pids = Vec::new();\r\n    let mut sids = Vec::new();\r\n    for i in 0..N {\r\n        let (sid, pid, conn) = spawn_flood(&name, &format!(\"zero-sub-ep-{i}\"));\r\n        sids.push(sid);\r\n        pids.push(pid);\r\n        conns.push(conn); // hold the spawner controller conns open (undrained)\r\n    }\r\n\r\n    // POSITIVE arm — the SHIPPING daemon resume path: cursor-only, NO subscribe.\r\n    let mut brain = Brain::cold_start(&name, 0).expect(\"cold_start probe brain\");\r\n    brain.resume_session_cursors().expect(\"cursor-only resume\");\r\n    for &sid in &sids {\r\n        assert_eq!(\r\n            broker.test_session_viewer_count(sid),\r\n            Some(0),\r\n            \"the daemon's cursor-only resume must attach ZERO viewers to session {sid}; a \\\r\n             non-zero count means the brain SUBSCRIBED its own undrained heartbeat conn — \\\r\n             the round-3 SharedSend self-deadlock class re-opens (post-r4 it instead \\\r\n             poisons/retires the heartbeat conn via the write watchdog — still a broken \\\r\n             resume).\"\r\n        );\r\n    }\r\n\r\n    // SEAM-SENSITIVITY arm — a SECOND brain on the SAME broker takes the hazard path\r\n    // (`resume_sessions` still exists) and MUST raise every count to >= 1, proving the\r\n    // seam actually observes the subscription the positive arm forbids (else the Some(0)\r\n    // above is a vacuous pass against an always-0 read).\r\n    let mut hazard_brain = Brain::cold_start(&name, 0).expect(\"cold_start hazard brain\");\r\n    hazard_brain.resume_sessions().expect(\"hazard subscribe resume\");\r\n    // Deterministic barrier: subscribe_with is fire-and-forget and resume_sessions'\r\n    // internal round-trip precedes the subscribes, so flush with ONE more same-conn\r\n    // sessions() round-trip. Per-conn in-order dispatch means all 3 KIND_SUBSCRIBE\r\n    // frames were applied (add_viewer inserts synchronously) before this reply; the\r\n    // reply-read loop also drains the replay so no viewer writer blocks/evicts. State\r\n    // is settled on return — no timing window.\r\n    hazard_brain\r\n        .sessions()\r\n        .expect(\"barrier round-trip flushes the fire-and-forget subscribes\");\r\n    for &sid in &sids {\r\n        let count = broker.test_session_viewer_count(sid);\r\n        assert!(\r\n            matches!(count, Some(c) if c >= 1),\r\n            \"seam check: a subscribing resume MUST attach >= 1 viewer to session {sid} \\\r\n             (got {count:?}). A 0/None here means the viewer-count seam does not observe \\\r\n             subscriptions, which would make the Some(0) positive assertion above vacuous.\"\r\n        );\r\n    }\r\n\r\n    for pid in pids {\r\n        kill_pid(pid);\r\n    }\r\n    drop(conns);\r\n    drop(brain);\r\n…\n}","startLine":259,"lineNumbers":[259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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,null,360]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\brain_resume_conn_deadlock.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":361,"totalBytes":5606,"outputLines":100,"outputBytes":5606,"shownRange":{"start":259,"end":358},"nextOffset":359}}},"isError":false,"timestamp":1784195833925}}
{"type":"message","id":"009755c3","parentId":"ceb3f688","timestamp":"2026-07-16T09:57:19.928Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reconnecting tests in rc module**"},{"type":"toolCall","id":"call_MsmwsIOjFrg6yaVkCnyR7BdB|fc_008390f508d1d09b016a58aafec328819ab9af0ff5cde47a35","name":"grep","arguments":{"i":"Listing rc lifecycle tests","pattern":"fn .*?(detach|reconnect|sever|eof|stall|serve|cursor|displac|broker|attach).*\\(","path":"crates/spt/src/rc.rs","selector":"2025-3214","case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":17671,"output":121,"cacheRead":217600,"cacheWrite":0,"totalTokens":235392,"reasoningTokens":35,"cost":{"input":0.088355,"output":0.00363,"cacheRead":0.1088,"cacheWrite":0,"total":0.200785}},"stopReason":"toolUse","timestamp":1784195833949,"responseId":"resp_008390f508d1d09b016a58aafa4fa0819aa13f51a1a21b381f","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_008390f508d1d09b016a58aafd6368819aa4c2e5084bb3adcd","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKr-wMYuByRGIW3TeD8eILFlxuHUcHv02G2GmsOiaGevh65LjJ-ouVvE1O9-jNsxuNA1o8W4U2kRA3dpdk9fPUMbzObYPwvw75NUpF1AlXovuSJQTY7azBkcdOJmoxFDxZ42LHbvcl8crkCSLWnQouEcRCehxsDlaQrX-o2auzd7eXgGXIdal_Lj4SyP7R0CDCN9GLEup7C6RgO4F9ZFqIONPOCV79_X-giCB3JjDtNzzrwNRzrk0l6xC00Qw-z8b9n48ZsXCprc19AjsCcqQKB8WUOH0WJaKJaAO2CnYTSJuKFDTpRCm14jYihqocDT3HVwTYhKagBKS1hE5j1HoYnCow75sh_Q6W-v8YWvKJeh7Eygf0MKkB_AQbz7lb5xkxCrekgB6DUEKuBnf2H0BPzCGEpnYfgPZjPViOh5Tb5YT51bbPMPPs206A14Q49bA9g2dLk3xQum7tcJMBz_SOzaGjh7SjB0Invt1ifCT4xbyaPO8csgmHjgOq5gYy7whKRMKmWC34vamxs7fv_ZoQotNo7KeWQu8jmW-fjypDHkFe4vVz1UlS1HtYYooJ8A1BoPRqwm-AYBgMgi8p6N40MSUSCmhKKRfSJWj-sp7xz435dtpTdX8YqNabDUuaVYAsBY0Q3QzgeuJszRxx2FsEy_vPSsD5Lf8qzbPrsi6Y2JrFe0YJh-608dnves3Mh8b3Z3b0zUt9D1c1tN5wsC52W3v1O9FT45XUieZklH5HFjiQ90xDDRwoLUXsJQhV4JaUwrwyel4WaMah8IizvRA0lwQR4WC9ejRZLZJ4SwlEIXhVJuoQvr1-2ga8mBj9ayKpYsNroXS2UYQQdSZwzS4hGHNCvpCOQyMykYK17mBrwRKQ0eIo7yAeaR9Vkj3scOrvvoZpZ8EQiUW8n7t7NDXSIcDTX8Z5Eu64LG2kf-Mr4WXnj3PkwHyA_cdLNEfprbHihYRLBhNHixP4_aDt7rCz0PuwPYOu4iwHGKvYLBP4k2kKphPYHQnnD7R6ReRERq7NZ6-tM08OosmORx1O8271oCJn6o8f1dIQXGS8_N6YTgP47OtZcn4Y7guc3pp3aix3le9z5znyeBfF4cB6YnCAEB2MF4gvfQnD2Mg35WZkODV0SfwNVW4TeqrmAkC9xBuLTY","summary":[{"type":"summary_text","text":"**Reconnecting tests in rc module**"}]},{"id":"fc_008390f508d1d09b016a58aafec328819ab9af0ff5cde47a35","type":"function_call","status":"completed","arguments":"{\"i\":\"Listing rc lifecycle tests\",\"pattern\":\"fn .*?(detach|reconnect|sever|eof|stall|serve|cursor|displac|broker|attach).*\\\\(\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"2025-3214\",\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_MsmwsIOjFrg6yaVkCnyR7BdB","name":"grep"}]},"duration":5978.305999999866,"ttft":3417.373999999836,"contextSnapshot":{"promptTokens":235271,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_MsmwsIOjFrg6yaVkCnyR7BdB|fc_008390f508d1d09b016a58aafec328819ab9af0ff5cde47a35","toolName":"grep","startedAt":"2026-07-16T09:57:19.928Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Listing rc lifecycle tests"},"id":"d47bbe80","parentId":"009755c3","timestamp":"2026-07-16T09:57:19.928Z"}
{"type":"message","id":"df31913b","parentId":"d47bbe80","timestamp":"2026-07-16T09:57:19.935Z","message":{"role":"toolResult","toolCallId":"call_MsmwsIOjFrg6yaVkCnyR7BdB|fc_008390f508d1d09b016a58aafec328819ab9af0ff5cde47a35","toolName":"grep","content":[{"type":"text","text":" 2037|    #[test]\n*2038|    fn attach_offline_endpoint_short_circuits_clean() {\n 2039|        let _home = crate::testutil::isolated_home();\n 2040|        let id = \"stopped-ep\";\n 2041|        let perch = spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);\n...\n 2061|    #[test]\n*2062|    fn first_event_stall_decision() {\n 2063|        // No event + past the generous grace → stalled.\n 2064|        assert!(first_event_stalled(false, FIRST_EVENT_GRACE));\n 2065|        assert!(first_event_stalled(false, FIRST_EVENT_GRACE + Duration::from_secs(1)));\n...\n 2143|    #[allow(clippy::zombie_processes)]\n*2144|    fn attach_viewport_reconnects_across_a_broker_bounce() {\n 2145|        let _home = crate::testutil::isolated_home();\n 2146|        let _seed = SeedGuard::start();\n 2147|        let broker_name = spt_daemon::endpoint::broker_socket_name();\n...\n 2360|    #[test]\n*2361|    fn broker_host_child() {\n 2362|        if std::env::var(\"SPT_RC_FIXTURE_BROKER_CHILD\").as_deref() != Ok(\"1\") {\n 2363|            return;\n 2364|        }\n...\n 2389|    #[test]\n*2390|    fn reconnect_class_only_severed_transport_redrives() {\n 2391|        assert_eq!(reconnect_class(&PumpEnd::BrokerGone), EndClass::Reconnect);\n 2392|        assert_eq!(reconnect_class(&PumpEnd::Severed), EndClass::Reconnect);\n 2393|        assert_eq!(reconnect_class(&PumpEnd::Exited(Some(0))), EndClass::Final);\n...\n 2414|    #[test]\n*2415|    fn serve_eof_severed_vs_refused() {\n 2416|        assert!(matches!(serve_eof_end(true), PumpEnd::Severed));\n 2417|        assert!(matches!(serve_eof_end(false), PumpEnd::NoLiveSession));\n 2418|    }\n...\n 2422|    #[test]\n*2423|    fn reconnect_window_expiry_decision() {\n 2424|        assert!(!reconnect_expired(Duration::from_secs(0)));\n 2425|        assert!(!reconnect_expired(RECONNECT_WINDOW - Duration::from_millis(1)));\n 2426|        assert!(reconnect_expired(RECONNECT_WINDOW));\n...\n 2433|    #[test]\n*2434|    fn reconnect_banner_clears_and_centers() {\n 2435|        let bytes = reconnect_banner_bytes(24, 80, \"kitsubito\", 30);\n 2436|        let s = String::from_utf8_lossy(&bytes);\n 2437|        assert!(s.starts_with(\"\\x1b[2J\\x1b[H\"), \"clear + home first\");\n...\n 2450|    #[test]\n*2451|    fn reconnect_banner_shows_the_countdown() {\n 2452|        let bytes = reconnect_banner_bytes(24, 80, \"local daemon\", 17);\n 2453|        let s = String::from_utf8_lossy(&bytes);\n 2454|        assert!(\n...\n 2468|    #[test]\n*2469|    fn reconnect_remaining_secs_counts_down_ceil() {\n 2470|        assert_eq!(\n 2471|            reconnect_remaining_secs(Duration::ZERO),\n 2472|            RECONNECT_WINDOW.as_secs()\n...\n 2535|    #[test]\n*2536|    fn attach_against_stopped_daemon_never_spawns_and_exits_loud() {\n 2537|        let _home = crate::testutil::isolated_home();\n 2538|        // Daemon is DOWN: no SeedGuard here, so is_running() (the home-tagged\n 2539|        // seed-control ping) is false and stays that way unless something spawns one.\n...\n 2579|    #[test]\n*2580|    fn public_attach_failure_maps_deadline_class_only() {\n 2581|        let mapped = public_attach_failure(\n 2582|            \"doyle\",\n 2583|            \"attach open: brain IPC read deadline elapsed\".to_string(),\n...\n 2602|    #[test]\n*2603|    fn classify_read_err_eof_is_graceful_not_fatal() {\n 2604|        use std::io::ErrorKind::*;\n 2605|        // The exact kind behind \"failed to fill whole buffer\".\n 2606|        assert_eq!(classify_read_err(UnexpectedEof), ReadDisposition::BrokerGone);\n...\n 2623|    #[test]\n*2624|    fn detach_keybind_semantics() {\n 2625|        let mut armed = false;\n 2626|        // plain passthrough\n 2627|        assert_eq!(parse(&mut armed, b\"hello\"), (b\"hello\".to_vec(), false));\n...\n 2651|    #[test]\n*2652|    fn detach_prefix_spans_chunks() {\n 2653|        let mut armed = false;\n 2654|        // chunk 1 ends on the bare prefix: nothing forwarded yet, armed carries.\n 2655|        assert_eq!(parse(&mut armed, &[b'a', DETACH_PREFIX]), (b\"a\".to_vec(), false));\n...\n 2798|    #[test]\n*2799|    fn key_event_step_detach_sm() {\n 2800|        use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};\n 2801|        let ctrl = KeyModifiers::CONTROL;\n 2802|        let none = KeyModifiers::NONE;\n...\n 3038|    #[test]\n*3039|    fn status_assert_reserves_row1_then_repaints() {\n 3040|        let s = StatusRow { text: \"x\".to_string(), rows: 24, cols: 80 };\n 3041|        let bytes = s.assert_bytes();\n 3042|        // The margin set comes first."}],"details":{"scopePath":"crates/spt/src/rc.rs","searchPath":"C:/Users/decid/Documents/projects/spt-core/crates/spt/src/rc.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":17,"fileCount":1,"files":["crates/spt/src/rc.rs"],"fileMatches":[{"path":"crates/spt/src/rc.rs","count":17}],"truncated":false,"displayContent":" 2037│    #[test]\n*2038│    fn attach_offline_endpoint_short_circuits_clean() {\n 2039│        let _home = crate::testutil::isolated_home();\n 2040│        let id = \"stopped-ep\";\n 2041│        let perch = spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);\n     │...\n 2061│    #[test]\n*2062│    fn first_event_stall_decision() {\n 2063│        // No event + past the generous grace → stalled.\n 2064│        assert!(first_event_stalled(false, FIRST_EVENT_GRACE));\n 2065│        assert!(first_event_stalled(false, FIRST_EVENT_GRACE + Duration::from_secs(1)));\n     │...\n 2143│    #[allow(clippy::zombie_processes)]\n*2144│    fn attach_viewport_reconnects_across_a_broker_bounce() {\n 2145│        let _home = crate::testutil::isolated_home();\n 2146│        let _seed = SeedGuard::start();\n 2147│        let broker_name = spt_daemon::endpoint::broker_socket_name();\n     │...\n 2360│    #[test]\n*2361│    fn broker_host_child() {\n 2362│        if std::env::var(\"SPT_RC_FIXTURE_BROKER_CHILD\").as_deref() != Ok(\"1\") {\n 2363│            return;\n 2364│        }\n     │...\n 2389│    #[test]\n*2390│    fn reconnect_class_only_severed_transport_redrives() {\n 2391│        assert_eq!(reconnect_class(&PumpEnd::BrokerGone), EndClass::Reconnect);\n 2392│        assert_eq!(reconnect_class(&PumpEnd::Severed), EndClass::Reconnect);\n 2393│        assert_eq!(reconnect_class(&PumpEnd::Exited(Some(0))), EndClass::Final);\n     │...\n 2414│    #[test]\n*2415│    fn serve_eof_severed_vs_refused() {\n 2416│        assert!(matches!(serve_eof_end(true), PumpEnd::Severed));\n 2417│        assert!(matches!(serve_eof_end(false), PumpEnd::NoLiveSession));\n 2418│    }\n     │...\n 2422│    #[test]\n*2423│    fn reconnect_window_expiry_decision() {\n 2424│        assert!(!reconnect_expired(Duration::from_secs(0)));\n 2425│        assert!(!reconnect_expired(RECONNECT_WINDOW - Duration::from_millis(1)));\n 2426│        assert!(reconnect_expired(RECONNECT_WINDOW));\n     │...\n 2433│    #[test]\n*2434│    fn reconnect_banner_clears_and_centers() {\n 2435│        let bytes = reconnect_banner_bytes(24, 80, \"kitsubito\", 30);\n 2436│        let s = String::from_utf8_lossy(&bytes);\n 2437│        assert!(s.starts_with(\"\\x1b[2J\\x1b[H\"), \"clear + home first\");\n     │...\n 2450│    #[test]\n*2451│    fn reconnect_banner_shows_the_countdown() {\n 2452│        let bytes = reconnect_banner_bytes(24, 80, \"local daemon\", 17);\n 2453│        let s = String::from_utf8_lossy(&bytes);\n 2454│        assert!(\n     │...\n 2468│    #[test]\n*2469│    fn reconnect_remaining_secs_counts_down_ceil() {\n 2470│        assert_eq!(\n 2471│            reconnect_remaining_secs(Duration::ZERO),\n 2472│            RECONNECT_WINDOW.as_secs()\n     │...\n 2535│    #[test]\n*2536│    fn attach_against_stopped_daemon_never_spawns_and_exits_loud() {\n 2537│        let _home = crate::testutil::isolated_home();\n 2538│        // Daemon is DOWN: no SeedGuard here, so is_running() (the home-tagged\n 2539│        // seed-control ping) is false and stays that way unless something spawns one.\n     │...\n 2579│    #[test]\n*2580│    fn public_attach_failure_maps_deadline_class_only() {\n 2581│        let mapped = public_attach_failure(\n 2582│            \"doyle\",\n 2583│            \"attach open: brain IPC read deadline elapsed\".to_string(),\n     │...\n 2602│    #[test]\n*2603│    fn classify_read_err_eof_is_graceful_not_fatal() {\n 2604│        use std::io::ErrorKind::*;\n 2605│        // The exact kind behind \"failed to fill whole buffer\".\n 2606│        assert_eq!(classify_read_err(UnexpectedEof), ReadDisposition::BrokerGone);\n     │...\n 2623│    #[test]\n*2624│    fn detach_keybind_semantics() {\n 2625│        let mut armed = false;\n 2626│        // plain passthrough\n 2627│        assert_eq!(parse(&mut armed, b\"hello\"), (b\"hello\".to_vec(), false));\n     │...\n 2651│    #[test]\n*2652│    fn detach_prefix_spans_chunks() {\n 2653│        let mut armed = false;\n 2654│        // chunk 1 ends on the bare prefix: nothing forwarded yet, armed carries.\n 2655│        assert_eq!(parse(&mut armed, &[b'a', DETACH_PREFIX]), (b\"a\".to_vec(), false));\n     │...\n 2798│    #[test]\n*2799│    fn key_event_step_detach_sm() {\n 2800│        use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};\n 2801│        let ctrl = KeyModifiers::CONTROL;\n 2802│        let none = KeyModifiers::NONE;\n     │...\n 3038│    #[test]\n*3039│    fn status_assert_reserves_row1_then_repaints() {\n 3040│        let s = StatusRow { text: \"x\".to_string(), rows: 24, cols: 80 };\n 3041│        let bytes = s.assert_bytes();\n 3042│        // The margin set comes first."},"isError":false,"timestamp":1784195839935}}
{"type":"compaction","id":"7a491218","parentId":"df31913b","timestamp":"2026-07-16T09:57:20.094Z","summary":"[Superseded compaction summary elided after a newer compaction]","shortSummary":"Superseded compaction elided","firstKeptEntryId":"312b743c","tokensBefore":235392,"details":{"readFiles":["CONTEXT.md","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/tests","crates/spt-daemon/tests/attach.rs","crates/spt-net/src/net/attach.rs","crates/spt-term/src/pty.rs","crates/spt/src/rc.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","docs/DEBUG-ROLLOUT.md","docs/KNOWN-HAZARDS.md","docs/adr/0008-live-activity-buffer-pty-digest.md","docs/adr/0018-broker-brain-process-isolation-restoration.md","docs/adr/0031-server-side-screen-grid-render-repaint.md"],"modifiedFiles":[]},"fromExtension":false}
{"type":"message","id":"5b247df0","parentId":"7a491218","timestamp":"2026-07-16T09:57:34.244Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reviewing reconnect test sections**"},{"type":"toolCall","id":"call_e61G0G8aalaByGpWy4w3mdgY|fc_047bb49263dfa303016a58ab0d2a8081988fccc40c9d4bd2a2","name":"read","arguments":{"i":"Reading rc reconnect e2e","path":"crates/spt/src/rc.rs","selector":"2130-2430"}},{"type":"toolCall","id":"call_kyaaHpoWPeD67kGnoDnogLSd|fc_047bb49263dfa303016a58ab0d2a948198a22494962e36e399","name":"read","arguments":{"i":"Reading attach wedge e2e","path":"crates/spt/tests/attach_wedge_e2e.rs","selector":"1-340"}},{"type":"toolCall","id":"call_3Cgsnd0GFMwWFpiDEaBW7CzF|fc_047bb49263dfa303016a58ab0d2aa88198b8da9705afc72a99","name":"read","arguments":{"i":"Reading daemon attach integration","path":"crates/spt-daemon/tests/attach.rs","selector":"1-375,760-1035"}},{"type":"toolCall","id":"call_oOkR70vqpTe6z0VOdHJFh8cu|fc_047bb49263dfa303016a58ab0d2ab48198a400a5c14a02164a","name":"read","arguments":{"i":"Reading stamp lifetime tests","path":"crates/spt-daemon/tests/control_stamp_lifetime.rs","selector":"1-420"}},{"type":"toolCall","id":"call_fa60VALw877CGsy4uvJyjTJV|fc_047bb49263dfa303016a58ab0d2adc81989352904d099d05c8","name":"read","arguments":{"i":"Reading broker subscribe tests index","path":"crates/spt-daemon/tests/broker.rs","selector":"1-80,700-1040,1250-1640"}},{"type":"toolCall","id":"call_ETDAnlMRsZ3ktImNNeVnEjd4|fc_047bb49263dfa303016a58ab0d2b408198837baa3969f76e61","name":"grep","arguments":{"i":"Finding broker connection lifecycle tests","pattern":"fn .*?(disconnect|detach|unsubscribe|controller|viewer|reap|epoch|writer|attach).*\\(","path":"crates/spt-daemon/src/broker.rs;crates/spt-daemon/tests/broker.rs;crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/control_stamp_lifetime.rs","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":52456,"output":341,"cacheRead":0,"cacheWrite":0,"totalTokens":52797,"reasoningTokens":9,"cost":{"input":0.26228,"output":0.01023,"cacheRead":0,"cacheWrite":0,"total":0.27251000000000003}},"stopReason":"toolUse","timestamp":1784195840151,"responseId":"resp_047bb49263dfa303016a58ab00ac988198a9c4b3e4400554ac","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58ab05bbf48198aa870a945d4ba9e8","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKsN9OJbPsksJhGbiMQcAdguZvXY3bugvOwrt4lksTIQC9iWQopd3YLNEE5U1mzv_e4GXmPTx7r4hOM3yQONzlckN5PjvqhyeXaYupci8Lt5sYc-5HU9jFFbpD3PPvk7mkPYgCbkhqTlsY2NmtCLRQ5eB2wI81ZYCZUdCFBMG4vbd6wY7M5zdZ23fnWigeb9kR5zltatZuzYxnvNo6Nq0qh8o3b5LZUmmD6ZqaPrIk8oVUAWbsnoQoH6U9gNZJmKfZEXElvXpVMkzJJW7GXcrZEhM9YfqXAEOFxcbyr80Hxgfwj9SDW5AQ2-oxpKZ_0uWgVzyUVTwU_xRGTrcPTLxJnqA2Cg0Yuw3DSaH2nCi7fpyklI4beZ7PzyZ6CWdE1VvnmtIMsKM38JfK6NTofetdM-gK_pz8Rg2f20pSXnYeyerbygI5T-h8WIuRq1w6B_-p4NzaMdTVVcVOfVk5reqGVr-EaWvXH7llGJRvcc5KnoB6Pp4Nhid_u5H3bKJd51fvuIzLHiVfQMue8x5bg8Ubb8PnQ3tuugTRVCQZcQNc2x8Ce7A-NCNCUR0j92_yIW4r3eO6EkMhBNLavA91MJTuOT8AaGEzj4gReIEdC5uLUJVVepGeHlDK1tP7_Qnn-MALsaDPjVDAlM073CGc1wfgSikW7JZEScIyZnV9KqFobwPiwEc76tmdG4py9NhFPivhJpzCeSFArN6qatwyYlc6Zp8ZQLfZIKgSlbo4LeDulTiaMOsP-PupWy8LcJXv8AxlDsgrvJ_MzmKuCVOklpzlV3tQNNLiU4fGa2KytdCW3NYK6gy_8tI9O2UFnj3TPwIbcMlnRSvliVSYqvXqeGrHhSr85HPLuL-3G_A5sjLfNd77_p1eaH0n-trFd_w11-mRJ-Z1SVmCstf75_b5E3nZT-yqxqTkccO_heNA9rqLVi2vCz0NkJQTBw6fGIoEx4CM4OzVQDxNbUOVk0hZdWXwh-hw==","summary":[{"type":"summary_text","text":"**Reviewing reconnect test sections**"}]},{"id":"fc_047bb49263dfa303016a58ab0d2a8081988fccc40c9d4bd2a2","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading rc reconnect e2e\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"2130-2430\"}","call_id":"call_e61G0G8aalaByGpWy4w3mdgY","name":"read"},{"id":"fc_047bb49263dfa303016a58ab0d2a948198a22494962e36e399","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach wedge e2e\",\"path\":\"crates/spt/tests/attach_wedge_e2e.rs\",\"selector\":\"1-340\"}","call_id":"call_kyaaHpoWPeD67kGnoDnogLSd","name":"read"},{"id":"fc_047bb49263dfa303016a58ab0d2aa88198b8da9705afc72a99","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading daemon attach integration\",\"path\":\"crates/spt-daemon/tests/attach.rs\",\"selector\":\"1-375,760-1035\"}","call_id":"call_3Cgsnd0GFMwWFpiDEaBW7CzF","name":"read"},{"id":"fc_047bb49263dfa303016a58ab0d2ab48198a400a5c14a02164a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading stamp lifetime tests\",\"path\":\"crates/spt-daemon/tests/control_stamp_lifetime.rs\",\"selector\":\"1-420\"}","call_id":"call_oOkR70vqpTe6z0VOdHJFh8cu","name":"read"},{"id":"fc_047bb49263dfa303016a58ab0d2adc81989352904d099d05c8","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker subscribe tests index\",\"path\":\"crates/spt-daemon/tests/broker.rs\",\"selector\":\"1-80,700-1040,1250-1640\"}","call_id":"call_fa60VALw877CGsy4uvJyjTJV","name":"read"},{"id":"fc_047bb49263dfa303016a58ab0d2b408198837baa3969f76e61","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding broker connection lifecycle tests\",\"pattern\":\"fn .*?(disconnect|detach|unsubscribe|controller|viewer|reap|epoch|writer|attach).*\\\\(\",\"path\":\"crates/spt-daemon/src/broker.rs;crates/spt-daemon/tests/broker.rs;crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/control_stamp_lifetime.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_ETDAnlMRsZ3ktImNNeVnEjd4","name":"grep"}]},"duration":14076.374699999578,"ttft":5560.2750000003725,"contextSnapshot":{"promptTokens":52456,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_e61G0G8aalaByGpWy4w3mdgY|fc_047bb49263dfa303016a58ab0d2a8081988fccc40c9d4bd2a2","toolName":"read","startedAt":"2026-07-16T09:57:34.245Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading rc reconnect e2e"},"id":"e4aa7ba5","parentId":"5b247df0","timestamp":"2026-07-16T09:57:34.245Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_kyaaHpoWPeD67kGnoDnogLSd|fc_047bb49263dfa303016a58ab0d2a948198a22494962e36e399","toolName":"read","startedAt":"2026-07-16T09:57:34.245Z","args":{"path":"crates/spt/tests/attach_wedge_e2e.rs"},"intent":"Reading attach wedge e2e"},"id":"885c3a24","parentId":"e4aa7ba5","timestamp":"2026-07-16T09:57:34.245Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_3Cgsnd0GFMwWFpiDEaBW7CzF|fc_047bb49263dfa303016a58ab0d2aa88198b8da9705afc72a99","toolName":"read","startedAt":"2026-07-16T09:57:34.245Z","args":{"path":"crates/spt-daemon/tests/attach.rs"},"intent":"Reading daemon attach integration"},"id":"28f59bb0","parentId":"885c3a24","timestamp":"2026-07-16T09:57:34.245Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_oOkR70vqpTe6z0VOdHJFh8cu|fc_047bb49263dfa303016a58ab0d2ab48198a400a5c14a02164a","toolName":"read","startedAt":"2026-07-16T09:57:34.245Z","args":{"path":"crates/spt-daemon/tests/control_stamp_lifetime.rs"},"intent":"Reading stamp lifetime tests"},"id":"e1ed94d7","parentId":"28f59bb0","timestamp":"2026-07-16T09:57:34.245Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_fa60VALw877CGsy4uvJyjTJV|fc_047bb49263dfa303016a58ab0d2adc81989352904d099d05c8","toolName":"read","startedAt":"2026-07-16T09:57:34.245Z","args":{"path":"crates/spt-daemon/tests/broker.rs"},"intent":"Reading broker subscribe tests index"},"id":"2fa16c27","parentId":"e1ed94d7","timestamp":"2026-07-16T09:57:34.245Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ETDAnlMRsZ3ktImNNeVnEjd4|fc_047bb49263dfa303016a58ab0d2b408198837baa3969f76e61","toolName":"grep","startedAt":"2026-07-16T09:57:34.245Z","args":{"path":"crates/spt-daemon/src/broker.rs;crates/spt-daemon/tests/broker.rs;crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/control_stamp_lifetime.rs"},"intent":"Finding broker connection lifecycle tests"},"id":"32a3b2e0","parentId":"2fa16c27","timestamp":"2026-07-16T09:57:34.245Z"}
{"type":"message","id":"c3caacbb","parentId":"32a3b2e0","timestamp":"2026-07-16T09:57:34.269Z","message":{"role":"toolResult","toolCallId":"call_fa60VALw877CGsy4uvJyjTJV|fc_047bb49263dfa303016a58ab0d2adc81989352904d099d05c8","toolName":"read","content":[{"type":"text","text":"//! B1 integration: a [`Broker`] hosts one real PTY child and relays its output\n//! and injected input over the versioned local IPC, on both OSes.\n//!\n//! The child is an echo program (`cat` on Unix, `findstr \".\"` on Windows — the\n//! same cross-OS shape spt-term's injection tests use): bytes the brain injects\n//! round-trip through the real PTY and come back as `output` events. We then\n//! kill it and observe the `exit` event. This exercises the whole B1 contract —\n//! spawn under a broker PTY, stream output, accept input, report exit — through\n//! the actual named-pipe / UDS transport.\n//!\n//! The brain is driven **single-threaded**: send a command, then read frames\n//! until the expected reply/event arrives (accumulating any interleaved output).\n//! interprocess `Stream::split()` shares the underlying fd, so a split client\n//! would only signal EOF once *both* halves drop; keeping one `Stream` means\n//! dropping it closes the fd, the broker's command loop hits EOF, and it tears\n//! the session down deterministically — which we then assert.\n// [int->REQ-DAEMON-2]\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::Duration;\n\nuse interprocess::local_socket::Stream;\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::effect::{EffectKey, Minter};\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{\n    decode_bytes, encode_bytes, AppliedEvent, EndpointInjected, EndpointInputReq, ExitEvent,\n    InputReq, KillReq, OutputEvent, SpawnReq, Spawned, SubscribeReq, KIND_APPLIED,\n    KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,\n    KIND_OUTPUT, KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,\n};\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\nuse spt_daemon::Broker;\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique_name() -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-b1-{}-{}.sock\", std::process::id(), n)\n}\n\n/// The echo child: reads stdin, writes each line to stdout.\nfn echo_spawn_req() -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n    #[cfg(windows)]\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n    SpawnReq {\n        program,\n        args,\n        rows: 24,\n        cols: 80,\n        endpoint: String::new(),\n        cwd: None,\n        env: Default::default(),\n        translation_binary: None,\n        adapter: String::new(),\n        install_dir: None,\n    }\n}\n\nfn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n}\n\nfn main_test() {\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n\n    // The broker accepts one brain and serves it on a handler thread.\n    let server_broker = Arc::clone(&broker);\n    let server = thread::spawn(move || {\n        let handle = server_broker.serve_once().expect(\"accept brain\");\n        handle.join().expect(\"handler thread\");\n    });\n\n    // Brain side: connect (retry briefly in case accept lags the bind).\n    let mut conn = None;\n    for _ in 0..200 {\n…\n    }\n…\n}\n…\nfn connect_brain(name: &str) -> Stream {\n…\n    for _ in 0..200 {\n        match LocalSocketTransport::connect(name) {\n…\n            Err(_) => thread::sleep(Duration::from_millis(10)),\n        }\n    }\n    let mut conn = conn.expect(\"connect to broker\");\n    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n    conn\n}\n\n/// Spawn the long-lived echo child and return its session id (draining any\n/// interleaved early output into `out`).\nfn spawn_echo(conn: &mut Stream, out: &mut Vec<u8>) -> u64 {\n    send(conn, KIND_SPAWN, serde_json::to_value(echo_spawn_req()).unwrap());\n    loop {\n        let f = read_frame(conn).expect(\"frame before spawned\");\n        match f.kind.as_str() {\n            KIND_SPAWNED => {\n                break serde_json::from_value::<Spawned>(f.payload)\n                    .unwrap()\n                    .session_id\n            }\n            KIND_OUTPUT => accumulate_output(&f, out),\n            _ => {}\n        }\n    }\n}\n\n/// Send one journaled `KIND_INPUT` (`op_id` carried) with the given `ack` flag.\nfn send_input_op(conn: &mut Stream, sid: u64, op_id: u64, bytes: &[u8], ack: bool) {\n    send(\n        conn,\n        KIND_INPUT,\n        serde_json::to_value(InputReq {\n            session_id: sid,\n            data_b64: encode_bytes(bytes),\n            op_id: Some(op_id),\n            minter: Some(Minter::Rc),\n            ack,\n        })\n        .unwrap(),\n    );\n}\n\n/// Count non-overlapping occurrences of `needle` in `hay`.\nfn count(hay: &[u8], needle: &[u8]) -> usize {\n    if needle.is_empty() || hay.len() < needle.len() {\n        return 0;\n    }\n    let (mut n, mut i) = (0usize, 0usize);\n    while i + needle.len() <= hay.len() {\n        if &hay[i..i + needle.len()] == needle {\n            n += 1;\n            i += needle.len();\n        } else {\n            i += 1;\n        }\n    }\n    n\n}\n\n/// REQ-HAZARD-INPUT-ACK-BACKPRESSURE — the ACK GATE, the crux unit. Against a\n/// REAL broker hosting a REAL spawned PTY session, an `op_id`-carrying input with\n/// `ack = false` produces NO `KIND_APPLIED` frame, while one with `ack = true`\n/// does. This is the broker leg of the fix: `dispatch_input` writes the applied\n/// envelope ONLY when `req.ack`, so the fire-and-forward operator/rc flood never\n/// fills the broker→brain return direction (the deadlock the whole REQ cures).\n///\n/// We prove \"no applied for the no-ack op\" by ORDERING: send the no-ack op FIRST,\n/// then a KNOWN-ACKED probe op. If the broker ever emitted an applied for the\n/// no-ack op it would arrive BEFORE the probe's (same single-threaded conn, FIFO\n/// frame order). So the FIRST applied frame we observe must carry the PROBE's\n/// op_id — never the no-ack op_id. Both bytes still round-trip through the PTY\n/// (the no-ack write is delivered; only the ack frame is suppressed).\n///\n/// Non-vacuous: revert the fix (drop the `if req.ack` gate so dispatch_input\n/// always sends the applied frame) and the no-ack op's applied frame arrives\n/// first → `first_applied_op == NO_ACK_OP` → the assert below flips to RED.\n// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\nfn ack_gate_test() {\n    const NO_ACK_OP: u64 = 100;\n    const PROBE_OP: u64 = 200;\n\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n    let server_broker = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = server_broker.serve();\n    });\n\n    // Drive the whole exchange on a worker thread feeding a result channel, so a\n    // BLOCKING `read_frame` (Windows named pipes reject I/O timeouts) that stalls\n    // once steady-state output stops surfaces as a watchdog timeout — a clean test\n    // FAILURE — rather than hanging the suite (the `probe()` discipline above).\n    let (tx, rx) = std::sync::mpsc::channel::<(Option<u64>, bool, bool)>();\n    let worker_name = name.clone();\n    thread::spawn(move || {\n        let mut conn = connect_brain(&worker_name);\n        let mut out = Vec::<u8>::new();\n        let sid = spawn_echo(&mut conn, &mut out);\n\n        // (1) The fire-and-forward write: ack=false. The byte must still reach the\n        //     PTY (delivery is unaffected), but NO applied frame may come back for it.\n        send_input_op(&mut conn, sid, NO_ACK_OP, b\"NOACK-LINE\\r\", false);\n        // (2) The known-acked probe: ack=true. Its applied frame is the\n        //     synchronization point — when we see it, any applied for the no-ack op\n        //     (if the gate were broken) would already have arrived before it on this\n        //     FIFO conn.\n        send_input_op(&mut conn, sid, PROBE_OP, b\"ACK-LINE\\r\", true);\n\n        let mut first_applied_op: Option<u64> = None;\n        let mut noack_echoed = false;\n        let mut probe_echoed = false;\n        for _ in 0..4000 {\n            if first_applied_op == Some(PROBE_OP) && noack_echoed && probe_echoed {\n                break;\n            }\n            match read_frame(&mut conn) {\n                Ok(f) if f.kind == KIND_APPLIED => {\n                    let ev: AppliedEvent = serde_json::from_value(f.payload).unwrap();\n                    if first_applied_op.is_none() {\n                        first_applied_op = Some(ev.op_id);\n                    }\n                }\n                Ok(f) if f.kind == KIND_OUTPUT => {\n                    accumulate_output(&f, &mut out);\n                    let s = String::from_utf8_lossy(&out);\n                    noack_echoed = s.contains(\"NOACK-LINE\");\n                    probe_echoed = s.contains(\"ACK-LINE\");\n                }\n                Ok(_) => {}\n                Err(_) => break,\n            }\n        }\n        let _ = tx.send((first_applied_op, noack_echoed, probe_echoed));\n        drop(conn);\n    });\n\n    // Watchdog: the gate must resolve well within this deadline. The acked probe's\n    // applied frame is pushed by the broker the moment it processes the input, and\n    // the echo child round-trips both lines promptly, so a stall here is itself a\n    // signal (e.g. a broken gate flooding the return direction).\n    let (first_applied_op, noack_echoed, probe_echoed) = rx\n        .recv_timeout(Duration::from_secs(20))\n        .expect(\"ack-gate exchange must resolve within the watchdog, never hang\");\n\n    // The POSITIVE leg: the acked op DID get its applied frame (otherwise the gate\n    // would be suppressing everything, a different bug).\n    assert_eq!(\n        first_applied_op,\n        Some(PROBE_OP),\n        \"the acked op (ack=true) must receive a KIND_APPLIED frame, and it must be \\\n         the FIRST applied seen — a no-ack op's applied (the bug) would precede it. \\\n         first_applied={first_applied_op:?}\"\n    );\n    // The crux: the no-ack op must NEVER be the source of an applied frame.\n    assert_ne!(\n        first_applied_op,\n        Some(NO_ACK_OP),\n        \"an ack=false input must NOT produce a KIND_APPLIED frame (the gate); a frame \\\n         on the return direction for every flooded input is exactly the deadlock \\\n         REQ-HAZARD-INPUT-ACK-BACKPRESSURE cures\"\n    );\n    // Delivery is unaffected by the ack flag — both lines reached the PTY.\n    assert!(\n        noack_echoed,\n        \"the ack=false input must still be DELIVERED to the PTY (only the ack frame \\\n         is suppressed)\"\n    );\n    assert!(probe_echoed, \"the ack=true input must also reach the PTY\");\n}\n\n#[test]\nfn ack_false_input_suppresses_applied_frame() {\n    ack_gate_test();\n}\n\n/// REQ-HAZARD-INPUT-ACK-BACKPRESSURE — EXACTLY-ONCE on the NO-ACK path. The fix\n/// must not weaken exactly-once: dedup happens at the broker's applied-set\n/// (`apply_once`) regardless of whether the ack frame is emitted. Replay the SAME\n/// `(session, op_id)` with `ack = false` MORE THAN ONCE; the byte is written to\n/// the PTY exactly once (a second apply_once for the key is deduped, the PtyWrite\n/// effect is not re-run), and the broker-owned applied-set holds the key once.\n///\n/// Mirrors `idempotent.rs`'s anchor-based proof: the applied-set is the\n/// authoritative exactly-once evidence (it is the ONLY path that writes the PTY\n/// for an `op_id`, and it records the key only after the write runs), so\n/// `applied_count == 1` for one distinct key ⇒ the write ran exactly once across\n/// the replays. The journal points at a temp file (not the shared home).\n///\n/// Non-vacuous: if the no-ack path bypassed `apply_once` (wrote directly without\n/// dedup), the replayed byte would be typed twice and `applied_count` would not\n/// hold the single-key invariant — this assert goes RED. The echo *count* alone\n/// is deliberately not the sole proof (cooked-mode PTY echo can re-emit a line),\n/// so the broker anchor is the load-bearing assertion, corroborated by receipt.\n// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\nfn exactly_once_no_ack_test() {\n    const OP: u64 = 77;\n\n    let name = unique_name();\n    let journal = tempfile::tempdir().expect(\"tempdir\");\n    let broker = Broker::bind_in(&name, journal.path().join(\"effects.log\")).expect(\"bind broker\");\n    let server_broker = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = server_broker.serve();\n    });\n\n    // Drive the send + drain on a worker (the blocking `read_frame` can stall once\n    // steady-state output stops) feeding back (session_id, echo_count). The main\n    // thread bounds it with a watchdog, then reads the AUTHORITATIVE exactly-once\n    // evidence straight off the broker-owned journal.\n    let (tx, rx) = std::sync::mpsc::channel::<(u64, usize)>();\n    let worker_name = name.clone();\n    thread::spawn(move || {\n        let mut conn = connect_brain(&worker_name);\n        let mut out = Vec::<u8>::new();\n        let sid = spawn_echo(&mut conn, &mut out);\n\n        // The SAME (session, op_id) sent THREE times, all ack=false (the fire-and-\n        // forward replay an rc re-drive after a hiccup would produce). The broker\n        // dedups by the key; the PtyWrite runs only on the first apply_once.\n        let line = b\"ONCE-ONLY-LINE\\r\";\n        for _ in 0..3 {\n            send_input_op(&mut conn, sid, OP, line, false);\n            // No applied frame comes back (ack=false), so don't wait for one — just\n            // give the broker a beat to process each frame in order.\n            thread::sleep(Duration::from_millis(40));\n        }\n\n        // Drain output until the line round-trips (or a bounded frame budget runs\n        // out). We do NOT add a trailing fixed-duration settle loop: a blocking\n        // `read_frame` on a Windows named pipe cannot be time-bounded, so once the\n        // echo child goes idle a settle read would hang. A double-type (the bug we\n        // guard) is caught authoritatively by the broker's applied-set\n        // (`applied_count == 1`) — the echo count is corroboration, not the proof —\n        // so an exact echo-count window is unnecessary here.\n        for _ in 0..2000 {\n            if String::from_utf8_lossy(&out).contains(\"ONCE-ONLY-LINE\") {\n                break;\n            }\n            match read_frame(&mut conn) {\n                Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\n                Ok(_) => {}\n                Err(_) => break,\n            }\n        }\n        let _ = tx.send((sid, count(&out, b\"ONCE-ONLY-LINE\")));\n        drop(conn);\n    });\n\n    let (sid, echo_count) = rx\n        .recv_timeout(Duration::from_secs(20))\n        .expect(\"exactly-once exchange must resolve within the watchdog, never hang\");\n\n    // ANCHOR (authoritative): the applied-set holds the single key exactly once —\n    // the PtyWrite ran once across the three no-ack deliveries (dedup at apply_once).\n    assert!(\n        broker.journal().is_applied(EffectKey {\n            class: sid,\n            minter: Minter::Rc,\n            op: OP,\n        }),\n        \"the no-ack op must be recorded in the applied-set (exactly-once anchor)\"\n    );\n    assert_eq!(\n        broker.journal().applied_count(),\n        1,\n        \"three replays of the same (session, op_id) with ack=false must apply EXACTLY \\\n         ONCE — dedup at the broker's applied-set is unaffected by the ack flag\"\n    );\n    // Corroboration: the line reached the PTY (delivery happened) — the byte count\n    // is NOT asserted == 1 because cooked-mode echo can re-emit; the anchor above is\n    // what proves single-application.\n    assert!(\n        echo_count >= 1,\n        \"the deduped no-ack write must still reach the PTY at least once (echo_count={echo_count})\"\n    );\n}\n\n#[test]\nfn exactly_once_preserved_on_no_ack_path() {\n    exactly_once_no_ack_test();\n}\n\n// ── v0.13.0 P1c — REQ-HAZARD-CONTROLLER-WRITER-REORDER ────────────────────────\n// THE INT KEYSTONE: deterministically drive the two-`become_controller`-on-ONE-\n// connection ordering through the PUBLIC broker path (a brain that subscribes\n// from_seq=1, then re-subscribes from_seq=0 over the SAME conn — reproducing the\n// pre-fix handoff(eager subscribe @ prior.next_seq=1) + re-serve(attach_as(sid,0))\n// sequence that spawned writer-A then writer-B racing one socket). The fix makes\n// the operator/consumer view monotonic + byte-exact + exactly-once, and the\n// session stays live. Real broker + real PTY + real local IPC — NO mocks.\n//\n// Why the assertion is at the CONSUMER (dedup) level, not raw-wire: fix #2 (the\n// epoch gate) stops a SUPERSEDED writer that has not yet committed, but a frame\n// the stale writer-A committed BEFORE the second `become_controller` bumps the\n// epoch is irreducible at the broker (per the controller_writer doc-comment). Fix\n// #1 removes the contradictory double-take in production; fix #3 makes the\n// consumer dedup-below+snap-above tolerant. The INVARIANT a consumer must always\n// see is therefore: the ACCEPTED seq stream is monotonic non-decreasing and\n// exactly-once (an at-least-once boundary re-send of an already-seen seq is\n// deduped, never a gap/`got seq 1 want 0` and never a dropped lower seq). We\n// apply the EXACT production discipline (mirroring brain.rs `read_event`'s\n// resume-mode path: drop `seq < cursor`, accept `seq >= cursor`, cursor=seq+1).\nfn controller_writer_reorder_test() {\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n    let server_broker = Arc::clone(&broker);\n    let server = thread::spawn(move || {\n        let handle = server_broker.serve_once().expect(\"accept brain\");\n        handle.join().expect(\"handler thread\");\n    });\n\n    let mut conn = None;\n    for _ in 0..200 {\n        match LocalSocketTransport::connect(&name) {\n            Ok(c) => {\n                conn = Some(c);\n                break;\n            }\n            Err(_) => thread::sleep(Duration::from_millis(10)),\n        }\n    }\n    let mut conn = conn.expect(\"connect to broker\");\n    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n\n    // Spawn the echo child; the broker auto-subscribes this conn as controller.\n    send(\n        &mut conn,\n        KIND_SPAWN,\n        serde_json::to_value(echo_spawn_req()).unwrap(),\n    );\n    let sid = loop {\n        let f = read_frame(&mut conn).expect(\"frame before spawned\");\n        if f.kind == KIND_SPAWNED {\n            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;\n        }\n    };\n\n    // The production dedup-below + snap-above filter — the EXACT discipline brain.rs\n    // `read_event` applies in resume mode: drop `seq < cursor` (a deduped\n    // at-least-once boundary re-send), accept `seq >= cursor` and advance\n    // `cursor = seq + 1`. As it accepts, it asserts the accepted seqs never go\n…\n}\n[Range 1250-1640 is beyond end of file (1219 lines total); skipped]"}],"details":{"displayContent":{"text":"//! B1 integration: a [`Broker`] hosts one real PTY child and relays its output\n//! and injected input over the versioned local IPC, on both OSes.\n//!\n//! The child is an echo program (`cat` on Unix, `findstr \".\"` on Windows — the\n//! same cross-OS shape spt-term's injection tests use): bytes the brain injects\n//! round-trip through the real PTY and come back as `output` events. We then\n//! kill it and observe the `exit` event. This exercises the whole B1 contract —\n//! spawn under a broker PTY, stream output, accept input, report exit — through\n//! the actual named-pipe / UDS transport.\n//!\n//! The brain is driven **single-threaded**: send a command, then read frames\n//! until the expected reply/event arrives (accumulating any interleaved output).\n//! interprocess `Stream::split()` shares the underlying fd, so a split client\n//! would only signal EOF once *both* halves drop; keeping one `Stream` means\n//! dropping it closes the fd, the broker's command loop hits EOF, and it tears\n//! the session down deterministically — which we then assert.\n// [int->REQ-DAEMON-2]\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::Duration;\n\nuse interprocess::local_socket::Stream;\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::effect::{EffectKey, Minter};\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{\n    decode_bytes, encode_bytes, AppliedEvent, EndpointInjected, EndpointInputReq, ExitEvent,\n    InputReq, KillReq, OutputEvent, SpawnReq, Spawned, SubscribeReq, KIND_APPLIED,\n    KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,\n    KIND_OUTPUT, KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,\n};\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\nuse spt_daemon::Broker;\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique_name() -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-b1-{}-{}.sock\", std::process::id(), n)\n}\n\n/// The echo child: reads stdin, writes each line to stdout.\nfn echo_spawn_req() -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n    #[cfg(windows)]\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n    SpawnReq {\n        program,\n        args,\n        rows: 24,\n        cols: 80,\n        endpoint: String::new(),\n        cwd: None,\n        env: Default::default(),\n        translation_binary: None,\n        adapter: String::new(),\n        install_dir: None,\n    }\n}\n\nfn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n}\n\nfn main_test() {\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n\n    // The broker accepts one brain and serves it on a handler thread.\n    let server_broker = Arc::clone(&broker);\n    let server = thread::spawn(move || {\n        let handle = server_broker.serve_once().expect(\"accept brain\");\n        handle.join().expect(\"handler thread\");\n    });\n\n    // Brain side: connect (retry briefly in case accept lags the bind).\n    let mut conn = None;\n    for _ in 0..200 {\n…\n    }\n…\n}\n…\nfn connect_brain(name: &str) -> Stream {\n…\n    for _ in 0..200 {\n        match LocalSocketTransport::connect(name) {\n…\n            Err(_) => thread::sleep(Duration::from_millis(10)),\n        }\n    }\n    let mut conn = conn.expect(\"connect to broker\");\n    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n    conn\n}\n\n/// Spawn the long-lived echo child and return its session id (draining any\n/// interleaved early output into `out`).\nfn spawn_echo(conn: &mut Stream, out: &mut Vec<u8>) -> u64 {\n    send(conn, KIND_SPAWN, serde_json::to_value(echo_spawn_req()).unwrap());\n    loop {\n        let f = read_frame(conn).expect(\"frame before spawned\");\n        match f.kind.as_str() {\n            KIND_SPAWNED => {\n                break serde_json::from_value::<Spawned>(f.payload)\n                    .unwrap()\n                    .session_id\n            }\n            KIND_OUTPUT => accumulate_output(&f, out),\n            _ => {}\n        }\n    }\n}\n\n/// Send one journaled `KIND_INPUT` (`op_id` carried) with the given `ack` flag.\nfn send_input_op(conn: &mut Stream, sid: u64, op_id: u64, bytes: &[u8], ack: bool) {\n    send(\n        conn,\n        KIND_INPUT,\n        serde_json::to_value(InputReq {\n            session_id: sid,\n            data_b64: encode_bytes(bytes),\n            op_id: Some(op_id),\n            minter: Some(Minter::Rc),\n            ack,\n        })\n        .unwrap(),\n    );\n}\n\n/// Count non-overlapping occurrences of `needle` in `hay`.\nfn count(hay: &[u8], needle: &[u8]) -> usize {\n    if needle.is_empty() || hay.len() < needle.len() {\n        return 0;\n    }\n    let (mut n, mut i) = (0usize, 0usize);\n    while i + needle.len() <= hay.len() {\n        if &hay[i..i + needle.len()] == needle {\n            n += 1;\n            i += needle.len();\n        } else {\n            i += 1;\n        }\n    }\n    n\n}\n\n/// REQ-HAZARD-INPUT-ACK-BACKPRESSURE — the ACK GATE, the crux unit. Against a\n/// REAL broker hosting a REAL spawned PTY session, an `op_id`-carrying input with\n/// `ack = false` produces NO `KIND_APPLIED` frame, while one with `ack = true`\n/// does. This is the broker leg of the fix: `dispatch_input` writes the applied\n/// envelope ONLY when `req.ack`, so the fire-and-forward operator/rc flood never\n/// fills the broker→brain return direction (the deadlock the whole REQ cures).\n///\n/// We prove \"no applied for the no-ack op\" by ORDERING: send the no-ack op FIRST,\n/// then a KNOWN-ACKED probe op. If the broker ever emitted an applied for the\n/// no-ack op it would arrive BEFORE the probe's (same single-threaded conn, FIFO\n/// frame order). So the FIRST applied frame we observe must carry the PROBE's\n/// op_id — never the no-ack op_id. Both bytes still round-trip through the PTY\n/// (the no-ack write is delivered; only the ack frame is suppressed).\n///\n/// Non-vacuous: revert the fix (drop the `if req.ack` gate so dispatch_input\n/// always sends the applied frame) and the no-ack op's applied frame arrives\n/// first → `first_applied_op == NO_ACK_OP` → the assert below flips to RED.\n// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\nfn ack_gate_test() {\n    const NO_ACK_OP: u64 = 100;\n    const PROBE_OP: u64 = 200;\n\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n    let server_broker = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = server_broker.serve();\n    });\n\n    // Drive the whole exchange on a worker thread feeding a result channel, so a\n    // BLOCKING `read_frame` (Windows named pipes reject I/O timeouts) that stalls\n    // once steady-state output stops surfaces as a watchdog timeout — a clean test\n    // FAILURE — rather than hanging the suite (the `probe()` discipline above).\n    let (tx, rx) = std::sync::mpsc::channel::<(Option<u64>, bool, bool)>();\n    let worker_name = name.clone();\n    thread::spawn(move || {\n        let mut conn = connect_brain(&worker_name);\n        let mut out = Vec::<u8>::new();\n        let sid = spawn_echo(&mut conn, &mut out);\n\n        // (1) The fire-and-forward write: ack=false. The byte must still reach the\n        //     PTY (delivery is unaffected), but NO applied frame may come back for it.\n        send_input_op(&mut conn, sid, NO_ACK_OP, b\"NOACK-LINE\\r\", false);\n        // (2) The known-acked probe: ack=true. Its applied frame is the\n        //     synchronization point — when we see it, any applied for the no-ack op\n        //     (if the gate were broken) would already have arrived before it on this\n        //     FIFO conn.\n        send_input_op(&mut conn, sid, PROBE_OP, b\"ACK-LINE\\r\", true);\n\n        let mut first_applied_op: Option<u64> = None;\n        let mut noack_echoed = false;\n        let mut probe_echoed = false;\n        for _ in 0..4000 {\n            if first_applied_op == Some(PROBE_OP) && noack_echoed && probe_echoed {\n                break;\n            }\n            match read_frame(&mut conn) {\n                Ok(f) if f.kind == KIND_APPLIED => {\n                    let ev: AppliedEvent = serde_json::from_value(f.payload).unwrap();\n                    if first_applied_op.is_none() {\n                        first_applied_op = Some(ev.op_id);\n                    }\n                }\n                Ok(f) if f.kind == KIND_OUTPUT => {\n                    accumulate_output(&f, &mut out);\n                    let s = String::from_utf8_lossy(&out);\n                    noack_echoed = s.contains(\"NOACK-LINE\");\n                    probe_echoed = s.contains(\"ACK-LINE\");\n                }\n                Ok(_) => {}\n                Err(_) => break,\n            }\n        }\n        let _ = tx.send((first_applied_op, noack_echoed, probe_echoed));\n        drop(conn);\n    });\n\n    // Watchdog: the gate must resolve well within this deadline. The acked probe's\n    // applied frame is pushed by the broker the moment it processes the input, and\n    // the echo child round-trips both lines promptly, so a stall here is itself a\n    // signal (e.g. a broken gate flooding the return direction).\n    let (first_applied_op, noack_echoed, probe_echoed) = rx\n        .recv_timeout(Duration::from_secs(20))\n        .expect(\"ack-gate exchange must resolve within the watchdog, never hang\");\n\n    // The POSITIVE leg: the acked op DID get its applied frame (otherwise the gate\n    // would be suppressing everything, a different bug).\n    assert_eq!(\n        first_applied_op,\n        Some(PROBE_OP),\n        \"the acked op (ack=true) must receive a KIND_APPLIED frame, and it must be \\\n         the FIRST applied seen — a no-ack op's applied (the bug) would precede it. \\\n         first_applied={first_applied_op:?}\"\n    );\n    // The crux: the no-ack op must NEVER be the source of an applied frame.\n    assert_ne!(\n        first_applied_op,\n        Some(NO_ACK_OP),\n        \"an ack=false input must NOT produce a KIND_APPLIED frame (the gate); a frame \\\n         on the return direction for every flooded input is exactly the deadlock \\\n         REQ-HAZARD-INPUT-ACK-BACKPRESSURE cures\"\n    );\n    // Delivery is unaffected by the ack flag — both lines reached the PTY.\n    assert!(\n        noack_echoed,\n        \"the ack=false input must still be DELIVERED to the PTY (only the ack frame \\\n         is suppressed)\"\n    );\n    assert!(probe_echoed, \"the ack=true input must also reach the PTY\");\n}\n\n#[test]\nfn ack_false_input_suppresses_applied_frame() {\n    ack_gate_test();\n}\n\n/// REQ-HAZARD-INPUT-ACK-BACKPRESSURE — EXACTLY-ONCE on the NO-ACK path. The fix\n/// must not weaken exactly-once: dedup happens at the broker's applied-set\n/// (`apply_once`) regardless of whether the ack frame is emitted. Replay the SAME\n/// `(session, op_id)` with `ack = false` MORE THAN ONCE; the byte is written to\n/// the PTY exactly once (a second apply_once for the key is deduped, the PtyWrite\n/// effect is not re-run), and the broker-owned applied-set holds the key once.\n///\n/// Mirrors `idempotent.rs`'s anchor-based proof: the applied-set is the\n/// authoritative exactly-once evidence (it is the ONLY path that writes the PTY\n/// for an `op_id`, and it records the key only after the write runs), so\n/// `applied_count == 1` for one distinct key ⇒ the write ran exactly once across\n/// the replays. The journal points at a temp file (not the shared home).\n///\n/// Non-vacuous: if the no-ack path bypassed `apply_once` (wrote directly without\n/// dedup), the replayed byte would be typed twice and `applied_count` would not\n/// hold the single-key invariant — this assert goes RED. The echo *count* alone\n/// is deliberately not the sole proof (cooked-mode PTY echo can re-emit a line),\n/// so the broker anchor is the load-bearing assertion, corroborated by receipt.\n// [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\nfn exactly_once_no_ack_test() {\n    const OP: u64 = 77;\n\n    let name = unique_name();\n    let journal = tempfile::tempdir().expect(\"tempdir\");\n    let broker = Broker::bind_in(&name, journal.path().join(\"effects.log\")).expect(\"bind broker\");\n    let server_broker = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = server_broker.serve();\n    });\n\n    // Drive the send + drain on a worker (the blocking `read_frame` can stall once\n    // steady-state output stops) feeding back (session_id, echo_count). The main\n    // thread bounds it with a watchdog, then reads the AUTHORITATIVE exactly-once\n    // evidence straight off the broker-owned journal.\n    let (tx, rx) = std::sync::mpsc::channel::<(u64, usize)>();\n    let worker_name = name.clone();\n    thread::spawn(move || {\n        let mut conn = connect_brain(&worker_name);\n        let mut out = Vec::<u8>::new();\n        let sid = spawn_echo(&mut conn, &mut out);\n\n        // The SAME (session, op_id) sent THREE times, all ack=false (the fire-and-\n        // forward replay an rc re-drive after a hiccup would produce). The broker\n        // dedups by the key; the PtyWrite runs only on the first apply_once.\n        let line = b\"ONCE-ONLY-LINE\\r\";\n        for _ in 0..3 {\n            send_input_op(&mut conn, sid, OP, line, false);\n            // No applied frame comes back (ack=false), so don't wait for one — just\n            // give the broker a beat to process each frame in order.\n            thread::sleep(Duration::from_millis(40));\n        }\n\n        // Drain output until the line round-trips (or a bounded frame budget runs\n        // out). We do NOT add a trailing fixed-duration settle loop: a blocking\n        // `read_frame` on a Windows named pipe cannot be time-bounded, so once the\n        // echo child goes idle a settle read would hang. A double-type (the bug we\n        // guard) is caught authoritatively by the broker's applied-set\n        // (`applied_count == 1`) — the echo count is corroboration, not the proof —\n        // so an exact echo-count window is unnecessary here.\n        for _ in 0..2000 {\n            if String::from_utf8_lossy(&out).contains(\"ONCE-ONLY-LINE\") {\n                break;\n            }\n            match read_frame(&mut conn) {\n                Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\n                Ok(_) => {}\n                Err(_) => break,\n            }\n        }\n        let _ = tx.send((sid, count(&out, b\"ONCE-ONLY-LINE\")));\n        drop(conn);\n    });\n\n    let (sid, echo_count) = rx\n        .recv_timeout(Duration::from_secs(20))\n        .expect(\"exactly-once exchange must resolve within the watchdog, never hang\");\n\n    // ANCHOR (authoritative): the applied-set holds the single key exactly once —\n    // the PtyWrite ran once across the three no-ack deliveries (dedup at apply_once).\n    assert!(\n        broker.journal().is_applied(EffectKey {\n            class: sid,\n            minter: Minter::Rc,\n            op: OP,\n        }),\n        \"the no-ack op must be recorded in the applied-set (exactly-once anchor)\"\n    );\n    assert_eq!(\n        broker.journal().applied_count(),\n        1,\n        \"three replays of the same (session, op_id) with ack=false must apply EXACTLY \\\n         ONCE — dedup at the broker's applied-set is unaffected by the ack flag\"\n    );\n    // Corroboration: the line reached the PTY (delivery happened) — the byte count\n    // is NOT asserted == 1 because cooked-mode echo can re-emit; the anchor above is\n    // what proves single-application.\n    assert!(\n        echo_count >= 1,\n        \"the deduped no-ack write must still reach the PTY at least once (echo_count={echo_count})\"\n    );\n}\n\n#[test]\nfn exactly_once_preserved_on_no_ack_path() {\n    exactly_once_no_ack_test();\n}\n\n// ── v0.13.0 P1c — REQ-HAZARD-CONTROLLER-WRITER-REORDER ────────────────────────\n// THE INT KEYSTONE: deterministically drive the two-`become_controller`-on-ONE-\n// connection ordering through the PUBLIC broker path (a brain that subscribes\n// from_seq=1, then re-subscribes from_seq=0 over the SAME conn — reproducing the\n// pre-fix handoff(eager subscribe @ prior.next_seq=1) + re-serve(attach_as(sid,0))\n// sequence that spawned writer-A then writer-B racing one socket). The fix makes\n// the operator/consumer view monotonic + byte-exact + exactly-once, and the\n// session stays live. Real broker + real PTY + real local IPC — NO mocks.\n//\n// Why the assertion is at the CONSUMER (dedup) level, not raw-wire: fix #2 (the\n// epoch gate) stops a SUPERSEDED writer that has not yet committed, but a frame\n// the stale writer-A committed BEFORE the second `become_controller` bumps the\n// epoch is irreducible at the broker (per the controller_writer doc-comment). Fix\n// #1 removes the contradictory double-take in production; fix #3 makes the\n// consumer dedup-below+snap-above tolerant. The INVARIANT a consumer must always\n// see is therefore: the ACCEPTED seq stream is monotonic non-decreasing and\n// exactly-once (an at-least-once boundary re-send of an already-seen seq is\n// deduped, never a gap/`got seq 1 want 0` and never a dropped lower seq). We\n// apply the EXACT production discipline (mirroring brain.rs `read_event`'s\n// resume-mode path: drop `seq < cursor`, accept `seq >= cursor`, cursor=seq+1).\nfn controller_writer_reorder_test() {\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n    let server_broker = Arc::clone(&broker);\n    let server = thread::spawn(move || {\n        let handle = server_broker.serve_once().expect(\"accept brain\");\n        handle.join().expect(\"handler thread\");\n    });\n\n    let mut conn = None;\n    for _ in 0..200 {\n        match LocalSocketTransport::connect(&name) {\n            Ok(c) => {\n                conn = Some(c);\n                break;\n            }\n            Err(_) => thread::sleep(Duration::from_millis(10)),\n        }\n    }\n    let mut conn = conn.expect(\"connect to broker\");\n    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n\n    // Spawn the echo child; the broker auto-subscribes this conn as controller.\n    send(\n        &mut conn,\n        KIND_SPAWN,\n        serde_json::to_value(echo_spawn_req()).unwrap(),\n    );\n    let sid = loop {\n        let f = read_frame(&mut conn).expect(\"frame before spawned\");\n        if f.kind == KIND_SPAWNED {\n            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;\n        }\n    };\n\n    // The production dedup-below + snap-above filter — the EXACT discipline brain.rs\n    // `read_event` applies in resume mode: drop `seq < cursor` (a deduped\n    // at-least-once boundary re-send), accept `seq >= cursor` and advance\n    // `cursor = seq + 1`. As it accepts, it asserts the accepted seqs never go\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,null,88,null,174,null,692,null,694,695,null,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,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,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,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,null,1206]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\broker.rs"}}},"isError":false,"timestamp":1784195854269}}
{"type":"message","id":"bfc26850","parentId":"c3caacbb","timestamp":"2026-07-16T09:57:34.270Z","message":{"role":"toolResult","toolCallId":"call_3Cgsnd0GFMwWFpiDEaBW7CzF|fc_047bb49263dfa303016a58ab0d2aa88198b8da9705afc72a99","toolName":"read","content":[{"type":"text","text":"//! D5b regression — remote terminal attach (REQ-INST-8): drive a real PTY\n//! session on daemon A from daemon B, byte-stream viewport over the\n//! broker-owned wire; compute stays on A.\n//!\n//! Two in-process brokers (hermetic loopback endpoints, relays disabled) play\n//! the two daemons; a real echo child runs under A's broker PTY. The\n//! invariants:\n//!\n//! - **remote drive**: keystrokes sent from B arrive at A's PTY through the\n//!   journaled input path, and the session's output streams back to B as\n//!   sequenced records — B sees its own marker echoed by A's child;\n//! - **origin is transport truth** (KNOWN-HAZARDS 7.5): the serve loop gates\n//!   on the stream table's handshake-proven `remote_id_hex`;\n//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies\n//!   mid-attach; the broker keeps the QUIC stream, the session, the output\n//!   log, and the effect journal. The successor brain re-serves the SAME\n//!   stream from seq 0 — the worst-case stale cursor: the replayed input\n//!   dedups at the PTY-write journal (typed exactly once), re-transmitted\n//!   output dedups at B's render cursor (rendered exactly once) — including\n//!   input B sent INTO the dead window.\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\nuse std::time::Duration;\n\nuse spt_daemon::attach::{\n    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n    AttachServeOutcome, ControllerIrrecoverablyBehind,\n};\n// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n#[cfg(unix)]\nuse spt_daemon::attach::request_attach_endpoint;\nuse spt_daemon::brain::{Brain, BrainState, BrokerEvent};\nuse spt_daemon::effect::{Minter, MintedOp};\nuse spt_daemon::msg::{decode_bytes, SpawnReq};\nuse spt_daemon::nethost::{NetConfig, NetHost};\nuse spt_daemon::Broker;\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\nuse spt_net::net::ndjson;\nuse spt_proto::identity::Identity;\nuse spt_term::ScreenGrid;\n\n/// Hold the temp home for the whole test binary so it isn't deleted mid-run.\n/// Every test inits it: serve_attach best-effort-stamps the endpoint perch's\n/// `driven_by` marker, and that resolution must never touch a real home.\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n\nfn init_home() {\n    HOME.get_or_init(|| {\n        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n        std::env::set_var(\"SPT_HOME\", dir.path());\n        dir\n    });\n}\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique_name() -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-d5b-{}-{}.sock\", std::process::id(), n)\n}\n\nfn hermetic() -> NetConfig {\n    NetConfig {\n        identity: Identity::generate(),\n        relay: RelayPolicy::Disabled,\n        discovery: LocalDiscovery::Off,\n        scope: BindScope::Loopback,\n        membership: None,\n    }\n}\n\nfn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n    let host = NetHost::start(hermetic()).expect(\"net host start\");\n    let broker =\n        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n    broker\n}\n\nfn connect_retry(name: &str) -> Brain {\n    for _ in 0..300 {\n        if let Ok(b) = Brain::cold_start(name, 1) {\n            return b;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"brain could not connect\");\n}\n\nfn echo_spawn_req() -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n    #[cfg(windows)]\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n    SpawnReq {\n        program,\n        args,\n        rows: 24,\n        cols: 80,\n        endpoint: String::new(),\n        cwd: None,\n        env: Default::default(),\n        translation_binary: None,\n        adapter: String::new(),\n        install_dir: None,\n    }\n}\n\n/// Poll the brain's `net-streams` until a PEER-initiated stream appears; return\n/// `(stream_id, remote_id_hex)`. Filtering on `!initiated_locally` is what the\n/// production dispatch loop does (dispatch.rs) — and it is REQUIRED for the\n/// single-broker loopback case, where the table holds BOTH the operator's own\n/// row and the cross-wired peer row: the serve target is always the peer row.\nfn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n    for _ in 0..400 {\n        let reply = brain.net_streams().expect(\"net-streams\");\n        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {\n            return (s.stream_id, s.remote_id_hex.clone());\n        }\n        thread::sleep(Duration::from_millis(5));\n    }\n    panic!(\"attach stream never appeared at the target's broker\");\n}\n\n/// The operator+serve drive shared by EVERY attach transport — the M12 W1.5\n/// one-pump invariant made STRUCTURAL: cross-node (QUIC conn) and local\n/// (broker loopback conn) call this identically; they diverge ONLY in how\n/// `conn_id` was acquired (`net_dial` vs `net_dial_loopback`) and in\n/// `expected_origin`, never in the `serve_attach`+render pump below. There is no\n/// second code path that could drift.\n///\n/// Given an acquired `conn_id` on `operator` and the `target` broker connection\n/// holding `session_id` for `endpoint`: open the attach stream, serve it on\n/// `target` under `expected_origin` (asserted == the stream table's proven\n/// origin), drive `marker` through the PTY and render it back over the wire,\n/// then detach and confirm the serve loop ended `Detached` (session outlives).\n/// Returns the rendered viewport bytes + the (still-live) target brain so the\n/// caller can assert session survival and clean up.\n#[allow(clippy::too_many_arguments)]\nfn attach_drive_detach(\n    operator: &mut Brain,\n    target_broker: &str,\n    mut target: Brain,\n    conn_id: u64,\n    session_id: u64,\n    endpoint: &str,\n    expected_origin: &str,\n    marker: &[u8],\n) -> (Vec<u8>, Brain) {\n    let stream_b = request_attach(operator, conn_id, session_id, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\n    operator\n        .net_stream_subscribe(stream_b, 0)\n        .expect(\"operator subscribe\");\n\n    // The target serves the inbound attach stream under the proven origin\n    // (KH 7.5 — the stream table, never payload). Cross-node: the handshake id.\n    // Loopback: this node's own id (broker-minted), which gates same-node.\n    let (stream_a, origin) = wait_for_stream(&mut target);\n    assert_eq!(\n        origin, expected_origin,\n        \"serve gate subject IS the stream table's proven origin\"\n    );\n    let endpoint_owned = endpoint.to_string();\n    let broker_owned = target_broker.to_string();\n    let server = thread::spawn(move || {\n        let outcome =\n            serve_attach(&mut target, &broker_owned, stream_a, &endpoint_owned, &origin, 0, None)\n                .expect(\"serve\");\n        (outcome, target)\n    });\n\n    // Drive the marker through; the echo streams back over the SAME pump.\n    let mut keys = marker.to_vec();\n    keys.push(b'\\r');\n    send_attach_input(operator, stream_b, &keys, 1).expect(\"input\");\n    let mut decoder = AttachDecoder::new();\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\n    render_until(\n        operator,\n        stream_b,\n        &mut decoder,\n        &mut cursor,\n        &mut viewport,\n        marker,\n    );\n\n    // Detach: the viewport ends with the session still running.\n    operator\n        .net_stream_send(stream_b, &[], None, true)\n        .expect(\"detach\");\n    let (outcome, target) = server.join().expect(\"serve thread\");\n    assert_eq!(outcome, AttachServeOutcome::Detached);\n    (viewport, target)\n}\n\n/// Operator-side viewport: consume attach-stream records, dedup `Output` by\n/// seq, append rendered bytes to `buf`, until `needle` is rendered. Returns\n/// the rendered-through cursor (last accepted seq + 1).\nfn render_until(\n    brain: &mut Brain,\n    stream_id: u64,\n    decoder: &mut AttachDecoder,\n    next_seq: &mut u64,\n    buf: &mut Vec<u8>,\n    needle: &[u8],\n) {\n    let contains = |hay: &[u8], needle: &[u8]| hay.windows(needle.len()).any(|w| w == needle);\n    while !contains(buf, needle) {\n        match brain.read_event().expect(\"operator read event\") {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    if let AttachRecord::Output { seq, data_b64 } = rec {\n                        if seq < *next_seq {\n                            continue; // replay duplicate — dedup by cursor\n                        }\n                        // #6 (ADR-0031): the initial batch is now a single synthesized\n                        // repaint at pseudo-seq `log.next_seq - 1`, so the FIRST frame a\n                        // fresh (from_seq=0) attach delivers is not seq 0 — it baselines\n                        // the cursor wherever the current screen sits. Every subsequent\n                        // live frame must then be contiguous (no forward gap).\n                        if *next_seq != 0 {\n                            assert_eq!(seq, *next_seq, \"no forward gap in the viewport\");\n                        }\n                        *next_seq = seq + 1;\n                        buf.extend_from_slice(&decode_bytes(&data_b64).expect(\"chunk b64\"));\n                    }\n                }\n            }\n            BrokerEvent::NetStreamEof { .. } => panic!(\"stream ended before the marker rendered\"),\n            BrokerEvent::Error { message } => panic!(\"operator broker error: {message}\"),\n            _ => {}\n        }\n    }\n}\n\n/// Count non-overlapping occurrences of `needle` in `hay`.\nfn count(hay: &[u8], needle: &[u8]) -> usize {\n    if needle.is_empty() || hay.len() < needle.len() {\n        return 0;\n    }\n    let (mut n, mut i) = (0, 0);\n    while i + needle.len() <= hay.len() {\n        if &hay[i..i + needle.len()] == needle {\n            n += 1;\n            i += needle.len();\n        } else {\n            i += 1;\n        }\n    }\n    n\n}\n\n// [int->REQ-RC-1] loopback self-dial is REFUSED by the transport — pins the\n// finding that drives `spt rc`'s LOCAL transport choice. A node cannot QUIC-dial\n// its own node id (iroh: \"Connecting to ourself is not supported\"), so local\n// attach CANNOT ride a self-dialed QUIC conn. The one-pump invariant therefore\n// holds via the fallback transport (a broker-local loopback conn feeding the same\n// `serve_attach` pump), NOT a self-dial — exactly the doyle-approved fallback.\n// Cross-node attach (the prize) is unaffected: it dials a real remote peer.\n#[test]\nfn loopback_self_dial_is_refused_local_uses_fallback_transport() {\n    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    let mut brain = connect_retry(&name);\n\n    let status = brain.net_status().expect(\"status\");\n    // Dialing our OWN advertised address is refused at the transport — fail-fast,\n    // never a deadlock; local attach must not be built on a self-dial.\n    let err = brain\n        .net_dial(status.addr, Some(MintedOp::new(Minter::Cli, 1)))\n        .expect_err(\"self-dial must be refused, not silently succeed\");\n    assert!(\n        err.to_string().contains(\"ourself\") || err.to_string().contains(\"our own\"),\n        \"self-dial refused for the documented reason, got: {err}\"\n    );\n}\n\n// [int->REQ-HOST-RUN-1] [int->REQ-RC-1] spt-hosted harness bringup + cross-node\n// attach, end to end: `harnesshost::launch_harness_brokered_in` spawns a harness's\n// `[session.self]` into broker A's PTY (the spt-hosted bringup — the daemon owns\n// the PTY, not an external launcher), then an operator on broker B attaches\n// CROSS-NODE and drives the brought-up PTY: keystrokes reach it, its output\n// streams back. This is the W1 gate proof — \"bring up + attach a real harness\n// endpoint\" over the byte-identical cross-node attach pump; the session outlives\n// the viewport (detach never kills it).\n#[test]\nfn spt_hosted_bringup_then_cross_node_attach_drives_the_pty() {\n    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    // Bring up an spt-hosted harness on A. Its `[session.self]` is a real echo\n    // binary standing in for a harness — the bringup spawns it into broker A's\n    // PTY exactly as it would a `claude` command.\n    #[cfg(unix)]\n    let self_cmd = \"cat\";\n    #[cfg(windows)]\n    let self_cmd = \"findstr .\";\n    let manifest = spt_runtime::Manifest::from_toml_str(&format!(\n        \"[adapter]\\nname = \\\"mock\\\"\\nkind = \\\"harness\\\"\\nversion = \\\"1\\\"\\n\\\n         min_spt_core_version = \\\"0\\\"\\n\\n[session.self]\\ncommand = '{self_cmd}'\\n\"\n    ))\n    .expect(\"harness manifest\");\n    let spawned = spt_daemon::harnesshost::launch_harness_brokered_in(\n        &name_a, \"cc-ep\", \"mock\", \"sess-1\", &manifest, None, false, None, None,\n    )\n    .expect(\"spt-hosted bringup spawns into broker A's PTY\");\n\n    // Operator on B attaches CROSS-NODE to the brought-up session.\n    let mut target = connect_retry(&name_a);\n    let a_addr = target.net_status().expect(\"a status\").addr;\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.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial A\");\n    let stream_b =\n        request_attach(&mut operator, conn.conn_id, spawned.session_id, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\n    operator\n        .net_stream_subscribe(stream_b, 0)\n        .expect(\"operator subscribe\");\n\n    // A serves the inbound attach stream (endpoint = the brought-up id).\n    let (stream_a, origin) = wait_for_stream(&mut target);\n    assert_eq!(origin, b_node, \"serve gate subject is the handshake identity\");\n    let serve_broker = name_a.clone();\n    let server = thread::spawn(move || {\n        let outcome =\n            serve_attach(&mut target, &serve_broker, stream_a, \"cc-ep\", &origin, 0, None).expect(\"serve\");\n        (outcome, target)\n    });\n\n    // Drive the brought-up PTY from B; the echo streams back over the wire.\n    send_attach_input(&mut operator, stream_b, b\"HOSTED_OK\\r\", 1).expect(\"input\");\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\"HOSTED_OK\",\n    );\n\n    // Detach: the brought-up session outlives the viewport.\n    operator\n        .net_stream_send(stream_b, &[], None, true)\n        .expect(\"detach\");\n    let (outcome, mut target) = server.join().expect(\"serve thread\");\n    assert_eq!(outcome, AttachServeOutcome::Detached);\n    target.kill_session().expect(\"cleanup kill\");\n}\n\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]\n…\nfn broker_spawns_the_pty_child_in_the_requested_cwd() {\n…\n    let mut out = Vec::new();\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    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.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\n    let stream_b = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).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    'life1: loop {\n        match life1.read_event().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.send_effect(MintedOp::new(Minter::Rc, op_id), &b).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(&mut life2, &serve_broker, stream_a, \"ling\", &origin2, 0, None)\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    // idempotent truth: a GAP from the restart/replay (a lost record) would leave a\n    // marker off the final screen. MARKER_ONE proves the pre-crash output survived\n    // the handoff + re-serve; MARKER_TWO proves the dead-window buffered input was\n    // delivered after life 2 took over. Rendering (vs a raw substring scan) is what\n    // makes this robust to the mid-stream repaint's clear/repaint bytes.\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(\"MARKER_ONE\"),\n        \"the operator's rendered screen still shows the pre-crash marker across the \\\n         brain restart (no gap in the re-served output): {painted:?}\"\n    );\n    assert!(\n        painted.contains(\"MARKER_TWO\"),\n        \"the operator's rendered screen shows the dead-window marker delivered after \\\n         the handoff (buffered input survived the restart): {painted:?}\"\n    );\n\n    // Ground truth: the session's OWN authoritative screen (a fresh local attach →\n    // repaint) also carries both markers exactly-once at the source.\n    life2.attach(sid, 0).expect(\"local ground-truth attach\");\n    let mut local: Vec<u8> = Vec::new();\n    life2\n        .read_output_until(b\"MARKER_TWO\", &mut local)\n        .expect(\"local replay\");\n    assert!(count(&local, b\"MARKER_ONE\") >= 1 && count(&local, b\"MARKER_TWO\") >= 1);\n\n    life2.kill_session().expect(\"cleanup kill\");\n}\n\n// [unit->REQ-REACH-1] remote-drive detection is a daemon fact: serving a\n// net-attach stamps the endpoint perch's `driven_by` with the\n// handshake-proven origin node (what the agent's `spt api driven-by` query\n// reads), and the marker clears when the viewport ends — the endpoint reads\n// locally driven again.\n#[test]\nfn attach_registers_remote_drive_detection() {\n    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    // The driven endpoint has a real perch on A (unique id: the shared test\n    // home outlives this test).\n    let endpoint = format!(\n        \"ling-d5c-{}-{}\",\n        std::process::id(),\n        SEQ.fetch_add(1, Ordering::Relaxed)\n    );\n    let perch_path =\n        spt_store::perch::resolve_perch_path(&endpoint, spt_store::perch::ParentHint::Infer);\n    std::fs::create_dir_all(&perch_path).expect(\"perch dir\");\n    let rec =\n        spt_store::info::InfoJson::new(&endpoint, \"now\", std::process::id(), \"sess\", \"live_agent\");\n    spt_store::info::write_info(&perch_path, &rec).expect(\"info.json\");\n\n    let mut target = connect_retry(&name_a);\n    // Spawn the session LABELED with this endpoint: driven_by is now broker-owned\n    // (single writer, REQ-RCVIEW-1) and keyed on the session's endpoint label —\n    // which in production equals the serve gate's endpoint (both resolved from\n    // the spawn). So the labeled spawn is the faithful shape.\n    let sid = target\n        .spawn_session(SpawnReq {\n            endpoint: endpoint.clone(),\n            ..echo_spawn_req()\n        })\n        .expect(\"spawn echo child\");\n    let a_addr = target.net_status().expect(\"a status\").addr;\n\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.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\n    let stream_b = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\n    operator\n        .net_stream_subscribe(stream_b, 0)\n        .expect(\"operator subscribe\");\n\n    let (stream_a, origin) = wait_for_stream(&mut target);\n    let endpoint_serve = endpoint.clone();\n    let serve_broker = name_a.clone();\n    let server = thread::spawn(move || {\n        let outcome =\n            serve_attach(&mut target, &serve_broker, stream_a, &endpoint_serve, &origin, 0, None)\n                .expect(\"serve\");\n        (outcome, target)\n    });\n\n    // Drive a marker through: once it renders at B, the serve loop has long\n    // since processed the Request — the stamp is durably on the perch.\n    send_attach_input(&mut operator, stream_b, b\"DETECT_ME\\r\", 1).expect(\"input\");\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\"DETECT_ME\",\n    );\n    let driven = spt_store::info::read_info(&perch_path)\n        .expect(\"info\")\n        .driven_by;\n    assert_eq!(\n        driven.as_deref(),\n        Some(b_node.as_str()),\n        \"the marker IS the handshake-proven origin node\"\n    );\n\n    // Detach: the viewport ends; the endpoint reads locally driven again.\n    operator\n        .net_stream_send(stream_b, &[], None, true)\n        .expect(\"detach\");\n    let (outcome, mut target) = server.join().expect(\"serve thread\");\n    assert_eq!(outcome, AttachServeOutcome::Detached);\n    // The clear is now BROKER-owned (REQ-RCVIEW-1, single writer): the worker's\n    // detach releases the controller slot over IPC, and the broker re-stamps\n    // driven_by=None asynchronously — so poll for it (production clears the same\n    // async way when the worker connection drops).\n    let cleared = (0..200).any(|_| {\n        if spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.driven_by)\n            .is_none()\n        {\n            return true;\n        }\n        std::thread::sleep(std::time::Duration::from_millis(10));\n        false\n    });\n    assert!(cleared, \"viewport over → marker cleared (broker re-stamps None)\");\n    target.kill_session().expect(\"cleanup kill\");\n}\n\n// ── W2.5 controller/viewer model — broker-IPC-level gate evidence ────────────\n// These exercise the OutputLog controller/viewer fan-out directly over the local\n// broker IPC: each `attach_as` carries a distinct `by` identity, so the role"}],"details":{"displayContent":{"text":"//! D5b regression — remote terminal attach (REQ-INST-8): drive a real PTY\n//! session on daemon A from daemon B, byte-stream viewport over the\n//! broker-owned wire; compute stays on A.\n//!\n//! Two in-process brokers (hermetic loopback endpoints, relays disabled) play\n//! the two daemons; a real echo child runs under A's broker PTY. The\n//! invariants:\n//!\n//! - **remote drive**: keystrokes sent from B arrive at A's PTY through the\n//!   journaled input path, and the session's output streams back to B as\n//!   sequenced records — B sees its own marker echoed by A's child;\n//! - **origin is transport truth** (KNOWN-HAZARDS 7.5): the serve loop gates\n//!   on the stream table's handshake-proven `remote_id_hex`;\n//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies\n//!   mid-attach; the broker keeps the QUIC stream, the session, the output\n//!   log, and the effect journal. The successor brain re-serves the SAME\n//!   stream from seq 0 — the worst-case stale cursor: the replayed input\n//!   dedups at the PTY-write journal (typed exactly once), re-transmitted\n//!   output dedups at B's render cursor (rendered exactly once) — including\n//!   input B sent INTO the dead window.\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\nuse std::time::Duration;\n\nuse spt_daemon::attach::{\n    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n    AttachServeOutcome, ControllerIrrecoverablyBehind,\n};\n// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n#[cfg(unix)]\nuse spt_daemon::attach::request_attach_endpoint;\nuse spt_daemon::brain::{Brain, BrainState, BrokerEvent};\nuse spt_daemon::effect::{Minter, MintedOp};\nuse spt_daemon::msg::{decode_bytes, SpawnReq};\nuse spt_daemon::nethost::{NetConfig, NetHost};\nuse spt_daemon::Broker;\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\nuse spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\nuse spt_net::net::ndjson;\nuse spt_proto::identity::Identity;\nuse spt_term::ScreenGrid;\n\n/// Hold the temp home for the whole test binary so it isn't deleted mid-run.\n/// Every test inits it: serve_attach best-effort-stamps the endpoint perch's\n/// `driven_by` marker, and that resolution must never touch a real home.\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n\nfn init_home() {\n    HOME.get_or_init(|| {\n        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n        std::env::set_var(\"SPT_HOME\", dir.path());\n        dir\n    });\n}\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique_name() -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-d5b-{}-{}.sock\", std::process::id(), n)\n}\n\nfn hermetic() -> NetConfig {\n    NetConfig {\n        identity: Identity::generate(),\n        relay: RelayPolicy::Disabled,\n        discovery: LocalDiscovery::Off,\n        scope: BindScope::Loopback,\n        membership: None,\n    }\n}\n\nfn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n    let host = NetHost::start(hermetic()).expect(\"net host start\");\n    let broker =\n        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n    broker\n}\n\nfn connect_retry(name: &str) -> Brain {\n    for _ in 0..300 {\n        if let Ok(b) = Brain::cold_start(name, 1) {\n            return b;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"brain could not connect\");\n}\n\nfn echo_spawn_req() -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n    #[cfg(windows)]\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n    SpawnReq {\n        program,\n        args,\n        rows: 24,\n        cols: 80,\n        endpoint: String::new(),\n        cwd: None,\n        env: Default::default(),\n        translation_binary: None,\n        adapter: String::new(),\n        install_dir: None,\n    }\n}\n\n/// Poll the brain's `net-streams` until a PEER-initiated stream appears; return\n/// `(stream_id, remote_id_hex)`. Filtering on `!initiated_locally` is what the\n/// production dispatch loop does (dispatch.rs) — and it is REQUIRED for the\n/// single-broker loopback case, where the table holds BOTH the operator's own\n/// row and the cross-wired peer row: the serve target is always the peer row.\nfn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n    for _ in 0..400 {\n        let reply = brain.net_streams().expect(\"net-streams\");\n        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {\n            return (s.stream_id, s.remote_id_hex.clone());\n        }\n        thread::sleep(Duration::from_millis(5));\n    }\n    panic!(\"attach stream never appeared at the target's broker\");\n}\n\n/// The operator+serve drive shared by EVERY attach transport — the M12 W1.5\n/// one-pump invariant made STRUCTURAL: cross-node (QUIC conn) and local\n/// (broker loopback conn) call this identically; they diverge ONLY in how\n/// `conn_id` was acquired (`net_dial` vs `net_dial_loopback`) and in\n/// `expected_origin`, never in the `serve_attach`+render pump below. There is no\n/// second code path that could drift.\n///\n/// Given an acquired `conn_id` on `operator` and the `target` broker connection\n/// holding `session_id` for `endpoint`: open the attach stream, serve it on\n/// `target` under `expected_origin` (asserted == the stream table's proven\n/// origin), drive `marker` through the PTY and render it back over the wire,\n/// then detach and confirm the serve loop ended `Detached` (session outlives).\n/// Returns the rendered viewport bytes + the (still-live) target brain so the\n/// caller can assert session survival and clean up.\n#[allow(clippy::too_many_arguments)]\nfn attach_drive_detach(\n    operator: &mut Brain,\n    target_broker: &str,\n    mut target: Brain,\n    conn_id: u64,\n    session_id: u64,\n    endpoint: &str,\n    expected_origin: &str,\n    marker: &[u8],\n) -> (Vec<u8>, Brain) {\n    let stream_b = request_attach(operator, conn_id, session_id, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\n    operator\n        .net_stream_subscribe(stream_b, 0)\n        .expect(\"operator subscribe\");\n\n    // The target serves the inbound attach stream under the proven origin\n    // (KH 7.5 — the stream table, never payload). Cross-node: the handshake id.\n    // Loopback: this node's own id (broker-minted), which gates same-node.\n    let (stream_a, origin) = wait_for_stream(&mut target);\n    assert_eq!(\n        origin, expected_origin,\n        \"serve gate subject IS the stream table's proven origin\"\n    );\n    let endpoint_owned = endpoint.to_string();\n    let broker_owned = target_broker.to_string();\n    let server = thread::spawn(move || {\n        let outcome =\n            serve_attach(&mut target, &broker_owned, stream_a, &endpoint_owned, &origin, 0, None)\n                .expect(\"serve\");\n        (outcome, target)\n    });\n\n    // Drive the marker through; the echo streams back over the SAME pump.\n    let mut keys = marker.to_vec();\n    keys.push(b'\\r');\n    send_attach_input(operator, stream_b, &keys, 1).expect(\"input\");\n    let mut decoder = AttachDecoder::new();\n    let (mut cursor, mut viewport) = (0u64, Vec::new());\n    render_until(\n        operator,\n        stream_b,\n        &mut decoder,\n        &mut cursor,\n        &mut viewport,\n        marker,\n    );\n\n    // Detach: the viewport ends with the session still running.\n    operator\n        .net_stream_send(stream_b, &[], None, true)\n        .expect(\"detach\");\n    let (outcome, target) = server.join().expect(\"serve thread\");\n    assert_eq!(outcome, AttachServeOutcome::Detached);\n    (viewport, target)\n}\n\n/// Operator-side viewport: consume attach-stream records, dedup `Output` by\n/// seq, append rendered bytes to `buf`, until `needle` is rendered. Returns\n/// the rendered-through cursor (last accepted seq + 1).\nfn render_until(\n    brain: &mut Brain,\n    stream_id: u64,\n    decoder: &mut AttachDecoder,\n    next_seq: &mut u64,\n    buf: &mut Vec<u8>,\n    needle: &[u8],\n) {\n    let contains = |hay: &[u8], needle: &[u8]| hay.windows(needle.len()).any(|w| w == needle);\n    while !contains(buf, needle) {\n        match brain.read_event().expect(\"operator read event\") {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    if let AttachRecord::Output { seq, data_b64 } = rec {\n                        if seq < *next_seq {\n                            continue; // replay duplicate — dedup by cursor\n                        }\n                        // #6 (ADR-0031): the initial batch is now a single synthesized\n                        // repaint at pseudo-seq `log.next_seq - 1`, so the FIRST frame a\n                        // fresh (from_seq=0) attach delivers is not seq 0 — it baselines\n                        // the cursor wherever the current screen sits. Every subsequent\n                        // live frame must then be contiguous (no forward gap).\n                        if *next_seq != 0 {\n                            assert_eq!(seq, *next_seq, \"no forward gap in the viewport\");\n                        }\n                        *next_seq = seq + 1;\n                        buf.extend_from_slice(&decode_bytes(&data_b64).expect(\"chunk b64\"));\n                    }\n                }\n            }\n            BrokerEvent::NetStreamEof { .. } => panic!(\"stream ended before the marker rendered\"),\n            BrokerEvent::Error { message } => panic!(\"operator broker error: {message}\"),\n            _ => {}\n        }\n    }\n}\n\n/// Count non-overlapping occurrences of `needle` in `hay`.\nfn count(hay: &[u8], needle: &[u8]) -> usize {\n    if needle.is_empty() || hay.len() < needle.len() {\n        return 0;\n    }\n    let (mut n, mut i) = (0, 0);\n    while i + needle.len() <= hay.len() {\n        if &hay[i..i + needle.len()] == needle {\n            n += 1;\n            i += needle.len();\n        } else {\n            i += 1;\n        }\n    }\n    n\n}\n\n// [int->REQ-RC-1] loopback self-dial is REFUSED by the transport — pins the\n// finding that drives `spt rc`'s LOCAL transport choice. A node cannot QUIC-dial\n// its own node id (iroh: \"Connecting to ourself is not supported\"), so local\n// attach CANNOT ride a self-dialed QUIC conn. The one-pump invariant therefore\n// holds via the fallback transport (a broker-local loopback conn feeding the same\n// `serve_attach` pump), NOT a self-dial — exactly the doyle-approved fallback.\n// Cross-node attach (the prize) is unaffected: it dials a real remote peer.\n#[test]\nfn loopback_self_dial_is_refused_local_uses_fallback_transport() {\n    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    let mut brain = connect_retry(&name);\n\n    let status = brain.net_status().expect(\"status\");\n    // Dialing our OWN advertised address is refused at the transport — fail-fast,\n    // never a deadlock; local attach must not be built on a self-dial.\n    let err = brain\n        .net_dial(status.addr, Some(MintedOp::new(Minter::Cli, 1)))\n        .expect_err(\"self-dial must be refused, not silently succeed\");\n    assert!(\n        err.to_string().contains(\"ourself\") || err.to_string().contains(\"our own\"),\n        \"self-dial refused for the documented reason, got: {err}\"\n    );\n}\n\n// [int->REQ-HOST-RUN-1] [int->REQ-RC-1] spt-hosted harness bringup + cross-node\n// attach, end to end: `harnesshost::launch_harness_brokered_in` spawns a harness's\n// `[session.self]` into broker A's PTY (the spt-hosted bringup — the daemon owns\n// the PTY, not an external launcher), then an operator on broker B attaches\n// CROSS-NODE and drives the brought-up PTY: keystrokes reach it, its output\n// streams back. This is the W1 gate proof — \"bring up + attach a real harness\n// endpoint\" over the byte-identical cross-node attach pump; the session outlives\n// the viewport (detach never kills it).\n#[test]\nfn spt_hosted_bringup_then_cross_node_attach_drives_the_pty() {\n    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    // Bring up an spt-hosted harness on A. Its `[session.self]` is a real echo\n    // binary standing in for a harness — the bringup spawns it into broker A's\n    // PTY exactly as it would a `claude` command.\n    #[cfg(unix)]\n    let self_cmd = \"cat\";\n    #[cfg(windows)]\n    let self_cmd = \"findstr .\";\n    let manifest = spt_runtime::Manifest::from_toml_str(&format!(\n        \"[adapter]\\nname = \\\"mock\\\"\\nkind = \\\"harness\\\"\\nversion = \\\"1\\\"\\n\\\n         min_spt_core_version = \\\"0\\\"\\n\\n[session.self]\\ncommand = '{self_cmd}'\\n\"\n    ))\n    .expect(\"harness manifest\");\n    let spawned = spt_daemon::harnesshost::launch_harness_brokered_in(\n        &name_a, \"cc-ep\", \"mock\", \"sess-1\", &manifest, None, false, None, None,\n    )\n    .expect(\"spt-hosted bringup spawns into broker A's PTY\");\n\n    // Operator on B attaches CROSS-NODE to the brought-up session.\n    let mut target = connect_retry(&name_a);\n    let a_addr = target.net_status().expect(\"a status\").addr;\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.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial A\");\n    let stream_b =\n        request_attach(&mut operator, conn.conn_id, spawned.session_id, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\n    operator\n        .net_stream_subscribe(stream_b, 0)\n        .expect(\"operator subscribe\");\n\n    // A serves the inbound attach stream (endpoint = the brought-up id).\n    let (stream_a, origin) = wait_for_stream(&mut target);\n    assert_eq!(origin, b_node, \"serve gate subject is the handshake identity\");\n    let serve_broker = name_a.clone();\n    let server = thread::spawn(move || {\n        let outcome =\n            serve_attach(&mut target, &serve_broker, stream_a, \"cc-ep\", &origin, 0, None).expect(\"serve\");\n        (outcome, target)\n    });\n\n    // Drive the brought-up PTY from B; the echo streams back over the wire.\n    send_attach_input(&mut operator, stream_b, b\"HOSTED_OK\\r\", 1).expect(\"input\");\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\"HOSTED_OK\",\n    );\n\n    // Detach: the brought-up session outlives the viewport.\n    operator\n        .net_stream_send(stream_b, &[], None, true)\n        .expect(\"detach\");\n    let (outcome, mut target) = server.join().expect(\"serve thread\");\n    assert_eq!(outcome, AttachServeOutcome::Detached);\n    target.kill_session().expect(\"cleanup kill\");\n}\n\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]\n…\nfn broker_spawns_the_pty_child_in_the_requested_cwd() {\n…\n    let mut out = Vec::new();\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    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.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\n    let stream_b = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).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    'life1: loop {\n        match life1.read_event().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.send_effect(MintedOp::new(Minter::Rc, op_id), &b).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(&mut life2, &serve_broker, stream_a, \"ling\", &origin2, 0, None)\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    // idempotent truth: a GAP from the restart/replay (a lost record) would leave a\n    // marker off the final screen. MARKER_ONE proves the pre-crash output survived\n    // the handoff + re-serve; MARKER_TWO proves the dead-window buffered input was\n    // delivered after life 2 took over. Rendering (vs a raw substring scan) is what\n    // makes this robust to the mid-stream repaint's clear/repaint bytes.\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(\"MARKER_ONE\"),\n        \"the operator's rendered screen still shows the pre-crash marker across the \\\n         brain restart (no gap in the re-served output): {painted:?}\"\n    );\n    assert!(\n        painted.contains(\"MARKER_TWO\"),\n        \"the operator's rendered screen shows the dead-window marker delivered after \\\n         the handoff (buffered input survived the restart): {painted:?}\"\n    );\n\n    // Ground truth: the session's OWN authoritative screen (a fresh local attach →\n    // repaint) also carries both markers exactly-once at the source.\n    life2.attach(sid, 0).expect(\"local ground-truth attach\");\n    let mut local: Vec<u8> = Vec::new();\n    life2\n        .read_output_until(b\"MARKER_TWO\", &mut local)\n        .expect(\"local replay\");\n    assert!(count(&local, b\"MARKER_ONE\") >= 1 && count(&local, b\"MARKER_TWO\") >= 1);\n\n    life2.kill_session().expect(\"cleanup kill\");\n}\n\n// [unit->REQ-REACH-1] remote-drive detection is a daemon fact: serving a\n// net-attach stamps the endpoint perch's `driven_by` with the\n// handshake-proven origin node (what the agent's `spt api driven-by` query\n// reads), and the marker clears when the viewport ends — the endpoint reads\n// locally driven again.\n#[test]\nfn attach_registers_remote_drive_detection() {\n    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    // The driven endpoint has a real perch on A (unique id: the shared test\n    // home outlives this test).\n    let endpoint = format!(\n        \"ling-d5c-{}-{}\",\n        std::process::id(),\n        SEQ.fetch_add(1, Ordering::Relaxed)\n    );\n    let perch_path =\n        spt_store::perch::resolve_perch_path(&endpoint, spt_store::perch::ParentHint::Infer);\n    std::fs::create_dir_all(&perch_path).expect(\"perch dir\");\n    let rec =\n        spt_store::info::InfoJson::new(&endpoint, \"now\", std::process::id(), \"sess\", \"live_agent\");\n    spt_store::info::write_info(&perch_path, &rec).expect(\"info.json\");\n\n    let mut target = connect_retry(&name_a);\n    // Spawn the session LABELED with this endpoint: driven_by is now broker-owned\n    // (single writer, REQ-RCVIEW-1) and keyed on the session's endpoint label —\n    // which in production equals the serve gate's endpoint (both resolved from\n    // the spawn). So the labeled spawn is the faithful shape.\n    let sid = target\n        .spawn_session(SpawnReq {\n            endpoint: endpoint.clone(),\n            ..echo_spawn_req()\n        })\n        .expect(\"spawn echo child\");\n    let a_addr = target.net_status().expect(\"a status\").addr;\n\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.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\n    let stream_b = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\n    operator\n        .net_stream_subscribe(stream_b, 0)\n        .expect(\"operator subscribe\");\n\n    let (stream_a, origin) = wait_for_stream(&mut target);\n    let endpoint_serve = endpoint.clone();\n    let serve_broker = name_a.clone();\n    let server = thread::spawn(move || {\n        let outcome =\n            serve_attach(&mut target, &serve_broker, stream_a, &endpoint_serve, &origin, 0, None)\n                .expect(\"serve\");\n        (outcome, target)\n    });\n\n    // Drive a marker through: once it renders at B, the serve loop has long\n    // since processed the Request — the stamp is durably on the perch.\n    send_attach_input(&mut operator, stream_b, b\"DETECT_ME\\r\", 1).expect(\"input\");\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\"DETECT_ME\",\n    );\n    let driven = spt_store::info::read_info(&perch_path)\n        .expect(\"info\")\n        .driven_by;\n    assert_eq!(\n        driven.as_deref(),\n        Some(b_node.as_str()),\n        \"the marker IS the handshake-proven origin node\"\n    );\n\n    // Detach: the viewport ends; the endpoint reads locally driven again.\n    operator\n        .net_stream_send(stream_b, &[], None, true)\n        .expect(\"detach\");\n    let (outcome, mut target) = server.join().expect(\"serve thread\");\n    assert_eq!(outcome, AttachServeOutcome::Detached);\n    // The clear is now BROKER-owned (REQ-RCVIEW-1, single writer): the worker's\n    // detach releases the controller slot over IPC, and the broker re-stamps\n    // driven_by=None asynchronously — so poll for it (production clears the same\n    // async way when the worker connection drops).\n    let cleared = (0..200).any(|_| {\n        if spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.driven_by)\n            .is_none()\n        {\n            return true;\n        }\n        std::thread::sleep(std::time::Duration::from_millis(10));\n        false\n    });\n    assert!(cleared, \"viewport over → marker cleared (broker re-stamps None)\");\n    target.kill_session().expect(\"cleanup kill\");\n}\n\n// ── W2.5 controller/viewer model — broker-IPC-level gate evidence ────────────\n// These exercise the OutputLog controller/viewer fan-out directly over the local\n// broker IPC: each `attach_as` carries a distinct `by` identity, so the role","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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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,null,721,null,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,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,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,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\attach.rs"}}},"isError":false,"timestamp":1784195854269}}
{"type":"message","id":"c424d172","parentId":"bfc26850","timestamp":"2026-07-16T09:57:34.276Z","message":{"role":"toolResult","toolCallId":"call_kyaaHpoWPeD67kGnoDnogLSd|fc_047bb49263dfa303016a58ab0d2a948198a22494962e36e399","toolName":"read","content":[{"type":"text","text":"//! Wave 2 (v0.12.1) **L2** — REQ-HAZARD-ATTACH-WEDGE robustness gate, on the REAL\n//! dummy-harness fixture + a REAL detached `spt daemon run` (no mocks, no in-proc\n//! `reconcile_once`). Companion to `dummy_harness_e2e.rs` (L0 keystone bring-up).\n//!\n//! The hazard (v0.12.0 real-harness finding): a *legitimately* dead PTY child (real\n//! crash/kill) plus an operator pump that was DROPPED without a clean detach (closed\n//! tab / `taskkill /F`) must NOT wedge the broker — every NEW attach / `endpoint run`\n//! must still be served, the dead endpoint must be offlined within a reconcile tick,\n//! and `daemon stop` must complete bounded.\n//!\n//! ## What this proves (prove-don't-change, like the M11-W4 capstone)\n//!\n//! The original B-side root hypothesized the broker's loopback forward (`serve_attach`\n//! → `send_stream` → a bounded 64 KB `tokio::io::duplex`) blocks forever on a dead\n//! operator and parks the 2-worker net runtime. On the post-L0 code that mechanism does\n//! NOT hold: the loopback duplex is drained broker-INTERNALLY by the operator row's own\n//! read pump (`nethost.rs` `RecvHalf::Loopback`), which for an ordinary attach stream\n//! (`retentive_cap == 0`) never parks — so `peer_w` never backs up on a dead `rc`; a\n//! dead `rc` is merely a dropped IPC subscriber against a bounded (evicting) ring. The\n//! `BROKER-QUIC-DEADLINE` bound (`bounded_block_on`, 10 s) is a further backstop on any\n//! residual loopback write. This test is the faithful real-daemon proof that the wedge\n//! does not occur — and folds in L3 (dead endpoint offlined within one\n//! `LIVE_RECONCILE_INTERVAL_MS` tick) and L4 (`daemon stop` bounded).\n//!\n//! All spawned pids are reaped SCOPED (never machine-wide — shared CI runner).\n\nuse std::io::Read;\nuse std::path::{Path, PathBuf};\nuse std::process::{Child, Command, Output, Stdio};\nuse std::time::{Duration, Instant};\n\nmod common;\nuse common::CommandNoWindowExt;\n\nuse spt_store::liveness::{STATUS_OFFLINE, STATUS_ONLINE};\nuse spt_store::perch::{self, ParentHint};\n\nfn kill_pid(pid: u32) {\n    #[cfg(windows)]\n    let _ = Command::new(\"taskkill\")\n        .no_window()\n        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n        .output();\n    #[cfg(unix)]\n    let _ = Command::new(\"kill\").args([\"-9\", &pid.to_string()]).output();\n}\n\nfn output_bounded(mut cmd: Command, deadline: Duration) -> Output {\n    let (tx, rx) = std::sync::mpsc::channel();\n    std::thread::spawn(move || {\n        let _ = tx.send(cmd.output());\n    });\n    rx.recv_timeout(deadline)\n        .expect(\"captured spt call must complete within its deadline\")\n        .expect(\"run spt\")\n}\n\nfn ready_pid(path: &Path) -> Option<u32> {\n    let s = std::fs::read_to_string(path).ok()?;\n    serde_json::from_str::<serde_json::Value>(&s)\n        .ok()?\n        .get(\"pid\")?\n        .as_u64()\n        .map(|p| p as u32)\n}\n\nfn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {\n    let deadline = Instant::now() + budget;\n    while Instant::now() < deadline {\n        if let Some(p) = ready_pid(path) {\n            return Some(p);\n        }\n        std::thread::sleep(Duration::from_millis(50));\n    }\n    None\n}\n\nfn sibling_bin(name: &str) -> PathBuf {\n    PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"))\n        .with_file_name(format!(\"{name}{}\", std::env::consts::EXE_SUFFIX))\n}\n\n/// `endpoint run --start <id>` against the registered dummyharness adapter; returns\n/// the captured output and the broker-spawned harness pid (parsed from the terse\n/// `ENDPOINT_RUN:… pid=NNN` stderr machine token — the long-lived PTY child).\nfn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {\n    let mut cmd = Command::new(spt_bin);\n    cmd.no_window()\n        .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id, \"--start\"])\n        .env(\"SPT_HOME\", home);\n    let out = output_bounded(cmd, Duration::from_secs(45));\n    let stderr = String::from_utf8_lossy(&out.stderr).to_string();\n    let pid = stderr\n        .split(\"pid=\")\n        .nth(1)\n        .and_then(|s| s.split_whitespace().next())\n        .and_then(|s| s.trim().parse().ok());\n    (out, pid)\n}\n\n/// Poll `<id>`'s self-perch status until it equals `want` or the budget elapses.\nfn wait_for_status(id: &str, want: &str, budget: Duration) -> bool {\n    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\n    let deadline = Instant::now() + budget;\n    while Instant::now() < deadline {\n        let now = spt_store::info::read_info(&perch).and_then(|i| i.status);\n        if now.as_deref() == Some(want) {\n            return true;\n        }\n        std::thread::sleep(Duration::from_millis(150));\n    }\n    false\n}\n\n/// Attach `rc <id>` and return whether its `DUMMY_HARNESS_TICK` output arrived within\n/// the window — the strong \"this endpoint is being SERVED\" proof. The rc child is then\n/// reaped ABRUPTLY (`kill()` = TerminateProcess, no clean detach record) — exactly the\n/// closed-tab / crashed-operator case (a DROPPED pump), which is what makes this the\n/// L2 wedge trigger when called on the victim. The reader thread observes the killed\n/// pipe (read → 0/Err) and exits.\nfn rc_sees_tick(spt_bin: &Path, home: &Path, id: &str, window: Duration) -> bool {\n    let mut rc: Child = Command::new(spt_bin)\n        .no_window()\n        .args([\"rc\", id])\n        .env(\"SPT_HOME\", home)\n        .stdout(Stdio::piped())\n        .stderr(Stdio::null())\n        .stdin(Stdio::piped()) // hold stdin open so the pump does not EOF-detach early\n        .spawn()\n        .expect(\"spawn spt rc <id>\");\n    let _rc_stdin = rc.stdin.take();\n    let mut rc_out = rc.stdout.take().unwrap();\n    let (tx, rx) = std::sync::mpsc::channel::<bool>();\n    let reader = std::thread::spawn(move || {\n        let mut buf = [0u8; 4096];\n        let mut acc = String::new();\n        loop {\n            match rc_out.read(&mut buf) {\n                Ok(0) => break,\n                Ok(n) => {\n                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\n                    if acc.contains(\"DUMMY_HARNESS_TICK\") {\n                        let _ = tx.send(true);\n                        break;\n                    }\n                }\n                Err(_) => break,\n            }\n        }\n        let _ = tx.send(false);\n    });\n    let saw = rx.recv_timeout(window).unwrap_or(false);\n    // Abrupt reap = the dropped-pump wedge trigger (no clean detach). `kill()` is\n    // idempotent on an already-exited child; `wait()` reaps the handle (clippy\n    // zombie_processes).\n    let _ = rc.kill();\n    let _ = rc.wait();\n    let _ = reader.join();\n    saw\n}\n\n#[test]\nfn attach_wedge_dead_child_plus_dropped_pump_does_not_wedge_the_broker() {\n    // ── (1) Isolated, NET-ENABLED SPT_HOME (rc attaches over a broker loopback conn\n    //    that surfaces as net-stream events — needs a net-bound broker). ──\n    let home = tempfile::tempdir().unwrap();\n    std::env::set_var(\"SPT_HOME\", home.path());\n\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n    let exe_suffix = spt_bin\n        .extension()\n        .map(|e| format!(\".{}\", e.to_string_lossy()))\n        .unwrap_or_default();\n    let mock_session = sibling_bin(\"mock-session\");\n    assert!(\n        mock_session.exists(),\n        \"the dummy-harness program must be built: {} \\\n         (pre-build: cargo build -p mock-adapter --bin mock-session)\",\n        mock_session.display()\n    );\n\n    // ── (2) Register the DUMMY-HARNESS adapter (same shape as dummy_harness_e2e). ──\n    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\\\', \"/\");\n    let mock_for_cmd = mock_session.to_string_lossy().replace('\\\\', \"/\");\n    let self_cmd = format!(\n        \"{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \\\n         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200\"\n    );\n    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\n    std::fs::create_dir_all(&src).unwrap();\n    let psyche_bin = src.join(format!(\"psychebin{exe_suffix}\"));\n    std::fs::copy(&spt_bin, &psyche_bin).expect(\"stage the psyche binary fixture\");\n    let drops = home.path().join(\"drops\");\n    std::fs::create_dir_all(&drops).unwrap();\n    let dir = drops.to_string_lossy().replace('\\\\', \"/\");\n    let manifest_toml = format!(\n        \"[adapter]\\nname=\\\"dummyharness\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\n         min_spt_core_version=\\\"0\\\"\\n\\n[session.self]\\ncommand='{self_cmd}'\\n\\n\\\n         [session]\\ncommune_dir=\\\"{dir}\\\"\\nsignoff_dir=\\\"{dir}\\\"\\n\\n\\\n         [session.psyche_init]\\ncommand='psychebin ready {{id}}'\\ncwd=\\\"{{psyche_dir}}\\\"\\nkeys=[]\\n\"\n    );\n    std::fs::write(src.join(\"manifest.toml\"), &manifest_toml).unwrap();\n    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\n\n    // ── (3) The REAL daemon broker process. ──\n    let brain_log = home.path().join(\"brain.stderr.log\");\n    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\n    let mut broker: Child = Command::new(&spt_bin)\n        .no_window()\n        .args([\"daemon\", \"run\"])\n        .env(\"SPT_HOME\", home.path())\n        .stdout(Stdio::null())\n        .stderr(Stdio::from(brain_log_file))\n        .spawn()\n        .expect(\"spawn spt daemon run (broker process)\");\n    let brain_pid = match wait_for_ready_pid(&home.path().join(\"brain.ready\"), Duration::from_secs(30))\n    {\n        Some(p) => p,\n        None => {\n            let _ = broker.kill();\n            let _ = broker.wait();\n            panic!(\n                \"PRECONDITION: brain never came up.\\n{}\",\n                std::fs::read_to_string(&brain_log).unwrap_or_default()\n            );\n        }\n    };\n\n    // ── (4) Bring up the VICTIM endpoint and prove it is SERVED (rc sees the tick). ──\n    let victim = \"wedge1\";\n    let (run1, harness_pid1) = endpoint_run(&spt_bin, home.path(), victim);\n    let victim_online = wait_for_status(victim, STATUS_ONLINE, Duration::from_secs(20));\n    // rc_sees_tick reaps its rc ABRUPTLY before returning = the dropped-pump half of\n    // the wedge trigger (no clean detach record).\n    let victim_served = rc_sees_tick(&spt_bin, home.path(), victim, Duration::from_secs(10));\n\n    // ── (5) THE WEDGE TRIGGER (second half): kill the harness PTY child abruptly (a\n    //    legitimately dead child). Together with the just-dropped operator pump this is\n    //    exactly the closed-tab + crash case — no clean teardown, no detach record. ──\n    if let Some(p) = harness_pid1 {\n        kill_pid(p);\n    }\n\n    // ── (6) L2 — the broker is NOT wedged: a brand-NEW endpoint still comes up online\n    //    AND is served (rc sees its tick) within bounded time. If the dead-child +\n    //    dropped-pump had parked the net runtime, this bring-up would time out. ──\n    let fresh = \"wedge2\";\n    let (run2, harness_pid2) = endpoint_run(&spt_bin, home.path(), fresh);\n    let fresh_online = wait_for_status(fresh, STATUS_ONLINE, Duration::from_secs(20));\n    let fresh_served = rc_sees_tick(&spt_bin, home.path(), fresh, Duration::from_secs(10));\n\n    // ── (7) L3 — the dead victim endpoint is OFFLINED within one reconcile tick\n    //    (LIVE_RECONCILE_INTERVAL_MS = 5 s; allow a generous window). The broker's\n    //    exit-waiter reaps the dead session from its table, and the brain's\n    //    reconcile_hosted_liveness clears the stale status=online latch. ──\n    let victim_offlined = wait_for_status(victim, STATUS_OFFLINE, Duration::from_secs(20));\n\n    // ── (8) L4 — daemon stop completes BOUNDED. ──\n    let stop = {\n        let mut cmd = Command::new(&spt_bin);\n        // `--force`: `wedge2` is a live hosted session by design, so the W3\n        // STOP-LIVE-SESSION-WARN contract refuses a plain stop — this teardown\n        // intends to kill it. [int->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\n        cmd.no_window().args([\"daemon\", \"stop\", \"--force\"]).env(\"SPT_HOME\", home.path());\n        output_bounded(cmd, Duration::from_secs(20))\n    };\n\n    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();\n    eprintln!(\n        \"=== DIAGNOSTIC: run1_ok={} victim_online={victim_online} victim_served={victim_served} \\\n         harness_pid1={harness_pid1:?} | run2_ok={} fresh_online={fresh_online} \\\n         fresh_served={fresh_served} | victim_offlined={victim_offlined} stop_ok={} ===\\n\\\n         === brain stderr ===\\n{brain_stderr}\",\n        run1.status.success(),\n        run2.status.success(),\n        stop.status.success(),\n    );\n\n    // ── (9) Reap scoped: both harness children + any hosted psyches + brain + broker\n    //    (both rc operators were already reaped inside rc_sees_tick). Never\n    //    machine-wide. ──\n    if let Some(p) = harness_pid2 {\n        kill_pid(p);\n    }\n    for id in [victim, fresh] {\n        let psyche_perch =\n            perch::resolve_perch_path(&format!(\"{id}-psyche\"), ParentHint::Explicit(id));\n        if let Some(p) = spt_store::info::read_pid(&psyche_perch) {\n            kill_pid(p);\n        }\n    }\n    kill_pid(brain_pid);\n    let _ = broker.kill();\n    let _ = broker.wait();\n\n    // ── Assertions. ──\n    assert!(victim_online, \"PRECONDITION: the victim endpoint must come ONLINE\");\n    assert!(\n        victim_served,\n        \"PRECONDITION: the victim must be SERVED (rc saw its tick) before we wedge it\"\n    );\n    // L2 keystone: the broker survived the dead-child + dropped-pump and still serves.\n    // [int->REQ-HAZARD-ATTACH-WEDGE]\n    assert!(\n        fresh_online,\n        \"after a dead PTY child + a dropped operator pump, a NEW `endpoint run` must \\\n         still come ONLINE — the broker must not be wedged.\\n=== brain stderr ===\\n{brain_stderr}\"\n    );\n    assert!(\n        fresh_served,\n        \"after the wedge trigger, a NEW `rc` attach must be SERVED (saw its tick) — proof \\\n         the net runtime was not parked by the dead loopback peer.\\n=== brain stderr ===\\n{brain_stderr}\"\n    );\n    // L3 (folded): the dead endpoint is offlined within a reconcile tick.\n    // [int->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\n    assert!(\n        victim_offlined,\n        \"the dead victim endpoint must be OFFLINED within a reconcile tick (its broker \\\n         session is reaped on child death; reconcile_hosted_liveness clears the latch).\\n\\\n         === brain stderr ===\\n{brain_stderr}\"\n    );\n    // L4 (folded): daemon stop is bounded (the output_bounded above would have panicked\n    // on timeout); assert it also succeeded cleanly.\n    assert!(\n        stop.status.success(),\n        \"`daemon stop` must complete bounded + clean after the wedge trigger: {}\",\n        String::from_utf8_lossy(&stop.stderr)\n    );\n}\n"}],"details":{"displayContent":{"text":"//! Wave 2 (v0.12.1) **L2** — REQ-HAZARD-ATTACH-WEDGE robustness gate, on the REAL\r\n//! dummy-harness fixture + a REAL detached `spt daemon run` (no mocks, no in-proc\r\n//! `reconcile_once`). Companion to `dummy_harness_e2e.rs` (L0 keystone bring-up).\r\n//!\r\n//! The hazard (v0.12.0 real-harness finding): a *legitimately* dead PTY child (real\r\n//! crash/kill) plus an operator pump that was DROPPED without a clean detach (closed\r\n//! tab / `taskkill /F`) must NOT wedge the broker — every NEW attach / `endpoint run`\r\n//! must still be served, the dead endpoint must be offlined within a reconcile tick,\r\n//! and `daemon stop` must complete bounded.\r\n//!\r\n//! ## What this proves (prove-don't-change, like the M11-W4 capstone)\r\n//!\r\n//! The original B-side root hypothesized the broker's loopback forward (`serve_attach`\r\n//! → `send_stream` → a bounded 64 KB `tokio::io::duplex`) blocks forever on a dead\r\n//! operator and parks the 2-worker net runtime. On the post-L0 code that mechanism does\r\n//! NOT hold: the loopback duplex is drained broker-INTERNALLY by the operator row's own\r\n//! read pump (`nethost.rs` `RecvHalf::Loopback`), which for an ordinary attach stream\r\n//! (`retentive_cap == 0`) never parks — so `peer_w` never backs up on a dead `rc`; a\r\n//! dead `rc` is merely a dropped IPC subscriber against a bounded (evicting) ring. The\r\n//! `BROKER-QUIC-DEADLINE` bound (`bounded_block_on`, 10 s) is a further backstop on any\r\n//! residual loopback write. This test is the faithful real-daemon proof that the wedge\r\n//! does not occur — and folds in L3 (dead endpoint offlined within one\r\n//! `LIVE_RECONCILE_INTERVAL_MS` tick) and L4 (`daemon stop` bounded).\r\n//!\r\n//! All spawned pids are reaped SCOPED (never machine-wide — shared CI runner).\r\n\r\nuse std::io::Read;\r\nuse std::path::{Path, PathBuf};\r\nuse std::process::{Child, Command, Output, Stdio};\r\nuse std::time::{Duration, Instant};\r\n\r\nmod common;\r\nuse common::CommandNoWindowExt;\r\n\r\nuse spt_store::liveness::{STATUS_OFFLINE, STATUS_ONLINE};\r\nuse spt_store::perch::{self, ParentHint};\r\n\r\nfn kill_pid(pid: u32) {\r\n    #[cfg(windows)]\r\n    let _ = Command::new(\"taskkill\")\r\n        .no_window()\r\n        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\r\n        .output();\r\n    #[cfg(unix)]\r\n    let _ = Command::new(\"kill\").args([\"-9\", &pid.to_string()]).output();\r\n}\r\n\r\nfn output_bounded(mut cmd: Command, deadline: Duration) -> Output {\r\n    let (tx, rx) = std::sync::mpsc::channel();\r\n    std::thread::spawn(move || {\r\n        let _ = tx.send(cmd.output());\r\n    });\r\n    rx.recv_timeout(deadline)\r\n        .expect(\"captured spt call must complete within its deadline\")\r\n        .expect(\"run spt\")\r\n}\r\n\r\nfn ready_pid(path: &Path) -> Option<u32> {\r\n    let s = std::fs::read_to_string(path).ok()?;\r\n    serde_json::from_str::<serde_json::Value>(&s)\r\n        .ok()?\r\n        .get(\"pid\")?\r\n        .as_u64()\r\n        .map(|p| p as u32)\r\n}\r\n\r\nfn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {\r\n    let deadline = Instant::now() + budget;\r\n    while Instant::now() < deadline {\r\n        if let Some(p) = ready_pid(path) {\r\n            return Some(p);\r\n        }\r\n        std::thread::sleep(Duration::from_millis(50));\r\n    }\r\n    None\r\n}\r\n\r\nfn sibling_bin(name: &str) -> PathBuf {\r\n    PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"))\r\n        .with_file_name(format!(\"{name}{}\", std::env::consts::EXE_SUFFIX))\r\n}\r\n\r\n/// `endpoint run --start <id>` against the registered dummyharness adapter; returns\r\n/// the captured output and the broker-spawned harness pid (parsed from the terse\r\n/// `ENDPOINT_RUN:… pid=NNN` stderr machine token — the long-lived PTY child).\r\nfn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {\r\n    let mut cmd = Command::new(spt_bin);\r\n    cmd.no_window()\r\n        .args([\"endpoint\", \"run\", \"--adapter\", \"dummyharness\", \"--id\", id, \"--start\"])\r\n        .env(\"SPT_HOME\", home);\r\n    let out = output_bounded(cmd, Duration::from_secs(45));\r\n    let stderr = String::from_utf8_lossy(&out.stderr).to_string();\r\n    let pid = stderr\r\n        .split(\"pid=\")\r\n        .nth(1)\r\n        .and_then(|s| s.split_whitespace().next())\r\n        .and_then(|s| s.trim().parse().ok());\r\n    (out, pid)\r\n}\r\n\r\n/// Poll `<id>`'s self-perch status until it equals `want` or the budget elapses.\r\nfn wait_for_status(id: &str, want: &str, budget: Duration) -> bool {\r\n    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\r\n    let deadline = Instant::now() + budget;\r\n    while Instant::now() < deadline {\r\n        let now = spt_store::info::read_info(&perch).and_then(|i| i.status);\r\n        if now.as_deref() == Some(want) {\r\n            return true;\r\n        }\r\n        std::thread::sleep(Duration::from_millis(150));\r\n    }\r\n    false\r\n}\r\n\r\n/// Attach `rc <id>` and return whether its `DUMMY_HARNESS_TICK` output arrived within\r\n/// the window — the strong \"this endpoint is being SERVED\" proof. The rc child is then\r\n/// reaped ABRUPTLY (`kill()` = TerminateProcess, no clean detach record) — exactly the\r\n/// closed-tab / crashed-operator case (a DROPPED pump), which is what makes this the\r\n/// L2 wedge trigger when called on the victim. The reader thread observes the killed\r\n/// pipe (read → 0/Err) and exits.\r\nfn rc_sees_tick(spt_bin: &Path, home: &Path, id: &str, window: Duration) -> bool {\r\n    let mut rc: Child = Command::new(spt_bin)\r\n        .no_window()\r\n        .args([\"rc\", id])\r\n        .env(\"SPT_HOME\", home)\r\n        .stdout(Stdio::piped())\r\n        .stderr(Stdio::null())\r\n        .stdin(Stdio::piped()) // hold stdin open so the pump does not EOF-detach early\r\n        .spawn()\r\n        .expect(\"spawn spt rc <id>\");\r\n    let _rc_stdin = rc.stdin.take();\r\n    let mut rc_out = rc.stdout.take().unwrap();\r\n    let (tx, rx) = std::sync::mpsc::channel::<bool>();\r\n    let reader = std::thread::spawn(move || {\r\n        let mut buf = [0u8; 4096];\r\n        let mut acc = String::new();\r\n        loop {\r\n            match rc_out.read(&mut buf) {\r\n                Ok(0) => break,\r\n                Ok(n) => {\r\n                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));\r\n                    if acc.contains(\"DUMMY_HARNESS_TICK\") {\r\n                        let _ = tx.send(true);\r\n                        break;\r\n                    }\r\n                }\r\n                Err(_) => break,\r\n            }\r\n        }\r\n        let _ = tx.send(false);\r\n    });\r\n    let saw = rx.recv_timeout(window).unwrap_or(false);\r\n    // Abrupt reap = the dropped-pump wedge trigger (no clean detach). `kill()` is\r\n    // idempotent on an already-exited child; `wait()` reaps the handle (clippy\r\n    // zombie_processes).\r\n    let _ = rc.kill();\r\n    let _ = rc.wait();\r\n    let _ = reader.join();\r\n    saw\r\n}\r\n\r\n#[test]\r\nfn attach_wedge_dead_child_plus_dropped_pump_does_not_wedge_the_broker() {\r\n    // ── (1) Isolated, NET-ENABLED SPT_HOME (rc attaches over a broker loopback conn\r\n    //    that surfaces as net-stream events — needs a net-bound broker). ──\r\n    let home = tempfile::tempdir().unwrap();\r\n    std::env::set_var(\"SPT_HOME\", home.path());\r\n\r\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\r\n    let exe_suffix = spt_bin\r\n        .extension()\r\n        .map(|e| format!(\".{}\", e.to_string_lossy()))\r\n        .unwrap_or_default();\r\n    let mock_session = sibling_bin(\"mock-session\");\r\n    assert!(\r\n        mock_session.exists(),\r\n        \"the dummy-harness program must be built: {} \\\r\n         (pre-build: cargo build -p mock-adapter --bin mock-session)\",\r\n        mock_session.display()\r\n    );\r\n\r\n    // ── (2) Register the DUMMY-HARNESS adapter (same shape as dummy_harness_e2e). ──\r\n    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\\\', \"/\");\r\n    let mock_for_cmd = mock_session.to_string_lossy().replace('\\\\', \"/\");\r\n    let self_cmd = format!(\r\n        \"{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \\\r\n         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200\"\r\n    );\r\n    let src = perch::spt_home().join(\"srcs\").join(\"dummyharness\");\r\n    std::fs::create_dir_all(&src).unwrap();\r\n    let psyche_bin = src.join(format!(\"psychebin{exe_suffix}\"));\r\n    std::fs::copy(&spt_bin, &psyche_bin).expect(\"stage the psyche binary fixture\");\r\n    let drops = home.path().join(\"drops\");\r\n    std::fs::create_dir_all(&drops).unwrap();\r\n    let dir = drops.to_string_lossy().replace('\\\\', \"/\");\r\n    let manifest_toml = format!(\r\n        \"[adapter]\\nname=\\\"dummyharness\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\r\n         min_spt_core_version=\\\"0\\\"\\n\\n[session.self]\\ncommand='{self_cmd}'\\n\\n\\\r\n         [session]\\ncommune_dir=\\\"{dir}\\\"\\nsignoff_dir=\\\"{dir}\\\"\\n\\n\\\r\n         [session.psyche_init]\\ncommand='psychebin ready {{id}}'\\ncwd=\\\"{{psyche_dir}}\\\"\\nkeys=[]\\n\"\r\n    );\r\n    std::fs::write(src.join(\"manifest.toml\"), &manifest_toml).unwrap();\r\n    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();\r\n\r\n    // ── (3) The REAL daemon broker process. ──\r\n    let brain_log = home.path().join(\"brain.stderr.log\");\r\n    let brain_log_file = std::fs::File::create(&brain_log).expect(\"create brain stderr log\");\r\n    let mut broker: Child = Command::new(&spt_bin)\r\n        .no_window()\r\n        .args([\"daemon\", \"run\"])\r\n        .env(\"SPT_HOME\", home.path())\r\n        .stdout(Stdio::null())\r\n        .stderr(Stdio::from(brain_log_file))\r\n        .spawn()\r\n        .expect(\"spawn spt daemon run (broker process)\");\r\n    let brain_pid = match wait_for_ready_pid(&home.path().join(\"brain.ready\"), Duration::from_secs(30))\r\n    {\r\n        Some(p) => p,\r\n        None => {\r\n            let _ = broker.kill();\r\n            let _ = broker.wait();\r\n            panic!(\r\n                \"PRECONDITION: brain never came up.\\n{}\",\r\n                std::fs::read_to_string(&brain_log).unwrap_or_default()\r\n            );\r\n        }\r\n    };\r\n\r\n    // ── (4) Bring up the VICTIM endpoint and prove it is SERVED (rc sees the tick). ──\r\n    let victim = \"wedge1\";\r\n    let (run1, harness_pid1) = endpoint_run(&spt_bin, home.path(), victim);\r\n    let victim_online = wait_for_status(victim, STATUS_ONLINE, Duration::from_secs(20));\r\n    // rc_sees_tick reaps its rc ABRUPTLY before returning = the dropped-pump half of\r\n    // the wedge trigger (no clean detach record).\r\n    let victim_served = rc_sees_tick(&spt_bin, home.path(), victim, Duration::from_secs(10));\r\n\r\n    // ── (5) THE WEDGE TRIGGER (second half): kill the harness PTY child abruptly (a\r\n    //    legitimately dead child). Together with the just-dropped operator pump this is\r\n    //    exactly the closed-tab + crash case — no clean teardown, no detach record. ──\r\n    if let Some(p) = harness_pid1 {\r\n        kill_pid(p);\r\n    }\r\n\r\n    // ── (6) L2 — the broker is NOT wedged: a brand-NEW endpoint still comes up online\r\n    //    AND is served (rc sees its tick) within bounded time. If the dead-child +\r\n    //    dropped-pump had parked the net runtime, this bring-up would time out. ──\r\n    let fresh = \"wedge2\";\r\n    let (run2, harness_pid2) = endpoint_run(&spt_bin, home.path(), fresh);\r\n    let fresh_online = wait_for_status(fresh, STATUS_ONLINE, Duration::from_secs(20));\r\n    let fresh_served = rc_sees_tick(&spt_bin, home.path(), fresh, Duration::from_secs(10));\r\n\r\n    // ── (7) L3 — the dead victim endpoint is OFFLINED within one reconcile tick\r\n    //    (LIVE_RECONCILE_INTERVAL_MS = 5 s; allow a generous window). The broker's\r\n    //    exit-waiter reaps the dead session from its table, and the brain's\r\n    //    reconcile_hosted_liveness clears the stale status=online latch. ──\r\n    let victim_offlined = wait_for_status(victim, STATUS_OFFLINE, Duration::from_secs(20));\r\n\r\n    // ── (8) L4 — daemon stop completes BOUNDED. ──\r\n    let stop = {\r\n        let mut cmd = Command::new(&spt_bin);\r\n        // `--force`: `wedge2` is a live hosted session by design, so the W3\r\n        // STOP-LIVE-SESSION-WARN contract refuses a plain stop — this teardown\r\n        // intends to kill it. [int->REQ-DAEMON-STOP-LIVE-SESSION-WARN]\r\n        cmd.no_window().args([\"daemon\", \"stop\", \"--force\"]).env(\"SPT_HOME\", home.path());\r\n        output_bounded(cmd, Duration::from_secs(20))\r\n    };\r\n\r\n    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();\r\n    eprintln!(\r\n        \"=== DIAGNOSTIC: run1_ok={} victim_online={victim_online} victim_served={victim_served} \\\r\n         harness_pid1={harness_pid1:?} | run2_ok={} fresh_online={fresh_online} \\\r\n         fresh_served={fresh_served} | victim_offlined={victim_offlined} stop_ok={} ===\\n\\\r\n         === brain stderr ===\\n{brain_stderr}\",\r\n        run1.status.success(),\r\n        run2.status.success(),\r\n        stop.status.success(),\r\n    );\r\n\r\n    // ── (9) Reap scoped: both harness children + any hosted psyches + brain + broker\r\n    //    (both rc operators were already reaped inside rc_sees_tick). Never\r\n    //    machine-wide. ──\r\n    if let Some(p) = harness_pid2 {\r\n        kill_pid(p);\r\n    }\r\n    for id in [victim, fresh] {\r\n        let psyche_perch =\r\n            perch::resolve_perch_path(&format!(\"{id}-psyche\"), ParentHint::Explicit(id));\r\n        if let Some(p) = spt_store::info::read_pid(&psyche_perch) {\r\n            kill_pid(p);\r\n        }\r\n    }\r\n    kill_pid(brain_pid);\r\n    let _ = broker.kill();\r\n    let _ = broker.wait();\r\n\r\n    // ── Assertions. ──\r\n    assert!(victim_online, \"PRECONDITION: the victim endpoint must come ONLINE\");\r\n    assert!(\r\n        victim_served,\r\n        \"PRECONDITION: the victim must be SERVED (rc saw its tick) before we wedge it\"\r\n    );\r\n    // L2 keystone: the broker survived the dead-child + dropped-pump and still serves.\r\n    // [int->REQ-HAZARD-ATTACH-WEDGE]\r\n    assert!(\r\n        fresh_online,\r\n        \"after a dead PTY child + a dropped operator pump, a NEW `endpoint run` must \\\r\n         still come ONLINE — the broker must not be wedged.\\n=== brain stderr ===\\n{brain_stderr}\"\r\n    );\r\n    assert!(\r\n        fresh_served,\r\n        \"after the wedge trigger, a NEW `rc` attach must be SERVED (saw its tick) — proof \\\r\n         the net runtime was not parked by the dead loopback peer.\\n=== brain stderr ===\\n{brain_stderr}\"\r\n    );\r\n    // L3 (folded): the dead endpoint is offlined within a reconcile tick.\r\n    // [int->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\r\n    assert!(\r\n        victim_offlined,\r\n        \"the dead victim endpoint must be OFFLINED within a reconcile tick (its broker \\\r\n         session is reaped on child death; reconcile_hosted_liveness clears the latch).\\n\\\r\n         === brain stderr ===\\n{brain_stderr}\"\r\n    );\r\n    // L4 (folded): daemon stop is bounded (the output_bounded above would have panicked\r\n    // on timeout); assert it also succeeded cleanly.\r\n    assert!(\r\n        stop.status.success(),\r\n        \"`daemon stop` must complete bounded + clean after the wedge trigger: {}\",\r\n        String::from_utf8_lossy(&stop.stderr)\r\n    );\r\n}\r\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,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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\attach_wedge_e2e.rs"}}},"isError":false,"timestamp":1784195854276}}
{"type":"message","id":"7627d10d","parentId":"c424d172","timestamp":"2026-07-16T09:57:34.278Z","message":{"role":"toolResult","toolCallId":"call_oOkR70vqpTe6z0VOdHJFh8cu|fc_047bb49263dfa303016a58ab0d2ab48198a400a5c14a02164a","toolName":"read","content":[{"type":"text","text":"//! F-026 W1 #2 (REQ-HAZARD-CONTROL-STAMP-LIFETIME) — a control/viewer stamp never\n//! outlives its session, on a REAL broker over the real local IPC (no PTY/transport\n//! mocks). Red-first: the exit-waiter used to `sessions.remove(&id)` with NO stamp\n//! clear, so `/exit` (which kills the CHILD, not the controller conn) left\n//! `controlled` / `driven_by` / `viewer_count` latched in info.json forever — and\n//! gossiped a stale `controller_node` cross-node.\n//!\n//! ## The bug this gate locks down\n//!\n//! `/exit` ends the RC session INDIRECTLY: the endpoint suspends when the child\n//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n//! path dropped the OutputLog with the controller slot still occupied, so the\n//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\n//! hall-a's info.json still read `controlled:true` and hfenduleam still gossiped\n//! `controller_node=self`.\n//!\n//! ## What the fix does\n//!\n//! The exit-waiter calls `OutputLog::stamp_reaped()` on reap, which clears all\n//! three stamps (`set_driven_by(None)`, `set_controlled(false)`,\n//! `set_viewer_count(0)`) via the known endpoint id. The broker stays the single\n//! writer; the session is dead so no live controller re-stamps concurrently (the\n//! same race-free argument as the W5 no-session self-heal).\n//!\n//! ## Why kill the CHILD (not drop the conn)\n//!\n//! Dropping the spawner conn would run the EXISTING `clear_controller` path and\n//! prove nothing about the reap leg. Killing the PTY child fires the exit-waiter\n//! while the controller conn stays open — isolating the reap clear as the ONLY\n//! path that can unlatch the stamps.\n//!\n//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n//!\n//! Shares ONE process-global `SPT_HOME` (set once in [`init_stamp_home`]) and spins\n//! a REAL broker per test; run isolated (the driven_by_selfheal precedent):\n//!   - `cargo nextest run --test control_stamp_lifetime`  (CI's path), or\n//!   - `cargo test --test control_stamp_lifetime -- --test-threads=1`.\n//!\n//! Do NOT run a bare `cargo test --test control_stamp_lifetime` on Windows.\n\nuse std::collections::BTreeSet;\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\nuse std::time::Duration;\n\nuse spt_daemon::brain::Brain;\nuse spt_daemon::msg::SpawnReq;\nuse spt_daemon::Broker;\n\nuse spt_store::perch::{self, ParentHint};\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique_name() -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-ctlstamp-{}-{}.sock\", std::process::id(), n)\n}\n\nstatic STAMP_HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\nfn init_stamp_home() {\n    STAMP_HOME.get_or_init(|| {\n        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n        std::env::set_var(\"SPT_HOME\", dir.path());\n        dir\n    });\n}\n\nfn kill_pid(pid: u32) {\n    #[cfg(windows)]\n    let _ = std::process::Command::new(\"taskkill\")\n        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\n        .output();\n    #[cfg(unix)]\n    let _ = std::process::Command::new(\"kill\")\n        .args([\"-9\", &pid.to_string()])\n        .output();\n}\n\nfn start_broker(name: &str) -> Arc<Broker> {\n    let broker = Broker::bind(name).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n    broker\n}\n\nfn connect_brain(name: &str) -> Brain {\n    for _ in 0..300 {\n        if let Ok(b) = Brain::cold_start(name, 1) {\n            return b;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"brain could not connect to {name}\");\n}\n\n/// A long-lived stdin-reading PTY child so the spawned session stays LIVE until we\n/// kill it (cross-OS).\nfn idle_spawn_req(endpoint: &str) -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n    #[cfg(windows)]\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n    SpawnReq {\n        program,\n        args,\n        rows: 24,\n        cols: 80,\n        endpoint: endpoint.to_string(),\n        cwd: None,\n        env: Default::default(),\n        translation_binary: None,\n        adapter: String::new(),\n        install_dir: None,\n    }\n}\n\n/// Create a hosted (`controllable=Some(true)`) `live_agent` self-perch, ONLINE, so\n/// the broker's stamps land on a real info.json (mirrors the selfheal rig's stamp).\nfn create_hosted_perch(id: &str) -> std::path::PathBuf {\n    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n    std::fs::create_dir_all(&self_perch).unwrap();\n    let mut rec =\n        spt_store::info::InfoJson::new(id, \"t\", std::process::id(), \"sid-ctlstamp\", \"live_agent\");\n    rec.controllable = Some(true);\n    spt_store::info::write_info(&self_perch, &rec).unwrap();\n    spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_ONLINE).unwrap();\n    self_perch\n}\n\n#[test]\nfn reap_clears_control_and_viewer_stamps() {\n    init_stamp_home();\n    let node = \"PEERNODEHEX_CTLSTAMP\";\n    let id = format!(\"ctlstamp-{}\", std::process::id());\n    let self_perch = create_hosted_perch(&id);\n\n    // ── A REAL broker hosting a LIVE session for THIS endpoint. dispatch_spawn\n    //    pre-attaches the spawner as the LOCAL controller (by=None) and its\n    //    become_controller → stamp_driven_by stamps `controlled=true` on the perch. ──\n    let name = unique_name();\n    let broker = start_broker(&name);\n    let mut spawner = connect_brain(&name);\n    let sid = spawner\n        .spawn_session(idle_spawn_req(&id))\n        .expect(\"spawn live session for the endpoint\");\n\n    // Give the spawn's become_controller stamp a beat to land.\n    thread::sleep(Duration::from_millis(200));\n\n    // Precondition (genuine): the live session stamped `controlled=true` (the local\n    // controller). This is a REAL broker stamp, not a hand-set value.\n    let before = spt_store::info::read_info(&self_perch).expect(\"info present\");\n    assert!(\n        before.controlled,\n        \"precondition: the live hosted session must stamp controlled=true \\\n         (become_controller → stamp_driven_by) — got {before:?}\"\n    );\n\n    // Simulate the FULL latch a remote-driven, multi-viewer session leaves in\n    // info.json: driven_by=Some(remote) + viewer_count=2 (broker is the single\n    // writer of both; here we set the on-disk state the reap must unlatch). The\n    // spawner stays attached, idle → nothing re-stamps these before the reap.\n    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\n    spt_store::info::set_viewer_count(&self_perch, 2).unwrap();\n\n    let latched = spt_store::info::read_info(&self_perch).expect(\"info present\");\n    assert_eq!(\n        latched.driven_by.as_deref(),\n        Some(node),\n        \"precondition: driven_by latched Some before reap\"\n    );\n    assert_eq!(\n        latched.viewer_count,\n        Some(2),\n        \"precondition: viewer_count latched Some(2) before reap\"\n    );\n    assert!(latched.controlled, \"precondition: controlled true before reap\");\n\n    // ── Kill the PTY CHILD → the exit-waiter reaps the session (NOT a conn drop, so\n    //    the controller-detach path never runs; only the reap path can unlatch). ──\n    let pid = broker\n        .session_pid(sid)\n        .expect(\"live session must have a pid before kill\");\n    kill_pid(pid);\n\n    // Poll for the reap to complete (exit-waiter runs on its own thread).\n    let mut cleared = false;\n    for _ in 0..100 {\n        thread::sleep(Duration::from_millis(100));\n        let info = spt_store::info::read_info(&self_perch).expect(\"info present\");\n        if info.driven_by.is_none() && !info.controlled && info.viewer_count.is_none() {\n            cleared = true;\n            break;\n        }\n    }\n\n    let after = spt_store::info::read_info(&self_perch).expect(\"info present after reap\");\n    eprintln!(\n        \"=== F026 #2 REAP RESULT: driven_by={:?} controlled={} viewer_count={:?} (all cleared=expected) ===\",\n        after.driven_by, after.controlled, after.viewer_count\n    );\n\n    drop(spawner);\n\n    // [int->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n    assert!(\n        cleared,\n        \"reap must clear every control/viewer stamp within the poll window — \\\n         got driven_by={:?} controlled={} viewer_count={:?}\",\n        after.driven_by, after.controlled, after.viewer_count\n    );\n    assert_eq!(\n        after.driven_by, None,\n        \"reap must clear driven_by (a remote-controller latch must not outlive the session)\"\n    );\n    assert!(\n        !after.controlled,\n        \"reap must clear controlled (ONLINE+CONTROLLED must not survive /exit)\"\n    );\n    assert_eq!(\n        after.viewer_count, None,\n        \"reap must clear viewer_count (set_viewer_count(0) → None)\"\n    );\n}\n\n/// F-026 W1-addendum (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE) — the STAMP-BEFORE-BIND\n/// loss: a picker-created endpoint's broker spawn stamps `controlled=true` BEFORE the\n/// adapter binds its perch (`mutate_info` NotFound → swallowed); the adapter's bind\n/// then writes `controlled:false` and no later edge re-stamps, so the endpoint reads\n/// uncontrolled forever while driven (hall-b / the original ball-b report). The\n/// `KIND_SESSIONS` poll now CONVERGES the perch to the live session truth,\n/// divergence-gated. RED-first: without the convergence hook, the perch stays\n/// `controlled:false` forever.\n#[test]\nfn converge_stamps_on_sessions_poll_after_late_bind() {\n    init_stamp_home();\n    let id = format!(\"ctlconv-{}\", std::process::id());\n\n    // A REAL broker + a spawned session for the endpoint — but NO perch yet, so the\n    // spawn's become_controller → set_controlled(true) is SWALLOWED (NotFound): the\n    // picker-created stamp-before-bind loss.\n    let name = unique_name();\n    let broker = start_broker(&name);\n    let mut spawner = connect_brain(&name);\n    let sid = spawner\n        .spawn_session(idle_spawn_req(&id))\n        .expect(\"spawn live session before its perch exists\");\n    thread::sleep(Duration::from_millis(150));\n\n    // The adapter binds LATE, writing controlled:false (InfoJson::new default) — the\n    // stamp the picker-created endpoint is stuck at.\n    let self_perch = create_hosted_perch(&id);\n    let before = spt_store::info::read_info(&self_perch).expect(\"info present\");\n    assert!(\n        !before.controlled,\n        \"precondition: the late bind wrote controlled=false (the spawn stamp was lost) — got {before:?}\"\n    );\n\n    // A KIND_SESSIONS poll (what the daemon reconcile does each cadence) converges the\n    // perch to broker truth: the spawner is the (local) controller ⇒ controlled=true.\n    let mut converged = false;\n    for _ in 0..50 {\n        let mut brain = connect_brain(&name);\n        let _ = brain.sessions().expect(\"KIND_SESSIONS reply\");\n        thread::sleep(Duration::from_millis(50));\n        if spt_store::info::read_info(&self_perch)\n            .map(|i| i.controlled)\n            .unwrap_or(false)\n        {\n            converged = true;\n            break;\n        }\n    }\n\n    let after = spt_store::info::read_info(&self_perch).expect(\"info present after poll\");\n    if let Some(pid) = broker.session_pid(sid) {\n        kill_pid(pid);\n    }\n    drop(spawner);\n\n    // [int->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n    assert!(\n        converged,\n        \"the KIND_SESSIONS poll must converge controlled=true within the window — got {after:?}\"\n    );\n    assert!(\n        after.controlled,\n        \"controlled converged to the live-session truth (a local controller drives it)\"\n    );\n    assert_eq!(\n        after.driven_by, None,\n        \"a LOCAL controller does not latch driven_by (remote-only, KH 7.15); #3 display reads controlled\"\n    );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// F-028 W3 B3 (REQ-PRESENCE-CONTROL-REAP-ON-EXIT) — wave-final int.\n//\n// The field bug (perri F-b / deferred #11): a control stamp latched `controlled`\n// + `driven_by` on a spt-hosted online live_agent perch AFTER the harness exited,\n// persisting >20min AND ACROSS A DAEMON RESTART. This is the DIFFERENT reap leg\n// from `reap_clears_control_and_viewer_stamps` above (that drives the broker\n// exit-waiter's `stamp_reaped`); B3 is the `reconcile_hosted_liveness` sweep — the\n// tick that clears a sticky stamp on a perch whose broker session is simply GONE,\n// regardless of status, so an already-offline perch left sticky across a restart is\n// swept (path iv, the boot sweep), not only on the online→offline transition.\n//\n// This int drives the REAL reconcile entry (`spt_daemon::livehost::reconcile_hosted_liveness`)\n// against the REAL on-disk owlery, with the live-session set DERIVED FROM A REAL\n// BROKER (query_live_session_endpoints's exact shape: `brain.sessions()` →\n// endpoint ids). A decoy live session keeps the broker set genuinely NON-empty, so\n// the reap fires precisely because the broker reports NO session for the sticky\n// perch — not because we hand it an empty set. Two arms:\n//   (A) ONLINE + sticky → cleared AND offlined (clean-exit / crash → sessionless).\n//   (B) already-OFFLINE + sticky → cleared REGARDLESS of status (the boot sweep; a\n//       restarted daemon must not resurrect the stamps).\n// [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n\n/// Seed a spt-hosted (`controllable=Some(true)`) live_agent self-perch at `status`,\n/// carrying the FULL sticky control latch a departed remote-driven session leaves:\n/// `controlled=true` + `driven_by=Some(node)`. Returns the perch path.\nfn seed_sticky_hosted_perch(id: &str, status: &str, node: &str) -> std::path::PathBuf {\n    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n    std::fs::create_dir_all(&self_perch).unwrap();\n    let mut rec =\n        spt_store::info::InfoJson::new(id, \"t\", std::process::id(), \"sid-b3-sticky\", \"live_agent\");\n    rec.controllable = Some(true);\n    spt_store::info::write_info(&self_perch, &rec).unwrap();\n    spt_store::info::set_status(&self_perch, status).unwrap();\n    // The sticky latch the reap must unwind (broker single-writer; here on disk).\n    spt_store::info::set_controlled(&self_perch, true).unwrap();\n    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\n    let seeded = spt_store::info::read_info(&self_perch).expect(\"seeded info present\");\n    assert!(\n        seeded.controlled && seeded.driven_by.as_deref() == Some(node),\n        \"precondition: perch {id} must be seeded sticky (controlled=true + driven_by=Some) — got {seeded:?}\"\n    );\n    self_perch\n}\n\n#[test]\nfn control_reap_clears_sticky_stamps_no_session_online_and_boot_sweep() {\n    init_stamp_home();\n    let node = \"PEERNODEHEX_B3\";\n    let pid = std::process::id();\n    // Arm A: ONLINE + sticky (clean-exit / crash → sessionless, online→offline).\n    let online_id = format!(\"b3-online-{pid}\");\n    // Arm B: already-OFFLINE + sticky (survived a daemon restart — the boot sweep).\n    let offline_id = format!(\"b3-boot-{pid}\");\n    let online_perch = seed_sticky_hosted_perch(&online_id, spt_store::liveness::STATUS_ONLINE, node);\n    let offline_perch =\n        seed_sticky_hosted_perch(&offline_id, spt_store::liveness::STATUS_OFFLINE, node);\n\n    // A REAL broker hosting a DECOY live session — so the session set the reconcile\n    // consults is genuinely non-empty and the reap distinguishes \"has session\" from\n    // \"no session\" against REAL broker truth (not a hand-passed empty set). The decoy\n    // has NO pre-created perch, so it is never scanned/offlined by the reconcile.\n    let decoy_id = format!(\"b3-decoy-{pid}\");\n    let name = unique_name();\n    let broker = start_broker(&name);\n    let mut spawner = connect_brain(&name);\n    let decoy_sid = spawner\n        .spawn_session(idle_spawn_req(&decoy_id))\n        .expect(\"spawn the decoy live session\");\n    thread::sleep(Duration::from_millis(200));\n\n    // Build the live-session set the daemon's reconcile uses — the exact shape of\n    // `query_live_session_endpoints`: KIND_SESSIONS reply → endpoint ids.\n    let mut probe = connect_brain(&name);\n    let reply = probe.sessions().expect(\"KIND_SESSIONS reply\");\n    let live: BTreeSet<String> = reply\n        .sessions\n        .into_iter()\n        .map(|s| s.endpoint)\n        .filter(|e| !e.is_empty())\n        .collect();\n    // Faithful precondition: the broker DOES host the decoy, and does NOT host a\n    // session for either sticky perch (so the reap must fire on both).\n    assert!(\n        live.contains(&decoy_id),\n        \"precondition: the real broker must report the decoy session live — got {live:?}\"\n    );\n    assert!(\n        !live.contains(&online_id) && !live.contains(&offline_id),\n        \"precondition: neither sticky perch has a broker session — got {live:?}\"\n    );\n\n    // ── Drive the REAL reconcile sweep (the daemon runs this from boot tick 1). ──\n    let offlined =\n        spt_daemon::livehost::reconcile_hosted_liveness(&spt_store::perch::owlery_dir(), &live);\n\n    let after_online = spt_store::info::read_info(&online_perch).expect(\"online info present\");\n    let after_offline = spt_store::info::read_info(&offline_perch).expect(\"offline info present\");\n    eprintln!(\n        \"=== F028 B3 REAP: online[controlled={} driven_by={:?} status={:?}] \\\n         boot[controlled={} driven_by={:?} status={:?}] offlined={offlined:?} ===\",\n        after_online.controlled, after_online.driven_by, after_online.status,\n        after_offline.controlled, after_offline.driven_by, after_offline.status,\n    );\n\n    // ── Reap scoped (decoy child + spawner). ──\n    if let Some(p) = broker.session_pid(decoy_sid) {\n        kill_pid(p);\n    }\n    drop(spawner);\n    drop(probe);\n\n    // Arm A — ONLINE + sticky: BOTH stamps cleared AND the perch offlined.\n    // [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n    assert!(\n        !after_online.controlled,\n        \"B3 arm A: reap must clear controlled on an online sessionless spt-hosted perch\"\n    );\n    assert_eq!(\n        after_online.driven_by, None,\n        \"B3 arm A: reap must clear driven_by (attached_node derives from it) on the online perch\"\n    );\n    assert!(\n        offlined.contains(&online_id),\n…\n    );\n…\n}\n\n[Showing lines 1-423 of 446. Use :424 to continue]"}],"details":{"truncation":{"content":"//! F-026 W1 #2 (REQ-HAZARD-CONTROL-STAMP-LIFETIME) — a control/viewer stamp never\r\n//! outlives its session, on a REAL broker over the real local IPC (no PTY/transport\r\n//! mocks). Red-first: the exit-waiter used to `sessions.remove(&id)` with NO stamp\r\n//! clear, so `/exit` (which kills the CHILD, not the controller conn) left\r\n//! `controlled` / `driven_by` / `viewer_count` latched in info.json forever — and\r\n//! gossiped a stale `controller_node` cross-node.\r\n//!\r\n//! ## The bug this gate locks down\r\n//!\r\n//! `/exit` ends the RC session INDIRECTLY: the endpoint suspends when the child\r\n//! binary terminates, so the teardown runs the REAP path (exit-waiter →\r\n//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\r\n//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\r\n//! path dropped the OutputLog with the controller slot still occupied, so the\r\n//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\r\n//! hall-a's info.json still read `controlled:true` and hfenduleam still gossiped\r\n//! `controller_node=self`.\r\n//!\r\n//! ## What the fix does\r\n//!\r\n//! The exit-waiter calls `OutputLog::stamp_reaped()` on reap, which clears all\r\n//! three stamps (`set_driven_by(None)`, `set_controlled(false)`,\r\n//! `set_viewer_count(0)`) via the known endpoint id. The broker stays the single\r\n//! writer; the session is dead so no live controller re-stamps concurrently (the\r\n//! same race-free argument as the W5 no-session self-heal).\r\n//!\r\n//! ## Why kill the CHILD (not drop the conn)\r\n//!\r\n//! Dropping the spawner conn would run the EXISTING `clear_controller` path and\r\n//! prove nothing about the reap leg. Killing the PTY child fires the exit-waiter\r\n//! while the controller conn stays open — isolating the reap clear as the ONLY\r\n//! path that can unlatch the stamps.\r\n//!\r\n//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\r\n//!\r\n//! Shares ONE process-global `SPT_HOME` (set once in [`init_stamp_home`]) and spins\r\n//! a REAL broker per test; run isolated (the driven_by_selfheal precedent):\r\n//!   - `cargo nextest run --test control_stamp_lifetime`  (CI's path), or\r\n//!   - `cargo test --test control_stamp_lifetime -- --test-threads=1`.\r\n//!\r\n//! Do NOT run a bare `cargo test --test control_stamp_lifetime` on Windows.\r\n\r\nuse std::collections::BTreeSet;\r\nuse std::sync::atomic::{AtomicU32, Ordering};\r\nuse std::sync::{Arc, OnceLock};\r\nuse std::thread;\r\nuse std::time::Duration;\r\n\r\nuse spt_daemon::brain::Brain;\r\nuse spt_daemon::msg::SpawnReq;\r\nuse spt_daemon::Broker;\r\n\r\nuse spt_store::perch::{self, ParentHint};\r\n\r\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\r\nfn unique_name() -> String {\r\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\r\n    format!(\"spt-daemon-ctlstamp-{}-{}.sock\", std::process::id(), n)\r\n}\r\n\r\nstatic STAMP_HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\r\nfn init_stamp_home() {\r\n    STAMP_HOME.get_or_init(|| {\r\n        let dir = tempfile::TempDir::new().expect(\"temp dir\");\r\n        std::env::set_var(\"SPT_HOME\", dir.path());\r\n        dir\r\n    });\r\n}\r\n\r\nfn kill_pid(pid: u32) {\r\n    #[cfg(windows)]\r\n    let _ = std::process::Command::new(\"taskkill\")\r\n        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\r\n        .output();\r\n    #[cfg(unix)]\r\n    let _ = std::process::Command::new(\"kill\")\r\n        .args([\"-9\", &pid.to_string()])\r\n        .output();\r\n}\r\n\r\nfn start_broker(name: &str) -> Arc<Broker> {\r\n    let broker = Broker::bind(name).expect(\"bind broker\");\r\n    let serve = Arc::clone(&broker);\r\n    thread::spawn(move || {\r\n        let _ = serve.serve();\r\n    });\r\n    broker\r\n}\r\n\r\nfn connect_brain(name: &str) -> Brain {\r\n    for _ in 0..300 {\r\n        if let Ok(b) = Brain::cold_start(name, 1) {\r\n            return b;\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n    panic!(\"brain could not connect to {name}\");\r\n}\r\n\r\n/// A long-lived stdin-reading PTY child so the spawned session stays LIVE until we\r\n/// kill it (cross-OS).\r\nfn idle_spawn_req(endpoint: &str) -> SpawnReq {\r\n    #[cfg(unix)]\r\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\r\n    #[cfg(windows)]\r\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\r\n    SpawnReq {\r\n        program,\r\n        args,\r\n        rows: 24,\r\n        cols: 80,\r\n        endpoint: endpoint.to_string(),\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}\r\n\r\n/// Create a hosted (`controllable=Some(true)`) `live_agent` self-perch, ONLINE, so\r\n/// the broker's stamps land on a real info.json (mirrors the selfheal rig's stamp).\r\nfn create_hosted_perch(id: &str) -> std::path::PathBuf {\r\n    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\r\n    std::fs::create_dir_all(&self_perch).unwrap();\r\n    let mut rec =\r\n        spt_store::info::InfoJson::new(id, \"t\", std::process::id(), \"sid-ctlstamp\", \"live_agent\");\r\n    rec.controllable = Some(true);\r\n    spt_store::info::write_info(&self_perch, &rec).unwrap();\r\n    spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_ONLINE).unwrap();\r\n    self_perch\r\n}\r\n\r\n#[test]\r\nfn reap_clears_control_and_viewer_stamps() {\r\n    init_stamp_home();\r\n    let node = \"PEERNODEHEX_CTLSTAMP\";\r\n    let id = format!(\"ctlstamp-{}\", std::process::id());\r\n    let self_perch = create_hosted_perch(&id);\r\n\r\n    // ── A REAL broker hosting a LIVE session for THIS endpoint. dispatch_spawn\r\n    //    pre-attaches the spawner as the LOCAL controller (by=None) and its\r\n    //    become_controller → stamp_driven_by stamps `controlled=true` on the perch. ──\r\n    let name = unique_name();\r\n    let broker = start_broker(&name);\r\n    let mut spawner = connect_brain(&name);\r\n    let sid = spawner\r\n        .spawn_session(idle_spawn_req(&id))\r\n        .expect(\"spawn live session for the endpoint\");\r\n\r\n    // Give the spawn's become_controller stamp a beat to land.\r\n    thread::sleep(Duration::from_millis(200));\r\n\r\n    // Precondition (genuine): the live session stamped `controlled=true` (the local\r\n    // controller). This is a REAL broker stamp, not a hand-set value.\r\n    let before = spt_store::info::read_info(&self_perch).expect(\"info present\");\r\n    assert!(\r\n        before.controlled,\r\n        \"precondition: the live hosted session must stamp controlled=true \\\r\n         (become_controller → stamp_driven_by) — got {before:?}\"\r\n    );\r\n\r\n    // Simulate the FULL latch a remote-driven, multi-viewer session leaves in\r\n    // info.json: driven_by=Some(remote) + viewer_count=2 (broker is the single\r\n    // writer of both; here we set the on-disk state the reap must unlatch). The\r\n    // spawner stays attached, idle → nothing re-stamps these before the reap.\r\n    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\r\n    spt_store::info::set_viewer_count(&self_perch, 2).unwrap();\r\n\r\n    let latched = spt_store::info::read_info(&self_perch).expect(\"info present\");\r\n    assert_eq!(\r\n        latched.driven_by.as_deref(),\r\n        Some(node),\r\n        \"precondition: driven_by latched Some before reap\"\r\n    );\r\n    assert_eq!(\r\n        latched.viewer_count,\r\n        Some(2),\r\n        \"precondition: viewer_count latched Some(2) before reap\"\r\n    );\r\n    assert!(latched.controlled, \"precondition: controlled true before reap\");\r\n\r\n    // ── Kill the PTY CHILD → the exit-waiter reaps the session (NOT a conn drop, so\r\n    //    the controller-detach path never runs; only the reap path can unlatch). ──\r\n    let pid = broker\r\n        .session_pid(sid)\r\n        .expect(\"live session must have a pid before kill\");\r\n    kill_pid(pid);\r\n\r\n    // Poll for the reap to complete (exit-waiter runs on its own thread).\r\n    let mut cleared = false;\r\n    for _ in 0..100 {\r\n        thread::sleep(Duration::from_millis(100));\r\n        let info = spt_store::info::read_info(&self_perch).expect(\"info present\");\r\n        if info.driven_by.is_none() && !info.controlled && info.viewer_count.is_none() {\r\n            cleared = true;\r\n            break;\r\n        }\r\n    }\r\n\r\n    let after = spt_store::info::read_info(&self_perch).expect(\"info present after reap\");\r\n    eprintln!(\r\n        \"=== F026 #2 REAP RESULT: driven_by={:?} controlled={} viewer_count={:?} (all cleared=expected) ===\",\r\n        after.driven_by, after.controlled, after.viewer_count\r\n    );\r\n\r\n    drop(spawner);\r\n\r\n    // [int->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\r\n    assert!(\r\n        cleared,\r\n        \"reap must clear every control/viewer stamp within the poll window — \\\r\n         got driven_by={:?} controlled={} viewer_count={:?}\",\r\n        after.driven_by, after.controlled, after.viewer_count\r\n    );\r\n    assert_eq!(\r\n        after.driven_by, None,\r\n        \"reap must clear driven_by (a remote-controller latch must not outlive the session)\"\r\n    );\r\n    assert!(\r\n        !after.controlled,\r\n        \"reap must clear controlled (ONLINE+CONTROLLED must not survive /exit)\"\r\n    );\r\n    assert_eq!(\r\n        after.viewer_count, None,\r\n        \"reap must clear viewer_count (set_viewer_count(0) → None)\"\r\n    );\r\n}\r\n\r\n/// F-026 W1-addendum (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE) — the STAMP-BEFORE-BIND\r\n/// loss: a picker-created endpoint's broker spawn stamps `controlled=true` BEFORE the\r\n/// adapter binds its perch (`mutate_info` NotFound → swallowed); the adapter's bind\r\n/// then writes `controlled:false` and no later edge re-stamps, so the endpoint reads\r\n/// uncontrolled forever while driven (hall-b / the original ball-b report). The\r\n/// `KIND_SESSIONS` poll now CONVERGES the perch to the live session truth,\r\n/// divergence-gated. RED-first: without the convergence hook, the perch stays\r\n/// `controlled:false` forever.\r\n#[test]\r\nfn converge_stamps_on_sessions_poll_after_late_bind() {\r\n    init_stamp_home();\r\n    let id = format!(\"ctlconv-{}\", std::process::id());\r\n\r\n    // A REAL broker + a spawned session for the endpoint — but NO perch yet, so the\r\n    // spawn's become_controller → set_controlled(true) is SWALLOWED (NotFound): the\r\n    // picker-created stamp-before-bind loss.\r\n    let name = unique_name();\r\n    let broker = start_broker(&name);\r\n    let mut spawner = connect_brain(&name);\r\n    let sid = spawner\r\n        .spawn_session(idle_spawn_req(&id))\r\n        .expect(\"spawn live session before its perch exists\");\r\n    thread::sleep(Duration::from_millis(150));\r\n\r\n    // The adapter binds LATE, writing controlled:false (InfoJson::new default) — the\r\n    // stamp the picker-created endpoint is stuck at.\r\n    let self_perch = create_hosted_perch(&id);\r\n    let before = spt_store::info::read_info(&self_perch).expect(\"info present\");\r\n    assert!(\r\n        !before.controlled,\r\n        \"precondition: the late bind wrote controlled=false (the spawn stamp was lost) — got {before:?}\"\r\n    );\r\n\r\n    // A KIND_SESSIONS poll (what the daemon reconcile does each cadence) converges the\r\n    // perch to broker truth: the spawner is the (local) controller ⇒ controlled=true.\r\n    let mut converged = false;\r\n    for _ in 0..50 {\r\n        let mut brain = connect_brain(&name);\r\n        let _ = brain.sessions().expect(\"KIND_SESSIONS reply\");\r\n        thread::sleep(Duration::from_millis(50));\r\n        if spt_store::info::read_info(&self_perch)\r\n            .map(|i| i.controlled)\r\n            .unwrap_or(false)\r\n        {\r\n            converged = true;\r\n            break;\r\n        }\r\n    }\r\n\r\n    let after = spt_store::info::read_info(&self_perch).expect(\"info present after poll\");\r\n    if let Some(pid) = broker.session_pid(sid) {\r\n        kill_pid(pid);\r\n    }\r\n    drop(spawner);\r\n\r\n    // [int->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\n    assert!(\r\n        converged,\r\n        \"the KIND_SESSIONS poll must converge controlled=true within the window — got {after:?}\"\r\n    );\r\n    assert!(\r\n        after.controlled,\r\n        \"controlled converged to the live-session truth (a local controller drives it)\"\r\n    );\r\n    assert_eq!(\r\n        after.driven_by, None,\r\n        \"a LOCAL controller does not latch driven_by (remote-only, KH 7.15); #3 display reads controlled\"\r\n    );\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n// F-028 W3 B3 (REQ-PRESENCE-CONTROL-REAP-ON-EXIT) — wave-final int.\r\n//\r\n// The field bug (perri F-b / deferred #11): a control stamp latched `controlled`\r\n// + `driven_by` on a spt-hosted online live_agent perch AFTER the harness exited,\r\n// persisting >20min AND ACROSS A DAEMON RESTART. This is the DIFFERENT reap leg\r\n// from `reap_clears_control_and_viewer_stamps` above (that drives the broker\r\n// exit-waiter's `stamp_reaped`); B3 is the `reconcile_hosted_liveness` sweep — the\r\n// tick that clears a sticky stamp on a perch whose broker session is simply GONE,\r\n// regardless of status, so an already-offline perch left sticky across a restart is\r\n// swept (path iv, the boot sweep), not only on the online→offline transition.\r\n//\r\n// This int drives the REAL reconcile entry (`spt_daemon::livehost::reconcile_hosted_liveness`)\r\n// against the REAL on-disk owlery, with the live-session set DERIVED FROM A REAL\r\n// BROKER (query_live_session_endpoints's exact shape: `brain.sessions()` →\r\n// endpoint ids). A decoy live session keeps the broker set genuinely NON-empty, so\r\n// the reap fires precisely because the broker reports NO session for the sticky\r\n// perch — not because we hand it an empty set. Two arms:\r\n//   (A) ONLINE + sticky → cleared AND offlined (clean-exit / crash → sessionless).\r\n//   (B) already-OFFLINE + sticky → cleared REGARDLESS of status (the boot sweep; a\r\n//       restarted daemon must not resurrect the stamps).\r\n// [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\r\n\r\n/// Seed a spt-hosted (`controllable=Some(true)`) live_agent self-perch at `status`,\r\n/// carrying the FULL sticky control latch a departed remote-driven session leaves:\r\n/// `controlled=true` + `driven_by=Some(node)`. Returns the perch path.\r\nfn seed_sticky_hosted_perch(id: &str, status: &str, node: &str) -> std::path::PathBuf {\r\n    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\r\n    std::fs::create_dir_all(&self_perch).unwrap();\r\n    let mut rec =\r\n        spt_store::info::InfoJson::new(id, \"t\", std::process::id(), \"sid-b3-sticky\", \"live_agent\");\r\n    rec.controllable = Some(true);\r\n    spt_store::info::write_info(&self_perch, &rec).unwrap();\r\n    spt_store::info::set_status(&self_perch, status).unwrap();\r\n    // The sticky latch the reap must unwind (broker single-writer; here on disk).\r\n    spt_store::info::set_controlled(&self_perch, true).unwrap();\r\n    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\r\n    let seeded = spt_store::info::read_info(&self_perch).expect(\"seeded info present\");\r\n    assert!(\r\n        seeded.controlled && seeded.driven_by.as_deref() == Some(node),\r\n        \"precondition: perch {id} must be seeded sticky (controlled=true + driven_by=Some) — got {seeded:?}\"\r\n    );\r\n    self_perch\r\n}\r\n\r\n#[test]\r\nfn control_reap_clears_sticky_stamps_no_session_online_and_boot_sweep() {\r\n    init_stamp_home();\r\n    let node = \"PEERNODEHEX_B3\";\r\n    let pid = std::process::id();\r\n    // Arm A: ONLINE + sticky (clean-exit / crash → sessionless, online→offline).\r\n    let online_id = format!(\"b3-online-{pid}\");\r\n    // Arm B: already-OFFLINE + sticky (survived a daemon restart — the boot sweep).\r\n    let offline_id = format!(\"b3-boot-{pid}\");\r\n    let online_perch = seed_sticky_hosted_perch(&online_id, spt_store::liveness::STATUS_ONLINE, node);\r\n    let offline_perch =\r\n        seed_sticky_hosted_perch(&offline_id, spt_store::liveness::STATUS_OFFLINE, node);\r\n\r\n    // A REAL broker hosting a DECOY live session — so the session set the reconcile\r\n    // consults is genuinely non-empty and the reap distinguishes \"has session\" from\r\n    // \"no session\" against REAL broker truth (not a hand-passed empty set). The decoy\r\n    // has NO pre-created perch, so it is never scanned/offlined by the reconcile.\r\n    let decoy_id = format!(\"b3-decoy-{pid}\");\r\n    let name = unique_name();\r\n    let broker = start_broker(&name);\r\n    let mut spawner = connect_brain(&name);\r\n    let decoy_sid = spawner\r\n        .spawn_session(idle_spawn_req(&decoy_id))\r\n        .expect(\"spawn the decoy live session\");\r\n    thread::sleep(Duration::from_millis(200));\r\n\r\n    // Build the live-session set the daemon's reconcile uses — the exact shape of\r\n    // `query_live_session_endpoints`: KIND_SESSIONS reply → endpoint ids.\r\n    let mut probe = connect_brain(&name);\r\n    let reply = probe.sessions().expect(\"KIND_SESSIONS reply\");\r\n    let live: BTreeSet<String> = reply\r\n        .sessions\r\n        .into_iter()\r\n        .map(|s| s.endpoint)\r\n        .filter(|e| !e.is_empty())\r\n        .collect();\r\n    // Faithful precondition: the broker DOES host the decoy, and does NOT host a\r\n    // session for either sticky perch (so the reap must fire on both).\r\n    assert!(\r\n        live.contains(&decoy_id),\r\n        \"precondition: the real broker must report the decoy session live — got {live:?}\"\r\n    );\r\n    assert!(\r\n        !live.contains(&online_id) && !live.contains(&offline_id),\r\n        \"precondition: neither sticky perch has a broker session — got {live:?}\"\r\n    );\r\n\r\n    // ── Drive the REAL reconcile sweep (the daemon runs this from boot tick 1). ──\r\n    let offlined =\r\n        spt_daemon::livehost::reconcile_hosted_liveness(&spt_store::perch::owlery_dir(), &live);\r\n\r\n    let after_online = spt_store::info::read_info(&online_perch).expect(\"online info present\");\r\n    let after_offline = spt_store::info::read_info(&offline_perch).expect(\"offline info present\");\r\n    eprintln!(\r\n        \"=== F028 B3 REAP: online[controlled={} driven_by={:?} status={:?}] \\\r\n         boot[controlled={} driven_by={:?} status={:?}] offlined={offlined:?} ===\",\r\n        after_online.controlled, after_online.driven_by, after_online.status,\r\n        after_offline.controlled, after_offline.driven_by, after_offline.status,\r\n    );\r\n\r\n    // ── Reap scoped (decoy child + spawner). ──\r\n    if let Some(p) = broker.session_pid(decoy_sid) {\r\n        kill_pid(p);\r\n    }\r\n    drop(spawner);\r\n    drop(probe);\r\n\r\n    // Arm A — ONLINE + sticky: BOTH stamps cleared AND the perch offlined.\r\n    // [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\r\n    assert!(\r\n        !after_online.controlled,\r\n        \"B3 arm A: reap must clear controlled on an online sessionless spt-hosted perch\"\r\n    );\r\n    assert_eq!(\r\n        after_online.driven_by, None,\r\n        \"B3 arm A: reap must clear driven_by (attached_node derives from it) on the online perch\"\r\n    );\r\n    assert!(\r\n        offlined.contains(&online_id),\r","truncated":true,"truncatedBy":"lines","totalLines":446,"totalBytes":18956,"outputLines":423,"outputBytes":18956,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! F-026 W1 #2 (REQ-HAZARD-CONTROL-STAMP-LIFETIME) — a control/viewer stamp never\r\n//! outlives its session, on a REAL broker over the real local IPC (no PTY/transport\r\n//! mocks). Red-first: the exit-waiter used to `sessions.remove(&id)` with NO stamp\r\n//! clear, so `/exit` (which kills the CHILD, not the controller conn) left\r\n//! `controlled` / `driven_by` / `viewer_count` latched in info.json forever — and\r\n//! gossiped a stale `controller_node` cross-node.\r\n//!\r\n//! ## The bug this gate locks down\r\n//!\r\n//! `/exit` ends the RC session INDIRECTLY: the endpoint suspends when the child\r\n//! binary terminates, so the teardown runs the REAP path (exit-waiter →\r\n//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\r\n//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\r\n//! path dropped the OutputLog with the controller slot still occupied, so the\r\n//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\r\n//! hall-a's info.json still read `controlled:true` and hfenduleam still gossiped\r\n//! `controller_node=self`.\r\n//!\r\n//! ## What the fix does\r\n//!\r\n//! The exit-waiter calls `OutputLog::stamp_reaped()` on reap, which clears all\r\n//! three stamps (`set_driven_by(None)`, `set_controlled(false)`,\r\n//! `set_viewer_count(0)`) via the known endpoint id. The broker stays the single\r\n//! writer; the session is dead so no live controller re-stamps concurrently (the\r\n//! same race-free argument as the W5 no-session self-heal).\r\n//!\r\n//! ## Why kill the CHILD (not drop the conn)\r\n//!\r\n//! Dropping the spawner conn would run the EXISTING `clear_controller` path and\r\n//! prove nothing about the reap leg. Killing the PTY child fires the exit-waiter\r\n//! while the controller conn stays open — isolating the reap clear as the ONLY\r\n//! path that can unlatch the stamps.\r\n//!\r\n//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\r\n//!\r\n//! Shares ONE process-global `SPT_HOME` (set once in [`init_stamp_home`]) and spins\r\n//! a REAL broker per test; run isolated (the driven_by_selfheal precedent):\r\n//!   - `cargo nextest run --test control_stamp_lifetime`  (CI's path), or\r\n//!   - `cargo test --test control_stamp_lifetime -- --test-threads=1`.\r\n//!\r\n//! Do NOT run a bare `cargo test --test control_stamp_lifetime` on Windows.\r\n\r\nuse std::collections::BTreeSet;\r\nuse std::sync::atomic::{AtomicU32, Ordering};\r\nuse std::sync::{Arc, OnceLock};\r\nuse std::thread;\r\nuse std::time::Duration;\r\n\r\nuse spt_daemon::brain::Brain;\r\nuse spt_daemon::msg::SpawnReq;\r\nuse spt_daemon::Broker;\r\n\r\nuse spt_store::perch::{self, ParentHint};\r\n\r\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\r\nfn unique_name() -> String {\r\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\r\n    format!(\"spt-daemon-ctlstamp-{}-{}.sock\", std::process::id(), n)\r\n}\r\n\r\nstatic STAMP_HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\r\nfn init_stamp_home() {\r\n    STAMP_HOME.get_or_init(|| {\r\n        let dir = tempfile::TempDir::new().expect(\"temp dir\");\r\n        std::env::set_var(\"SPT_HOME\", dir.path());\r\n        dir\r\n    });\r\n}\r\n\r\nfn kill_pid(pid: u32) {\r\n    #[cfg(windows)]\r\n    let _ = std::process::Command::new(\"taskkill\")\r\n        .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])\r\n        .output();\r\n    #[cfg(unix)]\r\n    let _ = std::process::Command::new(\"kill\")\r\n        .args([\"-9\", &pid.to_string()])\r\n        .output();\r\n}\r\n\r\nfn start_broker(name: &str) -> Arc<Broker> {\r\n    let broker = Broker::bind(name).expect(\"bind broker\");\r\n    let serve = Arc::clone(&broker);\r\n    thread::spawn(move || {\r\n        let _ = serve.serve();\r\n    });\r\n    broker\r\n}\r\n\r\nfn connect_brain(name: &str) -> Brain {\r\n    for _ in 0..300 {\r\n        if let Ok(b) = Brain::cold_start(name, 1) {\r\n            return b;\r\n        }\r\n        thread::sleep(Duration::from_millis(10));\r\n    }\r\n    panic!(\"brain could not connect to {name}\");\r\n}\r\n\r\n/// A long-lived stdin-reading PTY child so the spawned session stays LIVE until we\r\n/// kill it (cross-OS).\r\nfn idle_spawn_req(endpoint: &str) -> SpawnReq {\r\n    #[cfg(unix)]\r\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\r\n    #[cfg(windows)]\r\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\r\n    SpawnReq {\r\n        program,\r\n        args,\r\n        rows: 24,\r\n        cols: 80,\r\n        endpoint: endpoint.to_string(),\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}\r\n\r\n/// Create a hosted (`controllable=Some(true)`) `live_agent` self-perch, ONLINE, so\r\n/// the broker's stamps land on a real info.json (mirrors the selfheal rig's stamp).\r\nfn create_hosted_perch(id: &str) -> std::path::PathBuf {\r\n    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\r\n    std::fs::create_dir_all(&self_perch).unwrap();\r\n    let mut rec =\r\n        spt_store::info::InfoJson::new(id, \"t\", std::process::id(), \"sid-ctlstamp\", \"live_agent\");\r\n    rec.controllable = Some(true);\r\n    spt_store::info::write_info(&self_perch, &rec).unwrap();\r\n    spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_ONLINE).unwrap();\r\n    self_perch\r\n}\r\n\r\n#[test]\r\nfn reap_clears_control_and_viewer_stamps() {\r\n    init_stamp_home();\r\n    let node = \"PEERNODEHEX_CTLSTAMP\";\r\n    let id = format!(\"ctlstamp-{}\", std::process::id());\r\n    let self_perch = create_hosted_perch(&id);\r\n\r\n    // ── A REAL broker hosting a LIVE session for THIS endpoint. dispatch_spawn\r\n    //    pre-attaches the spawner as the LOCAL controller (by=None) and its\r\n    //    become_controller → stamp_driven_by stamps `controlled=true` on the perch. ──\r\n    let name = unique_name();\r\n    let broker = start_broker(&name);\r\n    let mut spawner = connect_brain(&name);\r\n    let sid = spawner\r\n        .spawn_session(idle_spawn_req(&id))\r\n        .expect(\"spawn live session for the endpoint\");\r\n\r\n    // Give the spawn's become_controller stamp a beat to land.\r\n    thread::sleep(Duration::from_millis(200));\r\n\r\n    // Precondition (genuine): the live session stamped `controlled=true` (the local\r\n    // controller). This is a REAL broker stamp, not a hand-set value.\r\n    let before = spt_store::info::read_info(&self_perch).expect(\"info present\");\r\n    assert!(\r\n        before.controlled,\r\n        \"precondition: the live hosted session must stamp controlled=true \\\r\n         (become_controller → stamp_driven_by) — got {before:?}\"\r\n    );\r\n\r\n    // Simulate the FULL latch a remote-driven, multi-viewer session leaves in\r\n    // info.json: driven_by=Some(remote) + viewer_count=2 (broker is the single\r\n    // writer of both; here we set the on-disk state the reap must unlatch). The\r\n    // spawner stays attached, idle → nothing re-stamps these before the reap.\r\n    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\r\n    spt_store::info::set_viewer_count(&self_perch, 2).unwrap();\r\n\r\n    let latched = spt_store::info::read_info(&self_perch).expect(\"info present\");\r\n    assert_eq!(\r\n        latched.driven_by.as_deref(),\r\n        Some(node),\r\n        \"precondition: driven_by latched Some before reap\"\r\n    );\r\n    assert_eq!(\r\n        latched.viewer_count,\r\n        Some(2),\r\n        \"precondition: viewer_count latched Some(2) before reap\"\r\n    );\r\n    assert!(latched.controlled, \"precondition: controlled true before reap\");\r\n\r\n    // ── Kill the PTY CHILD → the exit-waiter reaps the session (NOT a conn drop, so\r\n    //    the controller-detach path never runs; only the reap path can unlatch). ──\r\n    let pid = broker\r\n        .session_pid(sid)\r\n        .expect(\"live session must have a pid before kill\");\r\n    kill_pid(pid);\r\n\r\n    // Poll for the reap to complete (exit-waiter runs on its own thread).\r\n    let mut cleared = false;\r\n    for _ in 0..100 {\r\n        thread::sleep(Duration::from_millis(100));\r\n        let info = spt_store::info::read_info(&self_perch).expect(\"info present\");\r\n        if info.driven_by.is_none() && !info.controlled && info.viewer_count.is_none() {\r\n            cleared = true;\r\n            break;\r\n        }\r\n    }\r\n\r\n    let after = spt_store::info::read_info(&self_perch).expect(\"info present after reap\");\r\n    eprintln!(\r\n        \"=== F026 #2 REAP RESULT: driven_by={:?} controlled={} viewer_count={:?} (all cleared=expected) ===\",\r\n        after.driven_by, after.controlled, after.viewer_count\r\n    );\r\n\r\n    drop(spawner);\r\n\r\n    // [int->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\r\n    assert!(\r\n        cleared,\r\n        \"reap must clear every control/viewer stamp within the poll window — \\\r\n         got driven_by={:?} controlled={} viewer_count={:?}\",\r\n        after.driven_by, after.controlled, after.viewer_count\r\n    );\r\n    assert_eq!(\r\n        after.driven_by, None,\r\n        \"reap must clear driven_by (a remote-controller latch must not outlive the session)\"\r\n    );\r\n    assert!(\r\n        !after.controlled,\r\n        \"reap must clear controlled (ONLINE+CONTROLLED must not survive /exit)\"\r\n    );\r\n    assert_eq!(\r\n        after.viewer_count, None,\r\n        \"reap must clear viewer_count (set_viewer_count(0) → None)\"\r\n    );\r\n}\r\n\r\n/// F-026 W1-addendum (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE) — the STAMP-BEFORE-BIND\r\n/// loss: a picker-created endpoint's broker spawn stamps `controlled=true` BEFORE the\r\n/// adapter binds its perch (`mutate_info` NotFound → swallowed); the adapter's bind\r\n/// then writes `controlled:false` and no later edge re-stamps, so the endpoint reads\r\n/// uncontrolled forever while driven (hall-b / the original ball-b report). The\r\n/// `KIND_SESSIONS` poll now CONVERGES the perch to the live session truth,\r\n/// divergence-gated. RED-first: without the convergence hook, the perch stays\r\n/// `controlled:false` forever.\r\n#[test]\r\nfn converge_stamps_on_sessions_poll_after_late_bind() {\r\n    init_stamp_home();\r\n    let id = format!(\"ctlconv-{}\", std::process::id());\r\n\r\n    // A REAL broker + a spawned session for the endpoint — but NO perch yet, so the\r\n    // spawn's become_controller → set_controlled(true) is SWALLOWED (NotFound): the\r\n    // picker-created stamp-before-bind loss.\r\n    let name = unique_name();\r\n    let broker = start_broker(&name);\r\n    let mut spawner = connect_brain(&name);\r\n    let sid = spawner\r\n        .spawn_session(idle_spawn_req(&id))\r\n        .expect(\"spawn live session before its perch exists\");\r\n    thread::sleep(Duration::from_millis(150));\r\n\r\n    // The adapter binds LATE, writing controlled:false (InfoJson::new default) — the\r\n    // stamp the picker-created endpoint is stuck at.\r\n    let self_perch = create_hosted_perch(&id);\r\n    let before = spt_store::info::read_info(&self_perch).expect(\"info present\");\r\n    assert!(\r\n        !before.controlled,\r\n        \"precondition: the late bind wrote controlled=false (the spawn stamp was lost) — got {before:?}\"\r\n    );\r\n\r\n    // A KIND_SESSIONS poll (what the daemon reconcile does each cadence) converges the\r\n    // perch to broker truth: the spawner is the (local) controller ⇒ controlled=true.\r\n    let mut converged = false;\r\n    for _ in 0..50 {\r\n        let mut brain = connect_brain(&name);\r\n        let _ = brain.sessions().expect(\"KIND_SESSIONS reply\");\r\n        thread::sleep(Duration::from_millis(50));\r\n        if spt_store::info::read_info(&self_perch)\r\n            .map(|i| i.controlled)\r\n            .unwrap_or(false)\r\n        {\r\n            converged = true;\r\n            break;\r\n        }\r\n    }\r\n\r\n    let after = spt_store::info::read_info(&self_perch).expect(\"info present after poll\");\r\n    if let Some(pid) = broker.session_pid(sid) {\r\n        kill_pid(pid);\r\n    }\r\n    drop(spawner);\r\n\r\n    // [int->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\n    assert!(\r\n        converged,\r\n        \"the KIND_SESSIONS poll must converge controlled=true within the window — got {after:?}\"\r\n    );\r\n    assert!(\r\n        after.controlled,\r\n        \"controlled converged to the live-session truth (a local controller drives it)\"\r\n    );\r\n    assert_eq!(\r\n        after.driven_by, None,\r\n        \"a LOCAL controller does not latch driven_by (remote-only, KH 7.15); #3 display reads controlled\"\r\n    );\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n// F-028 W3 B3 (REQ-PRESENCE-CONTROL-REAP-ON-EXIT) — wave-final int.\r\n//\r\n// The field bug (perri F-b / deferred #11): a control stamp latched `controlled`\r\n// + `driven_by` on a spt-hosted online live_agent perch AFTER the harness exited,\r\n// persisting >20min AND ACROSS A DAEMON RESTART. This is the DIFFERENT reap leg\r\n// from `reap_clears_control_and_viewer_stamps` above (that drives the broker\r\n// exit-waiter's `stamp_reaped`); B3 is the `reconcile_hosted_liveness` sweep — the\r\n// tick that clears a sticky stamp on a perch whose broker session is simply GONE,\r\n// regardless of status, so an already-offline perch left sticky across a restart is\r\n// swept (path iv, the boot sweep), not only on the online→offline transition.\r\n//\r\n// This int drives the REAL reconcile entry (`spt_daemon::livehost::reconcile_hosted_liveness`)\r\n// against the REAL on-disk owlery, with the live-session set DERIVED FROM A REAL\r\n// BROKER (query_live_session_endpoints's exact shape: `brain.sessions()` →\r\n// endpoint ids). A decoy live session keeps the broker set genuinely NON-empty, so\r\n// the reap fires precisely because the broker reports NO session for the sticky\r\n// perch — not because we hand it an empty set. Two arms:\r\n//   (A) ONLINE + sticky → cleared AND offlined (clean-exit / crash → sessionless).\r\n//   (B) already-OFFLINE + sticky → cleared REGARDLESS of status (the boot sweep; a\r\n//       restarted daemon must not resurrect the stamps).\r\n// [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\r\n\r\n/// Seed a spt-hosted (`controllable=Some(true)`) live_agent self-perch at `status`,\r\n/// carrying the FULL sticky control latch a departed remote-driven session leaves:\r\n/// `controlled=true` + `driven_by=Some(node)`. Returns the perch path.\r\nfn seed_sticky_hosted_perch(id: &str, status: &str, node: &str) -> std::path::PathBuf {\r\n    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\r\n    std::fs::create_dir_all(&self_perch).unwrap();\r\n    let mut rec =\r\n        spt_store::info::InfoJson::new(id, \"t\", std::process::id(), \"sid-b3-sticky\", \"live_agent\");\r\n    rec.controllable = Some(true);\r\n    spt_store::info::write_info(&self_perch, &rec).unwrap();\r\n    spt_store::info::set_status(&self_perch, status).unwrap();\r\n    // The sticky latch the reap must unwind (broker single-writer; here on disk).\r\n    spt_store::info::set_controlled(&self_perch, true).unwrap();\r\n    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\r\n    let seeded = spt_store::info::read_info(&self_perch).expect(\"seeded info present\");\r\n    assert!(\r\n        seeded.controlled && seeded.driven_by.as_deref() == Some(node),\r\n        \"precondition: perch {id} must be seeded sticky (controlled=true + driven_by=Some) — got {seeded:?}\"\r\n    );\r\n    self_perch\r\n}\r\n\r\n#[test]\r\nfn control_reap_clears_sticky_stamps_no_session_online_and_boot_sweep() {\r\n    init_stamp_home();\r\n    let node = \"PEERNODEHEX_B3\";\r\n    let pid = std::process::id();\r\n    // Arm A: ONLINE + sticky (clean-exit / crash → sessionless, online→offline).\r\n    let online_id = format!(\"b3-online-{pid}\");\r\n    // Arm B: already-OFFLINE + sticky (survived a daemon restart — the boot sweep).\r\n    let offline_id = format!(\"b3-boot-{pid}\");\r\n    let online_perch = seed_sticky_hosted_perch(&online_id, spt_store::liveness::STATUS_ONLINE, node);\r\n    let offline_perch =\r\n        seed_sticky_hosted_perch(&offline_id, spt_store::liveness::STATUS_OFFLINE, node);\r\n\r\n    // A REAL broker hosting a DECOY live session — so the session set the reconcile\r\n    // consults is genuinely non-empty and the reap distinguishes \"has session\" from\r\n    // \"no session\" against REAL broker truth (not a hand-passed empty set). The decoy\r\n    // has NO pre-created perch, so it is never scanned/offlined by the reconcile.\r\n    let decoy_id = format!(\"b3-decoy-{pid}\");\r\n    let name = unique_name();\r\n    let broker = start_broker(&name);\r\n    let mut spawner = connect_brain(&name);\r\n    let decoy_sid = spawner\r\n        .spawn_session(idle_spawn_req(&decoy_id))\r\n        .expect(\"spawn the decoy live session\");\r\n    thread::sleep(Duration::from_millis(200));\r\n\r\n    // Build the live-session set the daemon's reconcile uses — the exact shape of\r\n    // `query_live_session_endpoints`: KIND_SESSIONS reply → endpoint ids.\r\n    let mut probe = connect_brain(&name);\r\n    let reply = probe.sessions().expect(\"KIND_SESSIONS reply\");\r\n    let live: BTreeSet<String> = reply\r\n        .sessions\r\n        .into_iter()\r\n        .map(|s| s.endpoint)\r\n        .filter(|e| !e.is_empty())\r\n        .collect();\r\n    // Faithful precondition: the broker DOES host the decoy, and does NOT host a\r\n    // session for either sticky perch (so the reap must fire on both).\r\n    assert!(\r\n        live.contains(&decoy_id),\r\n        \"precondition: the real broker must report the decoy session live — got {live:?}\"\r\n    );\r\n    assert!(\r\n        !live.contains(&online_id) && !live.contains(&offline_id),\r\n        \"precondition: neither sticky perch has a broker session — got {live:?}\"\r\n    );\r\n\r\n    // ── Drive the REAL reconcile sweep (the daemon runs this from boot tick 1). ──\r\n    let offlined =\r\n        spt_daemon::livehost::reconcile_hosted_liveness(&spt_store::perch::owlery_dir(), &live);\r\n\r\n    let after_online = spt_store::info::read_info(&online_perch).expect(\"online info present\");\r\n    let after_offline = spt_store::info::read_info(&offline_perch).expect(\"offline info present\");\r\n    eprintln!(\r\n        \"=== F028 B3 REAP: online[controlled={} driven_by={:?} status={:?}] \\\r\n         boot[controlled={} driven_by={:?} status={:?}] offlined={offlined:?} ===\",\r\n        after_online.controlled, after_online.driven_by, after_online.status,\r\n        after_offline.controlled, after_offline.driven_by, after_offline.status,\r\n    );\r\n\r\n    // ── Reap scoped (decoy child + spawner). ──\r\n    if let Some(p) = broker.session_pid(decoy_sid) {\r\n        kill_pid(p);\r\n    }\r\n    drop(spawner);\r\n    drop(probe);\r\n\r\n    // Arm A — ONLINE + sticky: BOTH stamps cleared AND the perch offlined.\r\n    // [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\r\n    assert!(\r\n        !after_online.controlled,\r\n        \"B3 arm A: reap must clear controlled on an online sessionless spt-hosted perch\"\r\n    );\r\n    assert_eq!(\r\n        after_online.driven_by, None,\r\n        \"B3 arm A: reap must clear driven_by (attached_node derives from it) on the online perch\"\r\n    );\r\n    assert!(\r\n        offlined.contains(&online_id),\r\n…\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,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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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,null,425,null,445]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\control_stamp_lifetime.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":446,"totalBytes":18956,"outputLines":423,"outputBytes":18956,"shownRange":{"start":1,"end":423},"nextOffset":424}}},"isError":false,"timestamp":1784195854278}}
{"type":"message","id":"c03287e0","parentId":"7627d10d","timestamp":"2026-07-16T09:57:34.299Z","message":{"role":"toolResult","toolCallId":"call_e61G0G8aalaByGpWy4w3mdgY|fc_047bb49263dfa303016a58ab0d2a8081988fccc40c9d4bd2a2","toolName":"read","content":[{"type":"text","text":"    // (doyle-ruled single-node fixture): a broker death under a live viewport\n    // auto-reconnects — the capture shows pre-bounce output, then the centered\n    // reconnect banner, then post-bounce output from the re-established serve,\n    // and the viewport still ends HONESTLY (session exit → Exited). Everything\n    // is in-process against an isolated home: the broker + seed sockets are\n    // home-tagged (no live-box collision), and the seed channel stays UP through\n    // the bounce so establish_attach's CONNECT-ONLY is_running() probe stays\n    // satisfied (REQ-RC-RECONNECT-TRUTH: rc never auto-starts a daemon — the seed\n    // channel up lets the retry re-connect the broker child; a seed death would\n    // instead yield DaemonDown + a bounded retry with NO resurrection — the gated\n    // step's remote leg owns the real-daemon `spt daemon stop` shape at B).\n    #[test]\n    // Both broker children are kill()+wait()ed on the assert path; a panicking\n    // test leaks them only until process teardown (their pipes die with us).\n    #[allow(clippy::zombie_processes)]\n    fn attach_viewport_reconnects_across_a_broker_bounce() {\n        let _home = crate::testutil::isolated_home();\n        let _seed = SeedGuard::start();\n        let broker_name = spt_daemon::endpoint::broker_socket_name();\n        let flag = spt_store::perch::spt_home().join(\"reheal.flag\");\n\n        #[cfg(unix)]\n        let (prog, args) = (\n            \"sh\".to_string(),\n            vec![\n                \"-c\".to_string(),\n                format!(\n                    \"while [ ! -f '{}' ]; do echo tick; sleep 0.2; done\",\n                    flag.display()\n                ),\n            ],\n        );\n        #[cfg(windows)]\n        let (prog, args) = (\n            \"cmd\".to_string(),\n            vec![\n                \"/c\".to_string(),\n                format!(\n                    \"for /l %i in (0,0,1) do @(if exist \\\"{}\\\" (exit) else (echo tick & ping -n 2 127.0.0.1 >nul))\",\n                    flag.display()\n                ),\n            ],\n        );\n        // Returns the OWNING brain too — the finish kills the session through it\n        // (KIND_KILL requires the spawner's session binding).\n        let spawn_ticker = |broker_name: &str| -> (spt_daemon::brain::Brain, u64) {\n            let mut host = spt_daemon::brain::Brain::cold_start(broker_name, 1)\n                .expect(\"host brain connects\");\n            let sid = host.spawn_session(spt_daemon::msg::SpawnReq {\n                program: prog.clone(),\n                args: args.clone(),\n                rows: 24,\n                cols: 80,\n                endpoint: \"reheal\".to_string(),\n                cwd: None,\n                env: Default::default(),\n                translation_binary: None,\n                adapter: String::new(),\n                install_dir: None,\n            })\n            .expect(\"spawn ticker session\");\n            (host, sid)\n        };\n\n        // The broker lives in a KILLABLE CHILD (the B-2 attacher re-exec\n        // pattern, inverted): an in-process broker can't model a daemon death —\n        // Broker::stop halts the ACCEPT loop, but per-conn handler threads in\n        // THIS process keep answering open conns (the viewport never sees an\n        // EOF, so never BrokerGone) AND their parked read_frame instances pin\n        // the pipe name against the rebind. A child-process broker dies like\n        // the real daemon: every pipe breaks at once, the name frees, the\n        // viewport's poll errors into the reconnect loop honestly.\n        let spawn_broker_child = || {\n            let exe = std::env::current_exe().expect(\"current test exe\");\n            let child = std::process::Command::new(&exe)\n                .args([\"rc::tests::broker_host_child\", \"--exact\", \"--nocapture\"])\n                .env(\"SPT_RC_FIXTURE_BROKER_CHILD\", \"1\")\n                .spawn()\n                .expect(\"spawn broker-host child\");\n            // Barrier: the broker answers a brain connect (bounded).\n            let end = Instant::now() + Duration::from_secs(30);\n            loop {\n                if let Ok(b) = spt_daemon::brain::Brain::cold_start(&broker_name, 1) {\n                    drop(b);\n                    break child;\n                }\n                assert!(Instant::now() < end, \"broker child never came up\");\n                std::thread::sleep(Duration::from_millis(50));\n            }\n        };\n        // The DISPATCHER serves the attach streams (the broker alone answers no\n        // stream Requests — production's daemon runs this loop; without it the\n        // viewport sees zero records and honestly Stalls). One per broker\n        // generation, stop-flagged across the bounce.\n        let start_dispatch = |name: String| {\n            let registry = std::sync::Arc::new(spt_daemon::registryhost::RegistryHost::new_at(\n                \"fixturenode\",\n                spt_store::epoch::EpochSource::load_from(&spt_store::perch::epoch_file()),\n                spt_daemon::presence::registry_snapshot_dir(),\n            ));\n            let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));\n            let flag = std::sync::Arc::clone(&stop);\n            let paths = spt_daemon::dispatch::DispatchPaths {\n                owlery: spt_store::perch::owlery_dir(),\n                releases: spt_store::perch::spt_home().join(\"releases\"),\n                scratch: spt_store::perch::spt_home().join(\"dispatch-scratch\"),\n            };\n            std::thread::spawn(move || {\n                let _ = spt_daemon::dispatch::run_dispatch_loop(\n                    &name,\n                    &paths,\n                    registry,\n                    &flag,\n                    Duration::from_millis(50),\n                );\n            });\n            stop\n        };\n\n        let mut broker_child1 = spawn_broker_child();\n        let dispatch1_stop = start_dispatch(broker_name.clone());\n        let (_host1, _sid1) = spawn_ticker(&broker_name);\n\n        // Drive the REAL loop headless: viewer intent (no stdin reader, no\n        // status row), the capture buffer as the out sink.\n        let cap = Capture::default();\n        let mut cap_out = cap.clone();\n        let viewport = std::thread::spawn(move || {\n            let est = establish_attach(\"reheal\", AttachIntent::Viewer).map_err(|e| {\n                let msg = match e {\n                    EstablishFail::NoTarget(m) => format!(\"establish no-target: {m}\"),\n                    EstablishFail::DaemonDown => \"establish daemon-down\".to_string(),\n                    EstablishFail::Error(m) => format!(\"establish error: {m}\"),\n                };\n                msg\n            })?;\n            let mouse = MouseMode::default();\n            let mut remote = est.remote_node.clone();\n            attach_viewport(\n                \"reheal\",\n                AttachIntent::Viewer,\n                true,\n                est,\n                None,\n                &mouse,\n                None,\n                &mut remote,\n                &mut cap_out,\n            )\n        });\n\n        let wait_for = |viewport: &std::thread::JoinHandle<Result<PumpEnd, String>>,\n                        what: &str,\n                        probe: &dyn Fn(&[u8]) -> bool| {\n            let end = Instant::now() + Duration::from_secs(60);\n            loop {\n                let snap = cap.snapshot();\n                if probe(&snap) {\n                    return snap;\n                }\n                assert!(\n                    Instant::now() < end,\n                    \"never observed: {what} (viewport finished: {}; {} bytes captured: {:?})\",\n                    viewport.is_finished(),\n                    snap.len(),\n                    String::from_utf8_lossy(&snap[..snap.len().min(200)])\n                );\n                std::thread::sleep(Duration::from_millis(50));\n            }\n        };\n        let pre = wait_for(&viewport, \"pre-bounce ticker output\", &|s| {\n            String::from_utf8_lossy(s).contains(\"tick\")\n        });\n\n        // BOUNCE: the broker CHILD dies (seed stays up) — every pipe breaks at\n        // once, exactly like a daemon death. The viewport's poll errors →\n        // BrokerGone → the reconnect loop paints the banner and re-drives\n        // establish; the successor child + respawn land well inside the 30s\n        // window.\n        dispatch1_stop.store(true, std::sync::atomic::Ordering::Relaxed);\n        broker_child1.kill().expect(\"kill broker child 1\");\n        let _ = broker_child1.wait();\n        let mut _broker_child2 = spawn_broker_child();\n        let _dispatch2_stop = start_dispatch(broker_name.clone());\n        let (mut host2, _sid2) = spawn_ticker(&broker_name);\n\n        let banner_needle = b\"Reconnecting to \";\n        let full = wait_for(&viewport, \"the reconnect banner + post-bounce output\", &|s| {\n            s.windows(banner_needle.len())\n                .position(|w| w == banner_needle)\n                .is_some_and(|i| String::from_utf8_lossy(&s[i..]).contains(\"tick\"))\n        });\n        // Ordering proof: output BEFORE the banner (the live pre-bounce\n        // viewport), the banner, then output AFTER it (the re-established one).\n        let banner_at = full\n            .windows(banner_needle.len())\n            .position(|w| w == banner_needle)\n            .expect(\"banner present\");\n        assert!(\n            banner_at >= pre.len(),\n            \"the banner paints after pre-bounce output\"\n        );\n        assert!(\n            String::from_utf8_lossy(&full[..banner_at]).contains(\"tick\"),\n            \"pre-bounce ticker output rendered before the banner\"\n        );\n\n        // Honest finish: the ticker exits on the flag → Exit record → the\n        // viewport returns Exited (a FINAL end — never re-driven).\n        std::fs::write(&flag, b\"done\").expect(\"write exit flag\");\n        host2.kill_session().expect(\"kill the ticker session\");\n        let outcome = viewport.join().expect(\"viewport thread joins\");\n        // Reap the successor child (never leak a broker past the test — the\n        // e2e-leaked-daemon class).\n        let _ = _broker_child2.kill();\n        let _ = _broker_child2.wait();\n        match outcome {\n            Ok(PumpEnd::Exited(_)) => {}\n            other => panic!(\"expected the session exit to END the viewport, got {other:?}\"),\n        }\n    }\n\n    /// The reheal fixture's BROKER-HOST child — spawned by the fixture above via\n    /// re-exec with `SPT_RC_FIXTURE_BROKER_CHILD=1` and the ISOLATED home\n    /// inherited through `SPT_HOME`, then KILLED to model a real daemon death\n    /// (an in-process broker cannot die honestly: its per-conn handler threads\n    /// outlive `stop()`, so the viewport never sees an EOF and the parked\n    /// read_frame instances pin the pipe name against the rebind). Binds the\n    /// canonical broker socket + a hermetic net host (loopback scope — the\n    /// net_dial_loopback carrier) and serves until killed. Env-gated: inert in\n    /// every normal sweep.\n    #[test]\n    fn broker_host_child() {\n        if std::env::var(\"SPT_RC_FIXTURE_BROKER_CHILD\").as_deref() != Ok(\"1\") {\n            return;\n        }\n        let name = spt_daemon::endpoint::broker_socket_name();\n        let host = spt_daemon::nethost::NetHost::start(spt_daemon::nethost::NetConfig {\n            identity: spt_proto::identity::Identity::from_seed(&[9u8; 32]),\n            relay: spt_net::net::endpoint::RelayPolicy::Disabled,\n            discovery: spt_net::net::endpoint::LocalDiscovery::Off,\n            scope: spt_net::net::endpoint::BindScope::Loopback,\n            membership: None,\n        })\n        .expect(\"hermetic net host\");\n        let broker = spt_daemon::broker::Broker::bind_in_with_net(\n            &name,\n            spt_store::perch::spt_home().join(\"effects.log\"),\n            Some(host),\n        )\n        .expect(\"bind child broker\");\n        // Serve until killed — the parent's kill IS the bounce.\n        let _ = broker.serve();\n    }\n\n    // [unit->REQ-RC-RECONNECT] B-3 red-first: ONLY the severed-transport ends\n    // re-drive the viewport — BrokerGone (broker bounce) and Severed (remote conn\n    // drop). Session exit / operator detach / displacement / the stall backstop /\n    // an attach refuse are FINAL: re-driving those would re-attach to a session\n    // the operator (or the session itself) deliberately ended.\n    #[test]\n    fn reconnect_class_only_severed_transport_redrives() {\n        assert_eq!(reconnect_class(&PumpEnd::BrokerGone), EndClass::Reconnect);\n        assert_eq!(reconnect_class(&PumpEnd::Severed), EndClass::Reconnect);\n        assert_eq!(reconnect_class(&PumpEnd::Exited(Some(0))), EndClass::Final);\n        assert_eq!(reconnect_class(&PumpEnd::Detached), EndClass::Final);\n        assert_eq!(\n            reconnect_class(&PumpEnd::Displaced(\"node\".into())),\n            EndClass::Final\n        );\n        assert_eq!(reconnect_class(&PumpEnd::Stalled), EndClass::Final);\n        assert_eq!(reconnect_class(&PumpEnd::NoLiveSession), EndClass::Final);\n        assert_eq!(\n            reconnect_class(&PumpEnd::ReconnectGaveUp {\n                detail: \"x\".into(),\n                daemon_down: false\n            }),\n            EndClass::Final\n        );\n    }\n\n    // [unit->REQ-RC-RECONNECT] B-3 red-first vs the pre-fix mislabel: a serve-side\n    // EOF AFTER rendered output is a SEVERED conn (reconnect) — previously a false\n    // \"detached — still running\". A nothing-rendered EOF stays the honest attach\n    // refuse (the #4/D6 stale-row case), NOT a reconnect candidate.\n    #[test]\n    fn serve_eof_severed_vs_refused() {\n        assert!(matches!(serve_eof_end(true), PumpEnd::Severed));\n        assert!(matches!(serve_eof_end(false), PumpEnd::NoLiveSession));\n    }\n\n    // [unit->REQ-RC-RECONNECT] the give-up decision: bounded window, boundary\n    // inclusive — unit-tested without a 30s wait.\n    #[test]\n    fn reconnect_window_expiry_decision() {\n        assert!(!reconnect_expired(Duration::from_secs(0)));\n        assert!(!reconnect_expired(RECONNECT_WINDOW - Duration::from_millis(1)));\n        assert!(reconnect_expired(RECONNECT_WINDOW));\n        assert!(reconnect_expired(RECONNECT_WINDOW + Duration::from_secs(1)));\n    }\n\n    // [unit->REQ-RC-RECONNECT] the reconnect banner: full clear + home, the text\n    // CENTERED on the terminal (row rows/2, col centered on the char count), and\n    // an over-wide text clamps to column 1 instead of underflowing off-screen.\n    #[test]\n\n[Showing lines 2129-2433 of 3214. Use :2434 to continue]"}],"details":{"truncation":{"content":"    // (doyle-ruled single-node fixture): a broker death under a live viewport\r\n    // auto-reconnects — the capture shows pre-bounce output, then the centered\r\n    // reconnect banner, then post-bounce output from the re-established serve,\r\n    // and the viewport still ends HONESTLY (session exit → Exited). Everything\r\n    // is in-process against an isolated home: the broker + seed sockets are\r\n    // home-tagged (no live-box collision), and the seed channel stays UP through\r\n    // the bounce so establish_attach's CONNECT-ONLY is_running() probe stays\r\n    // satisfied (REQ-RC-RECONNECT-TRUTH: rc never auto-starts a daemon — the seed\r\n    // channel up lets the retry re-connect the broker child; a seed death would\r\n    // instead yield DaemonDown + a bounded retry with NO resurrection — the gated\r\n    // step's remote leg owns the real-daemon `spt daemon stop` shape at B).\r\n    #[test]\r\n    // Both broker children are kill()+wait()ed on the assert path; a panicking\r\n    // test leaks them only until process teardown (their pipes die with us).\r\n    #[allow(clippy::zombie_processes)]\r\n    fn attach_viewport_reconnects_across_a_broker_bounce() {\r\n        let _home = crate::testutil::isolated_home();\r\n        let _seed = SeedGuard::start();\r\n        let broker_name = spt_daemon::endpoint::broker_socket_name();\r\n        let flag = spt_store::perch::spt_home().join(\"reheal.flag\");\r\n\r\n        #[cfg(unix)]\r\n        let (prog, args) = (\r\n            \"sh\".to_string(),\r\n            vec![\r\n                \"-c\".to_string(),\r\n                format!(\r\n                    \"while [ ! -f '{}' ]; do echo tick; sleep 0.2; done\",\r\n                    flag.display()\r\n                ),\r\n            ],\r\n        );\r\n        #[cfg(windows)]\r\n        let (prog, args) = (\r\n            \"cmd\".to_string(),\r\n            vec![\r\n                \"/c\".to_string(),\r\n                format!(\r\n                    \"for /l %i in (0,0,1) do @(if exist \\\"{}\\\" (exit) else (echo tick & ping -n 2 127.0.0.1 >nul))\",\r\n                    flag.display()\r\n                ),\r\n            ],\r\n        );\r\n        // Returns the OWNING brain too — the finish kills the session through it\r\n        // (KIND_KILL requires the spawner's session binding).\r\n        let spawn_ticker = |broker_name: &str| -> (spt_daemon::brain::Brain, u64) {\r\n            let mut host = spt_daemon::brain::Brain::cold_start(broker_name, 1)\r\n                .expect(\"host brain connects\");\r\n            let sid = host.spawn_session(spt_daemon::msg::SpawnReq {\r\n                program: prog.clone(),\r\n                args: args.clone(),\r\n                rows: 24,\r\n                cols: 80,\r\n                endpoint: \"reheal\".to_string(),\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 ticker session\");\r\n            (host, sid)\r\n        };\r\n\r\n        // The broker lives in a KILLABLE CHILD (the B-2 attacher re-exec\r\n        // pattern, inverted): an in-process broker can't model a daemon death —\r\n        // Broker::stop halts the ACCEPT loop, but per-conn handler threads in\r\n        // THIS process keep answering open conns (the viewport never sees an\r\n        // EOF, so never BrokerGone) AND their parked read_frame instances pin\r\n        // the pipe name against the rebind. A child-process broker dies like\r\n        // the real daemon: every pipe breaks at once, the name frees, the\r\n        // viewport's poll errors into the reconnect loop honestly.\r\n        let spawn_broker_child = || {\r\n            let exe = std::env::current_exe().expect(\"current test exe\");\r\n            let child = std::process::Command::new(&exe)\r\n                .args([\"rc::tests::broker_host_child\", \"--exact\", \"--nocapture\"])\r\n                .env(\"SPT_RC_FIXTURE_BROKER_CHILD\", \"1\")\r\n                .spawn()\r\n                .expect(\"spawn broker-host child\");\r\n            // Barrier: the broker answers a brain connect (bounded).\r\n            let end = Instant::now() + Duration::from_secs(30);\r\n            loop {\r\n                if let Ok(b) = spt_daemon::brain::Brain::cold_start(&broker_name, 1) {\r\n                    drop(b);\r\n                    break child;\r\n                }\r\n                assert!(Instant::now() < end, \"broker child never came up\");\r\n                std::thread::sleep(Duration::from_millis(50));\r\n            }\r\n        };\r\n        // The DISPATCHER serves the attach streams (the broker alone answers no\r\n        // stream Requests — production's daemon runs this loop; without it the\r\n        // viewport sees zero records and honestly Stalls). One per broker\r\n        // generation, stop-flagged across the bounce.\r\n        let start_dispatch = |name: String| {\r\n            let registry = std::sync::Arc::new(spt_daemon::registryhost::RegistryHost::new_at(\r\n                \"fixturenode\",\r\n                spt_store::epoch::EpochSource::load_from(&spt_store::perch::epoch_file()),\r\n                spt_daemon::presence::registry_snapshot_dir(),\r\n            ));\r\n            let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));\r\n            let flag = std::sync::Arc::clone(&stop);\r\n            let paths = spt_daemon::dispatch::DispatchPaths {\r\n                owlery: spt_store::perch::owlery_dir(),\r\n                releases: spt_store::perch::spt_home().join(\"releases\"),\r\n                scratch: spt_store::perch::spt_home().join(\"dispatch-scratch\"),\r\n            };\r\n            std::thread::spawn(move || {\r\n                let _ = spt_daemon::dispatch::run_dispatch_loop(\r\n                    &name,\r\n                    &paths,\r\n                    registry,\r\n                    &flag,\r\n                    Duration::from_millis(50),\r\n                );\r\n            });\r\n            stop\r\n        };\r\n\r\n        let mut broker_child1 = spawn_broker_child();\r\n        let dispatch1_stop = start_dispatch(broker_name.clone());\r\n        let (_host1, _sid1) = spawn_ticker(&broker_name);\r\n\r\n        // Drive the REAL loop headless: viewer intent (no stdin reader, no\r\n        // status row), the capture buffer as the out sink.\r\n        let cap = Capture::default();\r\n        let mut cap_out = cap.clone();\r\n        let viewport = std::thread::spawn(move || {\r\n            let est = establish_attach(\"reheal\", AttachIntent::Viewer).map_err(|e| {\r\n                let msg = match e {\r\n                    EstablishFail::NoTarget(m) => format!(\"establish no-target: {m}\"),\r\n                    EstablishFail::DaemonDown => \"establish daemon-down\".to_string(),\r\n                    EstablishFail::Error(m) => format!(\"establish error: {m}\"),\r\n                };\r\n                msg\r\n            })?;\r\n            let mouse = MouseMode::default();\r\n            let mut remote = est.remote_node.clone();\r\n            attach_viewport(\r\n                \"reheal\",\r\n                AttachIntent::Viewer,\r\n                true,\r\n                est,\r\n                None,\r\n                &mouse,\r\n                None,\r\n                &mut remote,\r\n                &mut cap_out,\r\n            )\r\n        });\r\n\r\n        let wait_for = |viewport: &std::thread::JoinHandle<Result<PumpEnd, String>>,\r\n                        what: &str,\r\n                        probe: &dyn Fn(&[u8]) -> bool| {\r\n            let end = Instant::now() + Duration::from_secs(60);\r\n            loop {\r\n                let snap = cap.snapshot();\r\n                if probe(&snap) {\r\n                    return snap;\r\n                }\r\n                assert!(\r\n                    Instant::now() < end,\r\n                    \"never observed: {what} (viewport finished: {}; {} bytes captured: {:?})\",\r\n                    viewport.is_finished(),\r\n                    snap.len(),\r\n                    String::from_utf8_lossy(&snap[..snap.len().min(200)])\r\n                );\r\n                std::thread::sleep(Duration::from_millis(50));\r\n            }\r\n        };\r\n        let pre = wait_for(&viewport, \"pre-bounce ticker output\", &|s| {\r\n            String::from_utf8_lossy(s).contains(\"tick\")\r\n        });\r\n\r\n        // BOUNCE: the broker CHILD dies (seed stays up) — every pipe breaks at\r\n        // once, exactly like a daemon death. The viewport's poll errors →\r\n        // BrokerGone → the reconnect loop paints the banner and re-drives\r\n        // establish; the successor child + respawn land well inside the 30s\r\n        // window.\r\n        dispatch1_stop.store(true, std::sync::atomic::Ordering::Relaxed);\r\n        broker_child1.kill().expect(\"kill broker child 1\");\r\n        let _ = broker_child1.wait();\r\n        let mut _broker_child2 = spawn_broker_child();\r\n        let _dispatch2_stop = start_dispatch(broker_name.clone());\r\n        let (mut host2, _sid2) = spawn_ticker(&broker_name);\r\n\r\n        let banner_needle = b\"Reconnecting to \";\r\n        let full = wait_for(&viewport, \"the reconnect banner + post-bounce output\", &|s| {\r\n            s.windows(banner_needle.len())\r\n                .position(|w| w == banner_needle)\r\n                .is_some_and(|i| String::from_utf8_lossy(&s[i..]).contains(\"tick\"))\r\n        });\r\n        // Ordering proof: output BEFORE the banner (the live pre-bounce\r\n        // viewport), the banner, then output AFTER it (the re-established one).\r\n        let banner_at = full\r\n            .windows(banner_needle.len())\r\n            .position(|w| w == banner_needle)\r\n            .expect(\"banner present\");\r\n        assert!(\r\n            banner_at >= pre.len(),\r\n            \"the banner paints after pre-bounce output\"\r\n        );\r\n        assert!(\r\n            String::from_utf8_lossy(&full[..banner_at]).contains(\"tick\"),\r\n            \"pre-bounce ticker output rendered before the banner\"\r\n        );\r\n\r\n        // Honest finish: the ticker exits on the flag → Exit record → the\r\n        // viewport returns Exited (a FINAL end — never re-driven).\r\n        std::fs::write(&flag, b\"done\").expect(\"write exit flag\");\r\n        host2.kill_session().expect(\"kill the ticker session\");\r\n        let outcome = viewport.join().expect(\"viewport thread joins\");\r\n        // Reap the successor child (never leak a broker past the test — the\r\n        // e2e-leaked-daemon class).\r\n        let _ = _broker_child2.kill();\r\n        let _ = _broker_child2.wait();\r\n        match outcome {\r\n            Ok(PumpEnd::Exited(_)) => {}\r\n            other => panic!(\"expected the session exit to END the viewport, got {other:?}\"),\r\n        }\r\n    }\r\n\r\n    /// The reheal fixture's BROKER-HOST child — spawned by the fixture above via\r\n    /// re-exec with `SPT_RC_FIXTURE_BROKER_CHILD=1` and the ISOLATED home\r\n    /// inherited through `SPT_HOME`, then KILLED to model a real daemon death\r\n    /// (an in-process broker cannot die honestly: its per-conn handler threads\r\n    /// outlive `stop()`, so the viewport never sees an EOF and the parked\r\n    /// read_frame instances pin the pipe name against the rebind). Binds the\r\n    /// canonical broker socket + a hermetic net host (loopback scope — the\r\n    /// net_dial_loopback carrier) and serves until killed. Env-gated: inert in\r\n    /// every normal sweep.\r\n    #[test]\r\n    fn broker_host_child() {\r\n        if std::env::var(\"SPT_RC_FIXTURE_BROKER_CHILD\").as_deref() != Ok(\"1\") {\r\n            return;\r\n        }\r\n        let name = spt_daemon::endpoint::broker_socket_name();\r\n        let host = spt_daemon::nethost::NetHost::start(spt_daemon::nethost::NetConfig {\r\n            identity: spt_proto::identity::Identity::from_seed(&[9u8; 32]),\r\n            relay: spt_net::net::endpoint::RelayPolicy::Disabled,\r\n            discovery: spt_net::net::endpoint::LocalDiscovery::Off,\r\n            scope: spt_net::net::endpoint::BindScope::Loopback,\r\n            membership: None,\r\n        })\r\n        .expect(\"hermetic net host\");\r\n        let broker = spt_daemon::broker::Broker::bind_in_with_net(\r\n            &name,\r\n            spt_store::perch::spt_home().join(\"effects.log\"),\r\n            Some(host),\r\n        )\r\n        .expect(\"bind child broker\");\r\n        // Serve until killed — the parent's kill IS the bounce.\r\n        let _ = broker.serve();\r\n    }\r\n\r\n    // [unit->REQ-RC-RECONNECT] B-3 red-first: ONLY the severed-transport ends\r\n    // re-drive the viewport — BrokerGone (broker bounce) and Severed (remote conn\r\n    // drop). Session exit / operator detach / displacement / the stall backstop /\r\n    // an attach refuse are FINAL: re-driving those would re-attach to a session\r\n    // the operator (or the session itself) deliberately ended.\r\n    #[test]\r\n    fn reconnect_class_only_severed_transport_redrives() {\r\n        assert_eq!(reconnect_class(&PumpEnd::BrokerGone), EndClass::Reconnect);\r\n        assert_eq!(reconnect_class(&PumpEnd::Severed), EndClass::Reconnect);\r\n        assert_eq!(reconnect_class(&PumpEnd::Exited(Some(0))), EndClass::Final);\r\n        assert_eq!(reconnect_class(&PumpEnd::Detached), EndClass::Final);\r\n        assert_eq!(\r\n            reconnect_class(&PumpEnd::Displaced(\"node\".into())),\r\n            EndClass::Final\r\n        );\r\n        assert_eq!(reconnect_class(&PumpEnd::Stalled), EndClass::Final);\r\n        assert_eq!(reconnect_class(&PumpEnd::NoLiveSession), EndClass::Final);\r\n        assert_eq!(\r\n            reconnect_class(&PumpEnd::ReconnectGaveUp {\r\n                detail: \"x\".into(),\r\n                daemon_down: false\r\n            }),\r\n            EndClass::Final\r\n        );\r\n    }\r\n\r\n    // [unit->REQ-RC-RECONNECT] B-3 red-first vs the pre-fix mislabel: a serve-side\r\n    // EOF AFTER rendered output is a SEVERED conn (reconnect) — previously a false\r\n    // \"detached — still running\". A nothing-rendered EOF stays the honest attach\r\n    // refuse (the #4/D6 stale-row case), NOT a reconnect candidate.\r\n    #[test]\r\n    fn serve_eof_severed_vs_refused() {\r\n        assert!(matches!(serve_eof_end(true), PumpEnd::Severed));\r\n        assert!(matches!(serve_eof_end(false), PumpEnd::NoLiveSession));\r\n    }\r\n\r\n    // [unit->REQ-RC-RECONNECT] the give-up decision: bounded window, boundary\r\n    // inclusive — unit-tested without a 30s wait.\r\n    #[test]\r\n    fn reconnect_window_expiry_decision() {\r\n        assert!(!reconnect_expired(Duration::from_secs(0)));\r\n        assert!(!reconnect_expired(RECONNECT_WINDOW - Duration::from_millis(1)));\r\n        assert!(reconnect_expired(RECONNECT_WINDOW));\r\n        assert!(reconnect_expired(RECONNECT_WINDOW + Duration::from_secs(1)));\r\n    }\r\n\r\n    // [unit->REQ-RC-RECONNECT] the reconnect banner: full clear + home, the text\r\n    // CENTERED on the terminal (row rows/2, col centered on the char count), and\r\n    // an over-wide text clamps to column 1 instead of underflowing off-screen.\r\n    #[test]\r","truncated":true,"truncatedBy":"lines","totalLines":1086,"totalBytes":14818,"outputLines":305,"outputBytes":14818,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    // (doyle-ruled single-node fixture): a broker death under a live viewport\r\n    // auto-reconnects — the capture shows pre-bounce output, then the centered\r\n    // reconnect banner, then post-bounce output from the re-established serve,\r\n    // and the viewport still ends HONESTLY (session exit → Exited). Everything\r\n    // is in-process against an isolated home: the broker + seed sockets are\r\n    // home-tagged (no live-box collision), and the seed channel stays UP through\r\n    // the bounce so establish_attach's CONNECT-ONLY is_running() probe stays\r\n    // satisfied (REQ-RC-RECONNECT-TRUTH: rc never auto-starts a daemon — the seed\r\n    // channel up lets the retry re-connect the broker child; a seed death would\r\n    // instead yield DaemonDown + a bounded retry with NO resurrection — the gated\r\n    // step's remote leg owns the real-daemon `spt daemon stop` shape at B).\r\n    #[test]\r\n    // Both broker children are kill()+wait()ed on the assert path; a panicking\r\n    // test leaks them only until process teardown (their pipes die with us).\r\n    #[allow(clippy::zombie_processes)]\r\n    fn attach_viewport_reconnects_across_a_broker_bounce() {\r\n        let _home = crate::testutil::isolated_home();\r\n        let _seed = SeedGuard::start();\r\n        let broker_name = spt_daemon::endpoint::broker_socket_name();\r\n        let flag = spt_store::perch::spt_home().join(\"reheal.flag\");\r\n\r\n        #[cfg(unix)]\r\n        let (prog, args) = (\r\n            \"sh\".to_string(),\r\n            vec![\r\n                \"-c\".to_string(),\r\n                format!(\r\n                    \"while [ ! -f '{}' ]; do echo tick; sleep 0.2; done\",\r\n                    flag.display()\r\n                ),\r\n            ],\r\n        );\r\n        #[cfg(windows)]\r\n        let (prog, args) = (\r\n            \"cmd\".to_string(),\r\n            vec![\r\n                \"/c\".to_string(),\r\n                format!(\r\n                    \"for /l %i in (0,0,1) do @(if exist \\\"{}\\\" (exit) else (echo tick & ping -n 2 127.0.0.1 >nul))\",\r\n                    flag.display()\r\n                ),\r\n            ],\r\n        );\r\n        // Returns the OWNING brain too — the finish kills the session through it\r\n        // (KIND_KILL requires the spawner's session binding).\r\n        let spawn_ticker = |broker_name: &str| -> (spt_daemon::brain::Brain, u64) {\r\n            let mut host = spt_daemon::brain::Brain::cold_start(broker_name, 1)\r\n                .expect(\"host brain connects\");\r\n            let sid = host.spawn_session(spt_daemon::msg::SpawnReq {\r\n                program: prog.clone(),\r\n                args: args.clone(),\r\n                rows: 24,\r\n                cols: 80,\r\n                endpoint: \"reheal\".to_string(),\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 ticker session\");\r\n            (host, sid)\r\n        };\r\n\r\n        // The broker lives in a KILLABLE CHILD (the B-2 attacher re-exec\r\n        // pattern, inverted): an in-process broker can't model a daemon death —\r\n        // Broker::stop halts the ACCEPT loop, but per-conn handler threads in\r\n        // THIS process keep answering open conns (the viewport never sees an\r\n        // EOF, so never BrokerGone) AND their parked read_frame instances pin\r\n        // the pipe name against the rebind. A child-process broker dies like\r\n        // the real daemon: every pipe breaks at once, the name frees, the\r\n        // viewport's poll errors into the reconnect loop honestly.\r\n        let spawn_broker_child = || {\r\n            let exe = std::env::current_exe().expect(\"current test exe\");\r\n            let child = std::process::Command::new(&exe)\r\n                .args([\"rc::tests::broker_host_child\", \"--exact\", \"--nocapture\"])\r\n                .env(\"SPT_RC_FIXTURE_BROKER_CHILD\", \"1\")\r\n                .spawn()\r\n                .expect(\"spawn broker-host child\");\r\n            // Barrier: the broker answers a brain connect (bounded).\r\n            let end = Instant::now() + Duration::from_secs(30);\r\n            loop {\r\n                if let Ok(b) = spt_daemon::brain::Brain::cold_start(&broker_name, 1) {\r\n                    drop(b);\r\n                    break child;\r\n                }\r\n                assert!(Instant::now() < end, \"broker child never came up\");\r\n                std::thread::sleep(Duration::from_millis(50));\r\n            }\r\n        };\r\n        // The DISPATCHER serves the attach streams (the broker alone answers no\r\n        // stream Requests — production's daemon runs this loop; without it the\r\n        // viewport sees zero records and honestly Stalls). One per broker\r\n        // generation, stop-flagged across the bounce.\r\n        let start_dispatch = |name: String| {\r\n            let registry = std::sync::Arc::new(spt_daemon::registryhost::RegistryHost::new_at(\r\n                \"fixturenode\",\r\n                spt_store::epoch::EpochSource::load_from(&spt_store::perch::epoch_file()),\r\n                spt_daemon::presence::registry_snapshot_dir(),\r\n            ));\r\n            let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));\r\n            let flag = std::sync::Arc::clone(&stop);\r\n            let paths = spt_daemon::dispatch::DispatchPaths {\r\n                owlery: spt_store::perch::owlery_dir(),\r\n                releases: spt_store::perch::spt_home().join(\"releases\"),\r\n                scratch: spt_store::perch::spt_home().join(\"dispatch-scratch\"),\r\n            };\r\n            std::thread::spawn(move || {\r\n                let _ = spt_daemon::dispatch::run_dispatch_loop(\r\n                    &name,\r\n                    &paths,\r\n                    registry,\r\n                    &flag,\r\n                    Duration::from_millis(50),\r\n                );\r\n            });\r\n            stop\r\n        };\r\n\r\n        let mut broker_child1 = spawn_broker_child();\r\n        let dispatch1_stop = start_dispatch(broker_name.clone());\r\n        let (_host1, _sid1) = spawn_ticker(&broker_name);\r\n\r\n        // Drive the REAL loop headless: viewer intent (no stdin reader, no\r\n        // status row), the capture buffer as the out sink.\r\n        let cap = Capture::default();\r\n        let mut cap_out = cap.clone();\r\n        let viewport = std::thread::spawn(move || {\r\n            let est = establish_attach(\"reheal\", AttachIntent::Viewer).map_err(|e| {\r\n                let msg = match e {\r\n                    EstablishFail::NoTarget(m) => format!(\"establish no-target: {m}\"),\r\n                    EstablishFail::DaemonDown => \"establish daemon-down\".to_string(),\r\n                    EstablishFail::Error(m) => format!(\"establish error: {m}\"),\r\n                };\r\n                msg\r\n            })?;\r\n            let mouse = MouseMode::default();\r\n            let mut remote = est.remote_node.clone();\r\n            attach_viewport(\r\n                \"reheal\",\r\n                AttachIntent::Viewer,\r\n                true,\r\n                est,\r\n                None,\r\n                &mouse,\r\n                None,\r\n                &mut remote,\r\n                &mut cap_out,\r\n            )\r\n        });\r\n\r\n        let wait_for = |viewport: &std::thread::JoinHandle<Result<PumpEnd, String>>,\r\n                        what: &str,\r\n                        probe: &dyn Fn(&[u8]) -> bool| {\r\n            let end = Instant::now() + Duration::from_secs(60);\r\n            loop {\r\n                let snap = cap.snapshot();\r\n                if probe(&snap) {\r\n                    return snap;\r\n                }\r\n                assert!(\r\n                    Instant::now() < end,\r\n                    \"never observed: {what} (viewport finished: {}; {} bytes captured: {:?})\",\r\n                    viewport.is_finished(),\r\n                    snap.len(),\r\n                    String::from_utf8_lossy(&snap[..snap.len().min(200)])\r\n                );\r\n                std::thread::sleep(Duration::from_millis(50));\r\n            }\r\n        };\r\n        let pre = wait_for(&viewport, \"pre-bounce ticker output\", &|s| {\r\n            String::from_utf8_lossy(s).contains(\"tick\")\r\n        });\r\n\r\n        // BOUNCE: the broker CHILD dies (seed stays up) — every pipe breaks at\r\n        // once, exactly like a daemon death. The viewport's poll errors →\r\n        // BrokerGone → the reconnect loop paints the banner and re-drives\r\n        // establish; the successor child + respawn land well inside the 30s\r\n        // window.\r\n        dispatch1_stop.store(true, std::sync::atomic::Ordering::Relaxed);\r\n        broker_child1.kill().expect(\"kill broker child 1\");\r\n        let _ = broker_child1.wait();\r\n        let mut _broker_child2 = spawn_broker_child();\r\n        let _dispatch2_stop = start_dispatch(broker_name.clone());\r\n        let (mut host2, _sid2) = spawn_ticker(&broker_name);\r\n\r\n        let banner_needle = b\"Reconnecting to \";\r\n        let full = wait_for(&viewport, \"the reconnect banner + post-bounce output\", &|s| {\r\n            s.windows(banner_needle.len())\r\n                .position(|w| w == banner_needle)\r\n                .is_some_and(|i| String::from_utf8_lossy(&s[i..]).contains(\"tick\"))\r\n        });\r\n        // Ordering proof: output BEFORE the banner (the live pre-bounce\r\n        // viewport), the banner, then output AFTER it (the re-established one).\r\n        let banner_at = full\r\n            .windows(banner_needle.len())\r\n            .position(|w| w == banner_needle)\r\n            .expect(\"banner present\");\r\n        assert!(\r\n            banner_at >= pre.len(),\r\n            \"the banner paints after pre-bounce output\"\r\n        );\r\n        assert!(\r\n            String::from_utf8_lossy(&full[..banner_at]).contains(\"tick\"),\r\n            \"pre-bounce ticker output rendered before the banner\"\r\n        );\r\n\r\n        // Honest finish: the ticker exits on the flag → Exit record → the\r\n        // viewport returns Exited (a FINAL end — never re-driven).\r\n        std::fs::write(&flag, b\"done\").expect(\"write exit flag\");\r\n        host2.kill_session().expect(\"kill the ticker session\");\r\n        let outcome = viewport.join().expect(\"viewport thread joins\");\r\n        // Reap the successor child (never leak a broker past the test — the\r\n        // e2e-leaked-daemon class).\r\n        let _ = _broker_child2.kill();\r\n        let _ = _broker_child2.wait();\r\n        match outcome {\r\n            Ok(PumpEnd::Exited(_)) => {}\r\n            other => panic!(\"expected the session exit to END the viewport, got {other:?}\"),\r\n        }\r\n    }\r\n\r\n    /// The reheal fixture's BROKER-HOST child — spawned by the fixture above via\r\n    /// re-exec with `SPT_RC_FIXTURE_BROKER_CHILD=1` and the ISOLATED home\r\n    /// inherited through `SPT_HOME`, then KILLED to model a real daemon death\r\n    /// (an in-process broker cannot die honestly: its per-conn handler threads\r\n    /// outlive `stop()`, so the viewport never sees an EOF and the parked\r\n    /// read_frame instances pin the pipe name against the rebind). Binds the\r\n    /// canonical broker socket + a hermetic net host (loopback scope — the\r\n    /// net_dial_loopback carrier) and serves until killed. Env-gated: inert in\r\n    /// every normal sweep.\r\n    #[test]\r\n    fn broker_host_child() {\r\n        if std::env::var(\"SPT_RC_FIXTURE_BROKER_CHILD\").as_deref() != Ok(\"1\") {\r\n            return;\r\n        }\r\n        let name = spt_daemon::endpoint::broker_socket_name();\r\n        let host = spt_daemon::nethost::NetHost::start(spt_daemon::nethost::NetConfig {\r\n            identity: spt_proto::identity::Identity::from_seed(&[9u8; 32]),\r\n            relay: spt_net::net::endpoint::RelayPolicy::Disabled,\r\n            discovery: spt_net::net::endpoint::LocalDiscovery::Off,\r\n            scope: spt_net::net::endpoint::BindScope::Loopback,\r\n            membership: None,\r\n        })\r\n        .expect(\"hermetic net host\");\r\n        let broker = spt_daemon::broker::Broker::bind_in_with_net(\r\n            &name,\r\n            spt_store::perch::spt_home().join(\"effects.log\"),\r\n            Some(host),\r\n        )\r\n        .expect(\"bind child broker\");\r\n        // Serve until killed — the parent's kill IS the bounce.\r\n        let _ = broker.serve();\r\n    }\r\n\r\n    // [unit->REQ-RC-RECONNECT] B-3 red-first: ONLY the severed-transport ends\r\n    // re-drive the viewport — BrokerGone (broker bounce) and Severed (remote conn\r\n    // drop). Session exit / operator detach / displacement / the stall backstop /\r\n    // an attach refuse are FINAL: re-driving those would re-attach to a session\r\n    // the operator (or the session itself) deliberately ended.\r\n    #[test]\r\n    fn reconnect_class_only_severed_transport_redrives() {\r\n        assert_eq!(reconnect_class(&PumpEnd::BrokerGone), EndClass::Reconnect);\r\n        assert_eq!(reconnect_class(&PumpEnd::Severed), EndClass::Reconnect);\r\n        assert_eq!(reconnect_class(&PumpEnd::Exited(Some(0))), EndClass::Final);\r\n        assert_eq!(reconnect_class(&PumpEnd::Detached), EndClass::Final);\r\n        assert_eq!(\r\n            reconnect_class(&PumpEnd::Displaced(\"node\".into())),\r\n            EndClass::Final\r\n        );\r\n        assert_eq!(reconnect_class(&PumpEnd::Stalled), EndClass::Final);\r\n        assert_eq!(reconnect_class(&PumpEnd::NoLiveSession), EndClass::Final);\r\n        assert_eq!(\r\n            reconnect_class(&PumpEnd::ReconnectGaveUp {\r\n                detail: \"x\".into(),\r\n                daemon_down: false\r\n            }),\r\n            EndClass::Final\r\n        );\r\n    }\r\n\r\n    // [unit->REQ-RC-RECONNECT] B-3 red-first vs the pre-fix mislabel: a serve-side\r\n    // EOF AFTER rendered output is a SEVERED conn (reconnect) — previously a false\r\n    // \"detached — still running\". A nothing-rendered EOF stays the honest attach\r\n    // refuse (the #4/D6 stale-row case), NOT a reconnect candidate.\r\n    #[test]\r\n    fn serve_eof_severed_vs_refused() {\r\n        assert!(matches!(serve_eof_end(true), PumpEnd::Severed));\r\n        assert!(matches!(serve_eof_end(false), PumpEnd::NoLiveSession));\r\n    }\r\n\r\n    // [unit->REQ-RC-RECONNECT] the give-up decision: bounded window, boundary\r\n    // inclusive — unit-tested without a 30s wait.\r\n    #[test]\r\n    fn reconnect_window_expiry_decision() {\r\n        assert!(!reconnect_expired(Duration::from_secs(0)));\r\n        assert!(!reconnect_expired(RECONNECT_WINDOW - Duration::from_millis(1)));\r\n        assert!(reconnect_expired(RECONNECT_WINDOW));\r\n        assert!(reconnect_expired(RECONNECT_WINDOW + Duration::from_secs(1)));\r\n    }\r\n\r\n    // [unit->REQ-RC-RECONNECT] the reconnect banner: full clear + home, the text\r\n    // CENTERED on the terminal (row rows/2, col centered on the char count), and\r\n    // an over-wide text clamps to column 1 instead of underflowing off-screen.\r\n    #[test]\r","startLine":2129,"lineNumbers":[2129,2130,2131,2132,2133,2134,2135,2136,2137,2138,2139,2140,2141,2142,2143,2144,2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2155,2156,2157,2158,2159,2160,2161,2162,2163,2164,2165,2166,2167,2168,2169,2170,2171,2172,2173,2174,2175,2176,2177,2178,2179,2180,2181,2182,2183,2184,2185,2186,2187,2188,2189,2190,2191,2192,2193,2194,2195,2196,2197,2198,2199,2200,2201,2202,2203,2204,2205,2206,2207,2208,2209,2210,2211,2212,2213,2214,2215,2216,2217,2218,2219,2220,2221,2222,2223,2224,2225,2226,2227,2228,2229,2230,2231,2232,2233,2234,2235,2236,2237,2238,2239,2240,2241,2242,2243,2244,2245,2246,2247,2248,2249,2250,2251,2252,2253,2254,2255,2256,2257,2258,2259,2260,2261,2262,2263,2264,2265,2266,2267,2268,2269,2270,2271,2272,2273,2274,2275,2276,2277,2278,2279,2280,2281,2282,2283,2284,2285,2286,2287,2288,2289,2290,2291,2292,2293,2294,2295,2296,2297,2298,2299,2300,2301,2302,2303,2304,2305,2306,2307,2308,2309,2310,2311,2312,2313,2314,2315,2316,2317,2318,2319,2320,2321,2322,2323,2324,2325,2326,2327,2328,2329,2330,2331,2332,2333,2334,2335,2336,2337,2338,2339,2340,2341,2342,2343,2344,2345,2346,2347,2348,2349,2350,2351,2352,2353,2354,2355,2356,2357,2358,2359,2360,2361,2362,2363,2364,2365,2366,2367,2368,2369,2370,2371,2372,2373,2374,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,2400,2401,2402,2403,2404,2405,2406,2407,2408,2409,2410,2411,2412,2413,2414,2415,2416,2417,2418,2419,2420,2421,2422,2423,2424,2425,2426,2427,2428,2429,2430,2431,2432,2433]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":3214,"totalBytes":14818,"outputLines":305,"outputBytes":14818,"shownRange":{"start":2129,"end":2433},"nextOffset":2434}}},"isError":false,"timestamp":1784195854299}}
{"type":"message","id":"301528e7","parentId":"c03287e0","timestamp":"2026-07-16T09:57:34.312Z","message":{"role":"toolResult","toolCallId":"call_ETDAnlMRsZ3ktImNNeVnEjd4|fc_047bb49263dfa303016a58ab0d2b408198837baa3969f76e61","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n\n## src/\n### broker.rs\n 108|// [impl->REQ-HAZARD-VIEWER-ISOLATION]\n*109|fn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {\n 110|    r.is_err()\n 111|}\n 112|\n...\n 859|    /// [`resolve_subscribe`] reads this.\n*860|    fn controller_by(&self) -> Option<&str> {\n 861|        self.controller.as_ref().and_then(|c| c.by.as_deref())\n 862|    }\n 863|\n...\n 866|    /// the log lock, then converged onto the perch OFF the lock.\n*867|    fn has_controller(&self) -> bool {\n 868|        self.controller.is_some()\n 869|    }\n 870|\n...\n 872|    /// for the convergence re-assert.\n*873|    fn live_viewer_count(&self) -> u32 {\n 874|        self.viewers.len() as u32\n 875|    }\n 876|\n...\n 890|    /// controller's identity.\n*891|    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {\n 892|        // Drop the prior controller sink first (its writer's live loop ends when\n 893|        // tx drops), and bump the generation so (a) an in-flight deadline-evict\n 894|        // for the old controller can't unseat this one and (b) a prior writer\n...\n 960|    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\n*961|    fn clear_controller(&mut self) {\n 962|        if self.controller.take().is_some() {\n 963|            self.stamp_driven_by();\n 964|        }\n...\n 991|    // [impl->REQ-CONTROLLER-LIVENESS-REAP]\n*992|    fn reap_dead_controller(&mut self) -> bool {\n 993|        let dead = self\n 994|            .controller\n 995|            .as_ref()\n...\n 1015|    /// attached during the stall is never unseated.\n*1016|    fn mark_controller_gone(&mut self, epoch: u64) {\n 1017|        if self.controller_epoch.load(Ordering::Acquire) == epoch {\n 1018|            self.clear_controller();\n 1019|        }\n...\n 1027|    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*1028|    fn controller_write_stalled(&self, now: Instant, deadline: Duration) -> bool {\n 1029|        self.controller.as_ref().is_some_and(|c| {\n 1030|            c.write_blocked_since\n 1031|                .lock()\n...\n 1046|    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n*1047|    fn local_controller_wedged(&self, now: Instant, deadline: Duration) -> bool {\n 1048|        self.controller.as_ref().is_some_and(|c| c.by.is_none())\n 1049|            && self.controller_write_stalled(now, deadline)\n 1050|    }\n...\n 1063|    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*1064|    fn stall_evict_controller(&mut self, now: Instant, stamp: StampMode) -> bool {\n 1065|        let deadline = brain_write_deadline();\n 1066|        if !self.controller_write_stalled(now, deadline) {\n 1067|            return false;\n...\n 1095|    /// live queue. The drain thread fans live frames in via `try_send`.\n*1096|    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {\n 1097|        let vid = self.next_viewer_id;\n 1098|        self.next_viewer_id += 1;\n 1099|        // Attribution (F-039 legs b+d): viewer role fact + attach record.\n...\n 1258|    /// `driven_by` / `viewer_count` when a role actually changed.\n*1259|    fn detach_if(&mut self, sub: &SharedSend) {\n 1260|        let is_ctrl = self\n 1261|            .controller\n 1262|            .as_ref()\n...\n 1325|    // [impl->REQ-RCVIEW-1]\n*1326|    fn stamp_viewer_count(&self) {\n 1327|        if self.endpoint.is_empty() {\n 1328|            return;\n 1329|        }\n...\n 1345|    // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n*1346|    fn stamp_reaped(&self) {\n 1347|        if self.endpoint.is_empty() {\n 1348|            return;\n 1349|        }\n...\n 1356|    /// Whether `sub` is the current controller (the resize-exclusivity check).\n*1357|    fn is_controller(&self, sub: &SharedSend) -> bool {\n 1358|        self.controller\n 1359|            .as_ref()\n 1360|            .is_some_and(|c| Arc::ptr_eq(&c.send, sub))\n...\n 1375|/// evicts the row. The drain thread itself never blocks on this socket.\n*1376|fn viewer_writer(\n 1377|    send: SharedSend,\n 1378|    session_id: u64,\n 1379|    initial: Vec<Envelope>,\n...\n 1442|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*1443|fn note_controller_write_retired(sid: u64, conn: u64, e: &io::Error) {\n 1444|    if e.kind() != io::ErrorKind::TimedOut {\n 1445|        return;\n 1446|    }\n...\n 1508|#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\n*1509|fn controller_writer(\n 1510|    sid: u64,\n 1511|    send: SharedSend,\n 1512|    initial: Vec<(u64, Vec<u8>)>,\n...\n 1799|/// when the last `tx` clone drops at session teardown.\n*1800|fn input_writer(session: Arc<PtySession>, rx: Receiver<InputRecord>) {\n 1801|    while let Ok(bytes) = rx.recv() {\n 1802|        let _ = session.write_input(&bytes);\n 1803|    }\n\n## tests/\n### broker.rs\n 1001|// resume-mode path: drop `seq < cursor`, accept `seq >= cursor`, cursor=seq+1).\n*1002|fn controller_writer_reorder_test() {\n 1003|    let name = unique_name();\n 1004|    let broker = Broker::bind(&name).expect(\"bind broker\");\n 1005|    let server_broker = Arc::clone(&broker);\n...\n 1215|#[test]\n*1216|fn controller_writer_reorder_consumer_view_stays_monotonic_and_session_live() {\n 1217|    controller_writer_reorder_test();\n 1218|}\n### attach.rs\n 144|#[allow(clippy::too_many_arguments)]\n*145|fn attach_drive_detach(\n 146|    operator: &mut Brain,\n 147|    target_broker: &str,\n 148|    mut target: Brain,\n...\n 297|#[test]\n*298|fn spt_hosted_bringup_then_cross_node_attach_drives_the_pty() {\n 299|    init_home();\n 300|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 301|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 375|#[test]\n*376|fn remote_attach_drives_a_real_pty_cross_daemon() {\n 377|    init_home();\n 378|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 379|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 429|#[test]\n*430|fn local_attach_via_loopback_conn_rides_the_same_pump() {\n 431|    init_home();\n 432|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 433|    let name = unique_name();\n...\n 480|#[test]\n*481|fn loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock() {\n 482|    init_home();\n 483|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 484|    let name = unique_name();\n...\n 592|#[test]\n*593|fn cross_node_cold_attach_to_alt_screen_gets_clean_repaint() {\n 594|    init_home();\n 595|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 596|    let name = unique_name();\n...\n 778|#[test]\n*779|fn attach_survives_target_brain_restart_exactly_once() {\n 780|    init_home();\n 781|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 782|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 930|#[test]\n*931|fn attach_registers_remote_drive_detection() {\n 932|    init_home();\n 933|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 934|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 1075|#[test]\n*1076|fn controller_viewer_matrix_and_loud_take() {\n 1077|    let (_broker, name, _spawner, sid) = labeled_session(\"cv-matrix\");\n 1078|\n 1079|    // Operator A drives an (undriven) endpoint → controller.\n...\n 1160|#[test]\n*1161|fn resize_is_controller_exclusive() {\n 1162|    let (_broker, name, _spawner, sid) = labeled_session(\"cv-resize\");\n 1163|\n 1164|    // PUMP conns: this test does bounded reads expecting a TIMEOUT (the\n...\n 1230|#[test]\n*1231|fn wedged_viewer_does_not_stall_controller() {\n 1232|    let (_broker, name, _spawner, sid) = labeled_session(\"cv-isolation\");\n 1233|\n 1234|    let mut ctrl = connect_retry(&name);\n...\n 1289|/// deadline (the viewer rides a pump conn so the per-slice timeout is honored).\n*1290|fn viewer_reads_marker_no_displace(brain: &mut Brain, needle: &[u8]) -> bool {\n 1291|    let mut got = Vec::new();\n 1292|    let deadline = std::time::Instant::now() + Duration::from_secs(15);\n 1293|    while std::time::Instant::now() < deadline {\n...\n 1315|#[test]\n*1316|fn controller_restart_with_viewer_no_displace_and_viewer_survives() {\n 1317|    let (_broker, name, _spawner, sid) = labeled_session(\"cv-gate7-combined\");\n 1318|\n 1319|    // Controller on a pump conn (so a bounded read is possible).\n...\n 1477|#[test]\n*1478|fn re_serve_resets_resume_cursor_after_a_pre_attached_consume() {\n 1479|    init_home();\n 1480|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 1481|    let name = unique_name();\n### control_stamp_lifetime.rs\n 134|#[test]\n*135|fn reap_clears_control_and_viewer_stamps() {\n 136|    init_stamp_home();\n 137|    let node = \"PEERNODEHEX_CTLSTAMP\";\n 138|    let id = format!(\"ctlstamp-{}\", std::process::id());\n...\n 345|#[test]\n*346|fn control_reap_clears_sticky_stamps_no_session_online_and_boot_sweep() {\n 347|    init_stamp_home();\n 348|    let node = \"PEERNODEHEX_B3\";\n 349|    let pid = std::process::id();"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs, crates/spt-daemon/tests/broker.rs, crates/spt-daemon/tests/attach.rs, crates/spt-daemon/tests/control_stamp_lifetime.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":38,"fileCount":4,"files":["crates/spt-daemon/src/broker.rs","crates/spt-daemon/tests/broker.rs","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/control_stamp_lifetime.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/tests/broker.rs","count":2},{"path":"crates/spt-daemon/tests/attach.rs","count":14},{"path":"crates/spt-daemon/tests/control_stamp_lifetime.rs","count":2}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/\n\n## src/\n### broker.rs\n  108│// [impl->REQ-HAZARD-VIEWER-ISOLATION]\n *109│fn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {\n  110│    r.is_err()\n  111│}\n  112│\n     │...\n  859│    /// [`resolve_subscribe`] reads this.\n *860│    fn controller_by(&self) -> Option<&str> {\n  861│        self.controller.as_ref().and_then(|c| c.by.as_deref())\n  862│    }\n  863│\n     │...\n  866│    /// the log lock, then converged onto the perch OFF the lock.\n *867│    fn has_controller(&self) -> bool {\n  868│        self.controller.is_some()\n  869│    }\n  870│\n     │...\n  872│    /// for the convergence re-assert.\n *873│    fn live_viewer_count(&self) -> u32 {\n  874│        self.viewers.len() as u32\n  875│    }\n  876│\n     │...\n  890│    /// controller's identity.\n *891│    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {\n  892│        // Drop the prior controller sink first (its writer's live loop ends when\n  893│        // tx drops), and bump the generation so (a) an in-flight deadline-evict\n  894│        // for the old controller can't unseat this one and (b) a prior writer\n     │...\n  960│    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\n *961│    fn clear_controller(&mut self) {\n  962│        if self.controller.take().is_some() {\n  963│            self.stamp_driven_by();\n  964│        }\n     │...\n  991│    // [impl->REQ-CONTROLLER-LIVENESS-REAP]\n *992│    fn reap_dead_controller(&mut self) -> bool {\n  993│        let dead = self\n  994│            .controller\n  995│            .as_ref()\n     │...\n 1015│    /// attached during the stall is never unseated.\n*1016│    fn mark_controller_gone(&mut self, epoch: u64) {\n 1017│        if self.controller_epoch.load(Ordering::Acquire) == epoch {\n 1018│            self.clear_controller();\n 1019│        }\n     │...\n 1027│    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*1028│    fn controller_write_stalled(&self, now: Instant, deadline: Duration) -> bool {\n 1029│        self.controller.as_ref().is_some_and(|c| {\n 1030│            c.write_blocked_since\n 1031│                .lock()\n     │...\n 1046│    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n*1047│    fn local_controller_wedged(&self, now: Instant, deadline: Duration) -> bool {\n 1048│        self.controller.as_ref().is_some_and(|c| c.by.is_none())\n 1049│            && self.controller_write_stalled(now, deadline)\n 1050│    }\n     │...\n 1063│    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*1064│    fn stall_evict_controller(&mut self, now: Instant, stamp: StampMode) -> bool {\n 1065│        let deadline = brain_write_deadline();\n 1066│        if !self.controller_write_stalled(now, deadline) {\n 1067│            return false;\n     │...\n 1095│    /// live queue. The drain thread fans live frames in via `try_send`.\n*1096│    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {\n 1097│        let vid = self.next_viewer_id;\n 1098│        self.next_viewer_id += 1;\n 1099│        // Attribution (F-039 legs b+d): viewer role fact + attach record.\n     │...\n 1258│    /// `driven_by` / `viewer_count` when a role actually changed.\n*1259│    fn detach_if(&mut self, sub: &SharedSend) {\n 1260│        let is_ctrl = self\n 1261│            .controller\n 1262│            .as_ref()\n     │...\n 1325│    // [impl->REQ-RCVIEW-1]\n*1326│    fn stamp_viewer_count(&self) {\n 1327│        if self.endpoint.is_empty() {\n 1328│            return;\n 1329│        }\n     │...\n 1345│    // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n*1346│    fn stamp_reaped(&self) {\n 1347│        if self.endpoint.is_empty() {\n 1348│            return;\n 1349│        }\n     │...\n 1356│    /// Whether `sub` is the current controller (the resize-exclusivity check).\n*1357│    fn is_controller(&self, sub: &SharedSend) -> bool {\n 1358│        self.controller\n 1359│            .as_ref()\n 1360│            .is_some_and(|c| Arc::ptr_eq(&c.send, sub))\n     │...\n 1375│/// evicts the row. The drain thread itself never blocks on this socket.\n*1376│fn viewer_writer(\n 1377│    send: SharedSend,\n 1378│    session_id: u64,\n 1379│    initial: Vec<Envelope>,\n     │...\n 1442│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*1443│fn note_controller_write_retired(sid: u64, conn: u64, e: &io::Error) {\n 1444│    if e.kind() != io::ErrorKind::TimedOut {\n 1445│        return;\n 1446│    }\n     │...\n 1508│#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\n*1509│fn controller_writer(\n 1510│    sid: u64,\n 1511│    send: SharedSend,\n 1512│    initial: Vec<(u64, Vec<u8>)>,\n     │...\n 1799│/// when the last `tx` clone drops at session teardown.\n*1800│fn input_writer(session: Arc<PtySession>, rx: Receiver<InputRecord>) {\n 1801│    while let Ok(bytes) = rx.recv() {\n 1802│        let _ = session.write_input(&bytes);\n 1803│    }\n\n## tests/\n### broker.rs\n 1001│// resume-mode path: drop `seq < cursor`, accept `seq >= cursor`, cursor=seq+1).\n*1002│fn controller_writer_reorder_test() {\n 1003│    let name = unique_name();\n 1004│    let broker = Broker::bind(&name).expect(\"bind broker\");\n 1005│    let server_broker = Arc::clone(&broker);\n     │...\n 1215│#[test]\n*1216│fn controller_writer_reorder_consumer_view_stays_monotonic_and_session_live() {\n 1217│    controller_writer_reorder_test();\n 1218│}\n### attach.rs\n  144│#[allow(clippy::too_many_arguments)]\n *145│fn attach_drive_detach(\n  146│    operator: &mut Brain,\n  147│    target_broker: &str,\n  148│    mut target: Brain,\n     │...\n  297│#[test]\n *298│fn spt_hosted_bringup_then_cross_node_attach_drives_the_pty() {\n  299│    init_home();\n  300│    let dir = tempfile::tempdir().expect(\"tempdir\");\n  301│    let (name_a, name_b) = (unique_name(), unique_name());\n     │...\n  375│#[test]\n *376│fn remote_attach_drives_a_real_pty_cross_daemon() {\n  377│    init_home();\n  378│    let dir = tempfile::tempdir().expect(\"tempdir\");\n  379│    let (name_a, name_b) = (unique_name(), unique_name());\n     │...\n  429│#[test]\n *430│fn local_attach_via_loopback_conn_rides_the_same_pump() {\n  431│    init_home();\n  432│    let dir = tempfile::tempdir().expect(\"tempdir\");\n  433│    let name = unique_name();\n     │...\n  480│#[test]\n *481│fn loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock() {\n  482│    init_home();\n  483│    let dir = tempfile::tempdir().expect(\"tempdir\");\n  484│    let name = unique_name();\n     │...\n  592│#[test]\n *593│fn cross_node_cold_attach_to_alt_screen_gets_clean_repaint() {\n  594│    init_home();\n  595│    let dir = tempfile::tempdir().expect(\"tempdir\");\n  596│    let name = unique_name();\n     │...\n  778│#[test]\n *779│fn attach_survives_target_brain_restart_exactly_once() {\n  780│    init_home();\n  781│    let dir = tempfile::tempdir().expect(\"tempdir\");\n  782│    let (name_a, name_b) = (unique_name(), unique_name());\n     │...\n  930│#[test]\n *931│fn attach_registers_remote_drive_detection() {\n  932│    init_home();\n  933│    let dir = tempfile::tempdir().expect(\"tempdir\");\n  934│    let (name_a, name_b) = (unique_name(), unique_name());\n     │...\n 1075│#[test]\n*1076│fn controller_viewer_matrix_and_loud_take() {\n 1077│    let (_broker, name, _spawner, sid) = labeled_session(\"cv-matrix\");\n 1078│\n 1079│    // Operator A drives an (undriven) endpoint → controller.\n     │...\n 1160│#[test]\n*1161│fn resize_is_controller_exclusive() {\n 1162│    let (_broker, name, _spawner, sid) = labeled_session(\"cv-resize\");\n 1163│\n 1164│    // PUMP conns: this test does bounded reads expecting a TIMEOUT (the\n     │...\n 1230│#[test]\n*1231│fn wedged_viewer_does_not_stall_controller() {\n 1232│    let (_broker, name, _spawner, sid) = labeled_session(\"cv-isolation\");\n 1233│\n 1234│    let mut ctrl = connect_retry(&name);\n     │...\n 1289│/// deadline (the viewer rides a pump conn so the per-slice timeout is honored).\n*1290│fn viewer_reads_marker_no_displace(brain: &mut Brain, needle: &[u8]) -> bool {\n 1291│    let mut got = Vec::new();\n 1292│    let deadline = std::time::Instant::now() + Duration::from_secs(15);\n 1293│    while std::time::Instant::now() < deadline {\n     │...\n 1315│#[test]\n*1316│fn controller_restart_with_viewer_no_displace_and_viewer_survives() {\n 1317│    let (_broker, name, _spawner, sid) = labeled_session(\"cv-gate7-combined\");\n 1318│\n 1319│    // Controller on a pump conn (so a bounded read is possible).\n     │...\n 1477│#[test]\n*1478│fn re_serve_resets_resume_cursor_after_a_pre_attached_consume() {\n 1479│    init_home();\n 1480│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 1481│    let name = unique_name();\n### control_stamp_lifetime.rs\n 134│#[test]\n*135│fn reap_clears_control_and_viewer_stamps() {\n 136│    init_stamp_home();\n 137│    let node = \"PEERNODEHEX_CTLSTAMP\";\n 138│    let id = format!(\"ctlstamp-{}\", std::process::id());\n    │...\n 345│#[test]\n*346│fn control_reap_clears_sticky_stamps_no_session_online_and_boot_sweep() {\n 347│    init_stamp_home();\n 348│    let node = \"PEERNODEHEX_B3\";\n 349│    let pid = std::process::id();"},"isError":false,"timestamp":1784195854312}}
{"type":"message","id":"125b677a","parentId":"301528e7","timestamp":"2026-07-16T09:57:40.052Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning detailed controller viewer tests**"},{"type":"toolCall","id":"call_S5qs7xd8EzLiZIZYRiutaAaE|fc_047bb49263dfa303016a58ab13d47481989c24897a2cdfaa0c","name":"read","arguments":{"i":"Reading controller viewer integration","path":"crates/spt-daemon/tests/attach.rs","selector":"1040-1455"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":29893,"output":65,"cacheRead":49664,"cacheWrite":0,"totalTokens":79622,"reasoningTokens":22,"cost":{"input":0.14946500000000001,"output":0.0019500000000000001,"cacheRead":0.024832,"cacheWrite":0,"total":0.17624700000000001}},"stopReason":"toolUse","timestamp":1784195854368,"responseId":"resp_047bb49263dfa303016a58ab0eba3c81988e19a1ba6d4552ef","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58ab1125c081989a4ceb8f8f270aef","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKsTc4fqgF7tWnC6LaFD36BFFj6yz54ou99E2C8PDpAVwXEFcuJ8z-uUYQZvofInyrxYy0kEitEsrnzCOQ5tkUwnzuIzk-IZxkovxXb73rjPxtLJESJBKEMmw5f9nQfA9t2khFBjeqnZbl9zPwof45BzslI6PdmzXBP_azGEvDsyr__vKfxt62hlwU-LH1dhXtj6_z7kGNqZhwQEZ23-4nG9R6rbbQ-5WTghpAg2h-I4bvKxxI4L00GrqjYE9SgtmVJJghWLaREPEJ7boOLMrkh_PVfCKK02fM_lCxYJJyBQgrxpFGe7oskseQ1WvakP2cdFpIfAuyPKfLNYughWCnMtnas_kmdJ91WHgQWRZSKML9-KuPw_e6c7g0CnE3jvAXkdHwxdhWgSqcf4rGBYLAKi1G8OQ5wBJfu-wX6glUE2baS-xmZeGalRuM-w00Zo4r_mQfQAeUmS9tDBbzndb9vhnI1FPkG1bNKF_l_cO_J_KAIqdukQztTqZ4-bs_9Qat2vXgLHhd6ndAP1_kkBZvilDYUY5_GlH_G4TYmPZNaGCEQUn0hpRtCqXMcLKYvKE_1UldEKrkt__CBCylC2ZdA3CN2lvveBsKnI7P03DlOLUqEk8JxvzS7H8dmEXP12j_f1COVKxn246yQA9Mk5M4lBJjmEyWJReAicBzPTF9Ea-g2uYadaHhqlIyFwpZOOFbazbnaYWIxc5B1V4b5iU3R_3sim_EwqFGAj8XY9lqZCGU_386N-fUfPp7ezw93izfWW-_DNCSMYCotNOW5yQCLPQeEtV_2dobAollo55bm1Ar3aFA3xHgxHmTSj1_Z2dYeoOr6UjzSY9cpu5CYzwWs12WM8L7FLkQ2BSGHED9oj3uGXQh-PNnH3EVjFrTJv9sUhBnLD93GVITMjL3MuiQEXG27-iE2AuEuP9wjuj6wM1KmiSq6FXNB97QTxcAqjMevXGc5EFh8uOKSCpk964OW9h0hTUEZX8H9UhDwXF5SktG0_P8u0lOOn8At6PIrZ6PwsTDvcfXxD7tkjjOoO-nOeyQ==","summary":[{"type":"summary_text","text":"**Planning detailed controller viewer tests**"}]},{"id":"fc_047bb49263dfa303016a58ab13d47481989c24897a2cdfaa0c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading controller viewer integration\",\"path\":\"crates/spt-daemon/tests/attach.rs\",\"selector\":\"1040-1455\"}","call_id":"call_S5qs7xd8EzLiZIZYRiutaAaE","name":"read"}]},"duration":5683.369200000539,"ttft":2759.9527000002563,"contextSnapshot":{"promptTokens":79557,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_S5qs7xd8EzLiZIZYRiutaAaE|fc_047bb49263dfa303016a58ab13d47481989c24897a2cdfaa0c","toolName":"read","startedAt":"2026-07-16T09:57:40.052Z","args":{"path":"crates/spt-daemon/tests/attach.rs"},"intent":"Reading controller viewer integration"},"id":"c85b2b41","parentId":"125b677a","timestamp":"2026-07-16T09:57:40.052Z"}
{"type":"message","id":"c8f126d4","parentId":"c85b2b41","timestamp":"2026-07-16T09:57:40.067Z","message":{"role":"toolResult","toolCallId":"call_S5qs7xd8EzLiZIZYRiutaAaE|fc_047bb49263dfa303016a58ab13d47481989c24897a2cdfaa0c","toolName":"read","content":[{"type":"text","text":"use spt_daemon::msg::SubscribeOutcome;\n\n/// Spawn a LABELED echo session on a fresh broker; return (broker, name, brain,\n/// sid). The spawner is the broker's by=None local controller placeholder.\nfn labeled_session(label: &str) -> (Arc<Broker>, String, Brain, u64) {\n    init_home();\n    let dir = HOME.get().expect(\"home\").path().to_path_buf();\n    let name = unique_name();\n    let broker = net_broker(&name, &dir.join(label));\n    let mut spawner = connect_retry(&name);\n    let sid = spawner\n        .spawn_session(SpawnReq {\n            endpoint: label.to_string(),\n            ..echo_spawn_req()\n        })\n        .expect(\"spawn labeled echo\");\n    (broker, name, spawner, sid)\n}\n\n/// Read events until the broker's `Subscribed` outcome arrives (skipping any\n/// replayed output / size frames), returning the resolved role.\nfn read_outcome(brain: &mut Brain) -> SubscribeOutcome {\n    for _ in 0..2000 {\n        match brain.read_event().expect(\"event\") {\n            BrokerEvent::Subscribed { outcome, .. } => return outcome,\n            _ => continue,\n        }\n    }\n    panic!(\"no Subscribed outcome\");\n}\n\n// [int->REQ-RCVIEW-1] [int->REQ-KICK-1] The controller/viewer/take matrix and the\n// loud kick over the broker fan-out: a FREE endpoint's first Control becomes\n// controller; a DIFFERENT operator's Control is BUSY-REFUSED (never a silent\n// displace, gate #4); a Viewer coexists; an explicit Take displaces the\n// incumbent with a LOUD `Displaced{by}` and becomes controller.\n#[test]\nfn controller_viewer_matrix_and_loud_take() {\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-matrix\");\n\n    // Operator A drives an (undriven) endpoint → controller.\n    let mut a = connect_retry(&name);\n    a.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"A control\");\n    assert_eq!(read_outcome(&mut a), SubscribeOutcome::Controller, \"free → controller\");\n\n    // Operator B's plain Control hits a CONTROLLED endpoint → busy-refuse, NOT a\n    // silent displace (gate #4). A is untouched.\n    let mut b = connect_retry(&name);\n    b.attach_as(sid, 0, AttachIntent::Control, Some(\"node-B\")).expect(\"B control\");\n    assert_eq!(\n        read_outcome(&mut b),\n        SubscribeOutcome::BusyControlled { by: \"node-A\".to_string() },\n        \"controlled → busy-refuse with the controller's identity\"\n    );\n\n    // A viewer coexists (read-only; never displaces).\n    let mut v = connect_retry(&name);\n    v.attach_as(sid, 0, AttachIntent::Viewer, Some(\"node-V\")).expect(\"V view\");\n    assert_eq!(read_outcome(&mut v), SubscribeOutcome::Viewer, \"viewer attaches\");\n\n    // B's explicit Take displaces A: A gets a LOUD Displaced{by:node-B}, B becomes\n    // controller. This is the ONLY displace path.\n    b.attach_as(sid, 0, AttachIntent::Take, Some(\"node-B\")).expect(\"B take\");\n    assert_eq!(read_outcome(&mut b), SubscribeOutcome::TookControl, \"take → took control\");\n    let displaced = (0..2000).find_map(|_| match a.read_event().expect(\"A event\") {\n        BrokerEvent::Displaced { by, .. } => Some(by),\n        _ => None,\n    });\n    assert_eq!(displaced.as_deref(), Some(\"node-B\"), \"A receives the loud kick notice\");\n}\n\n// [int->REQ-RCVIEW-1] GATE #7 (the self-kick trap): a SAME-origin re-subscribe —\n// a successor re-taking the controller slot after a brain restart — silently\n// re-takes (Controller), and NEVER fires Displaced to the prior holder. Only a\n// DIFFERENT origin's Take is loud. Here the same `by` re-subscribes and we assert\n// no Displaced reaches the first connection.\n#[test]\nfn same_origin_re_subscribe_does_not_displace() {\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-gate7\");\n\n    // `first` rides the PUMP carrier so `read_event_until`'s deadline is honored\n    // (a Whole conn blocks forever — the deadline is a pump-only feature). We need\n    // a BOUNDED read to assert the absence of a Displaced frame.\n    let mut first =\n        Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"first pump conn\");\n    first.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"first control\");\n    assert_eq!(read_outcome(&mut first), SubscribeOutcome::Controller);\n\n    // The \"successor\" re-serves the SAME operator's stream (same by) on a new\n    // connection — the brain-restart re-take.\n    let mut successor = connect_retry(&name);\n    successor.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"successor control\");\n    assert_eq!(\n        read_outcome(&mut successor),\n        SubscribeOutcome::Controller,\n        \"same-origin re-subscribe silently re-takes the controller\"\n    );\n\n    // The first connection must NOT receive a Displaced — re-taking your own slot\n    // is never a kick. Drive a marker through the successor and confirm `first`\n    // saw no Displaced in the window.\n    successor.send_effect(MintedOp::new(Minter::Rc, 7), b\"GATE7\\r\").expect(\"drive\");\n    let mut saw_displaced = false;\n    for _ in 0..50 {\n        // Non-fatal drain: the first conn may see nothing at all (it was silently\n        // superseded); we only care that it is NEVER a Displaced.\n        match first.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20))) {\n            Ok(BrokerEvent::Displaced { .. }) => {\n                saw_displaced = true;\n                break;\n            }\n            Ok(_) => continue,\n            Err(_) => break, // timeout — nothing pending\n        }\n    }\n    assert!(!saw_displaced, \"a same-origin re-take must never kick the prior holder (gate #7)\");\n}\n\n// [int->REQ-RCVIEW-1] Resize is CONTROLLER-EXCLUSIVE: a resize arriving on a\n// viewer's connection is refused by the broker (gate #3); the controller's\n// resize is honored.\n#[test]\nfn resize_is_controller_exclusive() {\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-resize\");\n\n    // PUMP conns: this test does bounded reads expecting a TIMEOUT (the\n    // controller's resize is honored ⇒ NO Error frame arrives), and the\n    // `read_event_until` deadline is only honored on the pump carrier — a Whole\n    // conn blocks forever when nothing is incoming (hung the Linux runner: `cat`\n    // emits nothing on resize, unlike Windows `findstr`).\n    let mut ctrl = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"ctrl conn\");\n    ctrl.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"control\");\n    assert_eq!(read_outcome(&mut ctrl), SubscribeOutcome::Controller);\n\n    let mut view = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"view conn\");\n    view.attach_as(sid, 0, AttachIntent::Viewer, Some(\"node-V\")).expect(\"view\");\n    assert_eq!(read_outcome(&mut view), SubscribeOutcome::Viewer);\n\n    // The viewer's resize is rejected (it is not the controller).\n    view.resize(50, 100).expect(\"send resize\");\n    let refused = (0..200).find_map(|_| {\n        match view.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20))) {\n            Ok(BrokerEvent::Error { message }) => Some(message),\n            _ => None,\n        }\n    });\n    assert!(\n        refused.as_deref().is_some_and(|m| m.contains(\"controller-exclusive\")),\n        \"a viewer's resize is refused as controller-exclusive, got {refused:?}\"\n    );\n\n    // The controller's resize is honored (no error).\n    ctrl.resize(50, 100).expect(\"controller resize\");\n    let ctrl_err = ctrl.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(50)));\n    assert!(\n        !matches!(ctrl_err, Ok(BrokerEvent::Error { .. })),\n        \"the controller's resize is honored\"\n    );\n}\n\n// [int->REQ-HAZARD-VIEWER-ISOLATION] A wedged viewer (one that stops draining its\n// socket) must NEVER stall the controller or the child: the drain `try_send`s and\n// evicts the viewer, while the controller keeps receiving output. Drive a burst\n// large enough to overflow the viewer's bounded queue + socket buffer, then a\n// trailing marker — the controller (actively read) renders the marker regardless.\n//\n// [int->REQ-HAZARD-CONTROLLER-GAP-RESUME] The controller is driven through\n// `read_controller_event_resuming`, NOT raw `read_event`, because the b4\n// drop-don't-block fix decoupled controller backpressure from the drain: the\n// controller is now a NON-BLOCKING try_send that DROPS on a full channel\n// (CONTROLLER_CHANNEL_DEPTH=4096), no longer a blocking writer. Under the 200KB\n// echo flood the controller falls behind its OWN output and the broker drops\n// controller frames (advancing nothing — `delivered_through` freezes), which\n// surfaces to the strict controller cursor as an `output gap`. The helper\n// RESUMES-FROM-FLOOR on that gap (re-fetch from `controller_resume_floor`, B2 — a\n// re-fetch, not a viewer-style snap).\n//\n// VIEWER-ISOLATION asserts the wedged viewer does not STALL the controller — and a\n// PROMPT CLEAN outcome proves that whichever way it lands (doyle ruling, the burst\n// is deliberately oversized so we assert non-stall robustly, not a fragile\n// ring-fit burst size). Three regimes (CONTROLLER_CHANNEL_DEPTH=4096, ring=4096):\n// (1) <4096 frames fit → no drop → renders ISOLATED; (2) 4096..8192 → drop → gap →\n// re-fetch, ring still retains → RECOVERABLE → renders ISOLATED; (3) >8192 (this\n// 200KB burst) → the ring rolls PAST the frozen delivered_through → the dropped\n// frames are GONE → exactly-once is genuinely impossible → a clean\n// `ControllerIrrecoverablyBehind` (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND). The\n// RECOVERABLE re-fetch (regime 2) is proven deterministically by the in-process\n// unit `controller_resume_from_floor_refetches_after_a_gap`; this int proves only\n// the non-stall property: the controller gets ISOLATED (Windows ConPTY floods\n// slower → regime 1/2) OR a clean irrecoverable marker (Linux forkpty → regime 3)\n// — NEVER a hang and NEVER the viewer's wedge propagating.\n#[test]\nfn wedged_viewer_does_not_stall_controller() {\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-isolation\");\n\n    let mut ctrl = connect_retry(&name);\n    ctrl.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"control\");\n    assert_eq!(read_outcome(&mut ctrl), SubscribeOutcome::Controller);\n\n    // A viewer attaches but then NEVER reads — its writer thread blocks on the\n    // socket, its bounded queue fills, and the drain evicts it.\n    let mut view = connect_retry(&name);\n    view.attach_as(sid, 0, AttachIntent::Viewer, Some(\"node-V\")).expect(\"view\");\n    assert_eq!(read_outcome(&mut view), SubscribeOutcome::Viewer);\n    // (deliberately drop the read side: `view` is never polled again)\n\n    // A big burst echoed by the child overflows the wedged viewer; a trailing\n    // marker proves the controller's stream kept flowing.\n    let big = vec![b'x'; 200_000];\n    ctrl.send_effect(MintedOp::new(Minter::Rc, 1), &big).expect(\"burst\");\n    ctrl.send_effect(MintedOp::new(Minter::Rc, 2), b\"\\rISOLATED\\r\").expect(\"marker\");\n\n    let mut got = Vec::new();\n    let mut irrecoverable = false;\n    let deadline = std::time::Instant::now() + Duration::from_secs(20);\n    while std::time::Instant::now() < deadline {\n        match read_controller_event_resuming(&mut ctrl, sid, Some(\"node-A\")) {\n            Ok(BrokerEvent::Output { bytes, .. }) => {\n                got.extend_from_slice(&bytes);\n                if count(&got, b\"ISOLATED\") >= 1 {\n                    break;\n                }\n            }\n            Ok(_) => continue,\n            // The controller fell PAST the ring on its OWN (the oversized 200KB burst\n            // exceeds CONTROLLER_CHANNEL_DEPTH + ring) — a PROMPT, CLEAN, MARKED\n            // outcome, NOT a hang and NOT the wedged viewer's stall propagating. This\n            // proves VIEWER-ISOLATION exactly as much as rendering ISOLATED does, so\n            // accept it (do NOT .expect()-panic on the irrecoverable marker).\n            Err(e) if e.get_ref().is_some_and(|i| i.is::<ControllerIrrecoverablyBehind>()) => {\n                irrecoverable = true;\n                break;\n            }\n            Err(e) => panic!(\"controller errored unexpectedly (not the irrecoverable marker): {e}\"),\n        }\n    }\n    assert!(\n        count(&got, b\"ISOLATED\") >= 1 || irrecoverable,\n        \"the wedged viewer must NOT stall the controller: the controller gets a prompt \\\n         clean outcome — either renders the ISOLATED marker (re-fetch succeeded) or \\\n         surfaces a clean ControllerIrrecoverablyBehind (it fell past the ring on its \\\n         own). Got NEITHER within the deadline = a hang / stall.\"\n    );\n    // Hold the viewer connection open until here so the eviction was the bounded-\n    // queue overflow path, not a connection drop.\n    drop(view);\n}\n\n/// Read a viewer's events until `needle` appears in its rendered output, PANICKING\n/// if a `Displaced` ever arrives (a viewer must never be displaced) — bounded by a\n/// deadline (the viewer rides a pump conn so the per-slice timeout is honored).\nfn viewer_reads_marker_no_displace(brain: &mut Brain, needle: &[u8]) -> bool {\n    let mut got = Vec::new();\n    let deadline = std::time::Instant::now() + Duration::from_secs(15);\n    while std::time::Instant::now() < deadline {\n        match brain.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200))) {\n            Ok(BrokerEvent::Output { bytes, .. }) => {\n                got.extend_from_slice(&bytes);\n                if count(&got, needle) >= 1 {\n                    return true;\n                }\n            }\n            Ok(BrokerEvent::Displaced { .. }) => panic!(\"a viewer must NEVER receive Displaced\"),\n            Ok(_) => continue,    // Size / other — keep reading\n            Err(_) => continue,   // slice timeout — keep waiting until the deadline\n        }\n    }\n    false\n}\n\n// [int->REQ-RCVIEW-1] GATE #7 COMBINED (doyle's named close-out): a controller +\n// a viewer are both attached when the controller's serving brain RESTARTS (its\n// connection drops; a successor re-serves with the SAME operator identity). The\n// successor silently re-takes the controller slot — NO `Displaced` to anyone — and\n// the VIEWER survives the churn and keeps receiving output. This is the self-kick\n// guard ∩ viewer survival, the one scenario the separate tests left uncombined.\n#[test]\nfn controller_restart_with_viewer_no_displace_and_viewer_survives() {\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-gate7-combined\");\n\n    // Controller on a pump conn (so a bounded read is possible).\n    let mut ctrl = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"ctrl conn\");\n    ctrl.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"ctrl control\");\n    assert_eq!(read_outcome(&mut ctrl), SubscribeOutcome::Controller);\n\n    // Viewer attached BEFORE the crash (pump conn).\n    let mut view = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"view conn\");\n    view.attach_as(sid, 0, AttachIntent::Viewer, Some(\"node-V\")).expect(\"view\");\n    assert_eq!(read_outcome(&mut view), SubscribeOutcome::Viewer);\n\n    // The viewer renders pre-crash output (it is genuinely live).\n    ctrl.send_effect(MintedOp::new(Minter::Rc, 1), b\"BEFORE\\r\").expect(\"before marker\");\n    assert!(\n        viewer_reads_marker_no_displace(&mut view, b\"BEFORE\"),\n        \"viewer renders pre-restart output\"\n    );\n\n    // CRASH the controller's serving brain — drop its connection. The broker keeps\n    // the session, the OutputLog, and the (separate) viewer connection.\n    drop(ctrl);\n\n    // SUCCESSOR re-serves with the SAME identity (node-A) — the brain-restart\n    // re-take. It silently becomes controller (NEVER displacing anyone): the\n    // gate-#7 self-kick guard on a real controller-conn restart.\n    let mut succ = Brain::cold_start_pump(&name, 2, Duration::from_secs(5)).expect(\"succ conn\");\n    succ.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"succ control\");\n    assert_eq!(\n        read_outcome(&mut succ),\n        SubscribeOutcome::Controller,\n        \"successor silently re-takes the controller slot — no self-kick (gate #7)\"\n    );\n\n    // The viewer SURVIVED the controller restart: output driven by the successor\n    // reaches it, and it NEVER saw a Displaced.\n    succ.send_effect(MintedOp::new(Minter::Rc, 2), b\"AFTER\\r\").expect(\"after marker\");\n    assert!(\n        viewer_reads_marker_no_displace(&mut view, b\"AFTER\"),\n        \"viewer survives the controller restart and keeps receiving (no Displaced)\"\n    );\n}\n\n// [int->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n// v0.13.0 P1c — the handoff side of the fix (the defense-in-depth seed). The\n// controller-writer reorder is fixed in the BROKER (the epoch gate guarantees one\n// live `controller_writer` per connection), NOT by dropping handoff's eager\n// subscribe — that subscribe IS the resume mechanism every standalone handoff\n// caller (the brain-only update engine, `idempotent`/`daemon_e2e`/`handoff` int\n// tests) replays output through with no later `attach`. So `handoff` must (1)\n// re-subscribe for a gapless resume AND (2) seed `session_cursors` at\n// `prior.next_seq` so the consumer runs the production dedup-below + snap-above\n// path (resume mode), never the strict reject-gap legacy path that turns any\n// at-least-once boundary re-send into a fatal `output gap` (fix #3).\n//\n// We assert this against a REAL bound broker + a REAL session (so `handoff`\n// connects + handshakes + subscribes for real). The behavioral end-to-end proof\n// that the epoch gate eliminates the two-writer race lives in the broker int\n// keystone (`controller_writer_reorder_*`) and the restart carrier\n// (`attach_survives_target_brain_restart_exactly_once`); HERE we lock the\n// load-bearing post-conditions of `handoff` itself, deterministically.\n#[test]\nfn handoff_seeds_resume_cursor_and_resubscribes_for_resume() {\n    init_home();\n    let dir = tempfile::tempdir().expect(\"tempdir\");\n    let name = unique_name();\n    // A non-net broker is sufficient — handoff is pure brain-IPC (connect +\n    // handshake + local cursor rehydrate); no QUIC needed.\n    let broker = Broker::bind_in(&name, dir.path().join(\"effects.log\")).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n\n    // A driver brain spawns the session and reads exactly ONE output chunk, so its\n    // consumption cursor (`next_seq`) sits at 1 while the broker's ring holds more\n    // — the worst-case stale floor the bug hinged on (`prior.next_seq` is the\n    // CONSUMPTION cursor, not the successor connection's delivered state).\n    let mut driver = connect_retry(&name);\n    let sid = driver.spawn_session(echo_spawn_req()).expect(\"spawn echo\");\n    driver.send_effect(MintedOp::new(Minter::Rc, 1), b\"SEED_ONE\\r\").expect(\"seed input\");\n    // Read one Output event to advance the driver's next_seq to 1.\n    loop {\n        match driver.read_event().expect(\"driver event\") {\n            BrokerEvent::Output { .. } => break,\n            BrokerEvent::Error { message } => panic!(\"driver broker error: {message}\"),\n            _ => {}\n        }\n    }\n    let state: BrainState = driver.snapshot();\n    let prior_next_seq = state.next_seq;\n    let prior_generation = state.generation;\n    assert!(\n        prior_next_seq >= 1,\n        \"driver must have consumed at least seq 0 so the resume cursor is non-trivial \\\n         (got next_seq={prior_next_seq})\"\n    );\n    drop(driver); // the \"crash\" — broker, session, output log all survive\n\n    // THE HANDOFF. This connects + handshakes + rehydrates local state AND\n    // re-subscribes from the resume cursor (the gapless-resume mechanism). The\n    // reorder safety for a later `serve_attach` re-serve over this same socket is\n    // the broker's epoch gate, not the absence of this subscribe.\n    let succ = Brain::handoff(&name, state, 2).expect(\"handoff\");\n\n    // (fix #3) The resume cursor for this session is seeded at exactly the prior\n    // consumption point — so `read_event` routes it through the dedup-below +\n    // snap-above path (resume mode), never the reject-gap legacy trap.\n    assert_eq!(\n        succ.session_cursor(sid),\n        Some(prior_next_seq),\n        \"handoff seeds session_cursors at prior.next_seq (resume-mode dedup floor) — \\\n         this is what keeps a re-served at-least-once boundary re-send from becoming \\\n         a fatal `output gap` (REQ-HAZARD-CONTROLLER-WRITER-REORDER fix #3)\"\n    );\n    // The brain is in RESUME mode (the per-session map is populated), which is the\n    // structural precondition the fix depends on — a handoff that left the map\n    // empty (the pre-fix shape) would fall into the strict single-`next_seq`\n    // reject-gap path.\n    assert!(\n        succ.session_cursor(sid).is_some(),\n        \"handoff leaves the successor in resume mode for the carried session\"\n    );\n    // The local resume bookkeeping matches the snapshot (generation bumped, the\n    // session carried, the cursor rehydrated) alongside the resume subscribe.\n    assert_eq!(succ.session_id(), Some(sid), \"the carried session id is set\");\n    assert_eq!(\n        succ.next_seq(),\n        prior_next_seq,\n        \"the legacy resume cursor is rehydrated from the snapshot\"\n    );\n    assert_eq!(\n        succ.generation(),\n        prior_generation + 1,\n        \"handoff bumps the generation (a logic handoff, not a second reader)\"\n    );\n\n    // Cleanup: reap the echo child (the session id is carried on `succ`).\n    let mut succ = succ;\n    let _ = succ.kill_session();\n    drop(succ);\n    let _ = &broker; // hold the broker for the whole test, then drop\n}\n\n[Showing lines 1039-1458 of 1550. Use :1459 to continue]"}],"details":{"truncation":{"content":"use spt_daemon::msg::SubscribeOutcome;\r\n\r\n/// Spawn a LABELED echo session on a fresh broker; return (broker, name, brain,\r\n/// sid). The spawner is the broker's by=None local controller placeholder.\r\nfn labeled_session(label: &str) -> (Arc<Broker>, String, Brain, u64) {\r\n    init_home();\r\n    let dir = HOME.get().expect(\"home\").path().to_path_buf();\r\n    let name = unique_name();\r\n    let broker = net_broker(&name, &dir.join(label));\r\n    let mut spawner = connect_retry(&name);\r\n    let sid = spawner\r\n        .spawn_session(SpawnReq {\r\n            endpoint: label.to_string(),\r\n            ..echo_spawn_req()\r\n        })\r\n        .expect(\"spawn labeled echo\");\r\n    (broker, name, spawner, sid)\r\n}\r\n\r\n/// Read events until the broker's `Subscribed` outcome arrives (skipping any\r\n/// replayed output / size frames), returning the resolved role.\r\nfn read_outcome(brain: &mut Brain) -> SubscribeOutcome {\r\n    for _ in 0..2000 {\r\n        match brain.read_event().expect(\"event\") {\r\n            BrokerEvent::Subscribed { outcome, .. } => return outcome,\r\n            _ => continue,\r\n        }\r\n    }\r\n    panic!(\"no Subscribed outcome\");\r\n}\r\n\r\n// [int->REQ-RCVIEW-1] [int->REQ-KICK-1] The controller/viewer/take matrix and the\r\n// loud kick over the broker fan-out: a FREE endpoint's first Control becomes\r\n// controller; a DIFFERENT operator's Control is BUSY-REFUSED (never a silent\r\n// displace, gate #4); a Viewer coexists; an explicit Take displaces the\r\n// incumbent with a LOUD `Displaced{by}` and becomes controller.\r\n#[test]\r\nfn controller_viewer_matrix_and_loud_take() {\r\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-matrix\");\r\n\r\n    // Operator A drives an (undriven) endpoint → controller.\r\n    let mut a = connect_retry(&name);\r\n    a.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"A control\");\r\n    assert_eq!(read_outcome(&mut a), SubscribeOutcome::Controller, \"free → controller\");\r\n\r\n    // Operator B's plain Control hits a CONTROLLED endpoint → busy-refuse, NOT a\r\n    // silent displace (gate #4). A is untouched.\r\n    let mut b = connect_retry(&name);\r\n    b.attach_as(sid, 0, AttachIntent::Control, Some(\"node-B\")).expect(\"B control\");\r\n    assert_eq!(\r\n        read_outcome(&mut b),\r\n        SubscribeOutcome::BusyControlled { by: \"node-A\".to_string() },\r\n        \"controlled → busy-refuse with the controller's identity\"\r\n    );\r\n\r\n    // A viewer coexists (read-only; never displaces).\r\n    let mut v = connect_retry(&name);\r\n    v.attach_as(sid, 0, AttachIntent::Viewer, Some(\"node-V\")).expect(\"V view\");\r\n    assert_eq!(read_outcome(&mut v), SubscribeOutcome::Viewer, \"viewer attaches\");\r\n\r\n    // B's explicit Take displaces A: A gets a LOUD Displaced{by:node-B}, B becomes\r\n    // controller. This is the ONLY displace path.\r\n    b.attach_as(sid, 0, AttachIntent::Take, Some(\"node-B\")).expect(\"B take\");\r\n    assert_eq!(read_outcome(&mut b), SubscribeOutcome::TookControl, \"take → took control\");\r\n    let displaced = (0..2000).find_map(|_| match a.read_event().expect(\"A event\") {\r\n        BrokerEvent::Displaced { by, .. } => Some(by),\r\n        _ => None,\r\n    });\r\n    assert_eq!(displaced.as_deref(), Some(\"node-B\"), \"A receives the loud kick notice\");\r\n}\r\n\r\n// [int->REQ-RCVIEW-1] GATE #7 (the self-kick trap): a SAME-origin re-subscribe —\r\n// a successor re-taking the controller slot after a brain restart — silently\r\n// re-takes (Controller), and NEVER fires Displaced to the prior holder. Only a\r\n// DIFFERENT origin's Take is loud. Here the same `by` re-subscribes and we assert\r\n// no Displaced reaches the first connection.\r\n#[test]\r\nfn same_origin_re_subscribe_does_not_displace() {\r\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-gate7\");\r\n\r\n    // `first` rides the PUMP carrier so `read_event_until`'s deadline is honored\r\n    // (a Whole conn blocks forever — the deadline is a pump-only feature). We need\r\n    // a BOUNDED read to assert the absence of a Displaced frame.\r\n    let mut first =\r\n        Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"first pump conn\");\r\n    first.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"first control\");\r\n    assert_eq!(read_outcome(&mut first), SubscribeOutcome::Controller);\r\n\r\n    // The \"successor\" re-serves the SAME operator's stream (same by) on a new\r\n    // connection — the brain-restart re-take.\r\n    let mut successor = connect_retry(&name);\r\n    successor.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"successor control\");\r\n    assert_eq!(\r\n        read_outcome(&mut successor),\r\n        SubscribeOutcome::Controller,\r\n        \"same-origin re-subscribe silently re-takes the controller\"\r\n    );\r\n\r\n    // The first connection must NOT receive a Displaced — re-taking your own slot\r\n    // is never a kick. Drive a marker through the successor and confirm `first`\r\n    // saw no Displaced in the window.\r\n    successor.send_effect(MintedOp::new(Minter::Rc, 7), b\"GATE7\\r\").expect(\"drive\");\r\n    let mut saw_displaced = false;\r\n    for _ in 0..50 {\r\n        // Non-fatal drain: the first conn may see nothing at all (it was silently\r\n        // superseded); we only care that it is NEVER a Displaced.\r\n        match first.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20))) {\r\n            Ok(BrokerEvent::Displaced { .. }) => {\r\n                saw_displaced = true;\r\n                break;\r\n            }\r\n            Ok(_) => continue,\r\n            Err(_) => break, // timeout — nothing pending\r\n        }\r\n    }\r\n    assert!(!saw_displaced, \"a same-origin re-take must never kick the prior holder (gate #7)\");\r\n}\r\n\r\n// [int->REQ-RCVIEW-1] Resize is CONTROLLER-EXCLUSIVE: a resize arriving on a\r\n// viewer's connection is refused by the broker (gate #3); the controller's\r\n// resize is honored.\r\n#[test]\r\nfn resize_is_controller_exclusive() {\r\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-resize\");\r\n\r\n    // PUMP conns: this test does bounded reads expecting a TIMEOUT (the\r\n    // controller's resize is honored ⇒ NO Error frame arrives), and the\r\n    // `read_event_until` deadline is only honored on the pump carrier — a Whole\r\n    // conn blocks forever when nothing is incoming (hung the Linux runner: `cat`\r\n    // emits nothing on resize, unlike Windows `findstr`).\r\n    let mut ctrl = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"ctrl conn\");\r\n    ctrl.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"control\");\r\n    assert_eq!(read_outcome(&mut ctrl), SubscribeOutcome::Controller);\r\n\r\n    let mut view = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"view conn\");\r\n    view.attach_as(sid, 0, AttachIntent::Viewer, Some(\"node-V\")).expect(\"view\");\r\n    assert_eq!(read_outcome(&mut view), SubscribeOutcome::Viewer);\r\n\r\n    // The viewer's resize is rejected (it is not the controller).\r\n    view.resize(50, 100).expect(\"send resize\");\r\n    let refused = (0..200).find_map(|_| {\r\n        match view.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20))) {\r\n            Ok(BrokerEvent::Error { message }) => Some(message),\r\n            _ => None,\r\n        }\r\n    });\r\n    assert!(\r\n        refused.as_deref().is_some_and(|m| m.contains(\"controller-exclusive\")),\r\n        \"a viewer's resize is refused as controller-exclusive, got {refused:?}\"\r\n    );\r\n\r\n    // The controller's resize is honored (no error).\r\n    ctrl.resize(50, 100).expect(\"controller resize\");\r\n    let ctrl_err = ctrl.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(50)));\r\n    assert!(\r\n        !matches!(ctrl_err, Ok(BrokerEvent::Error { .. })),\r\n        \"the controller's resize is honored\"\r\n    );\r\n}\r\n\r\n// [int->REQ-HAZARD-VIEWER-ISOLATION] A wedged viewer (one that stops draining its\r\n// socket) must NEVER stall the controller or the child: the drain `try_send`s and\r\n// evicts the viewer, while the controller keeps receiving output. Drive a burst\r\n// large enough to overflow the viewer's bounded queue + socket buffer, then a\r\n// trailing marker — the controller (actively read) renders the marker regardless.\r\n//\r\n// [int->REQ-HAZARD-CONTROLLER-GAP-RESUME] The controller is driven through\r\n// `read_controller_event_resuming`, NOT raw `read_event`, because the b4\r\n// drop-don't-block fix decoupled controller backpressure from the drain: the\r\n// controller is now a NON-BLOCKING try_send that DROPS on a full channel\r\n// (CONTROLLER_CHANNEL_DEPTH=4096), no longer a blocking writer. Under the 200KB\r\n// echo flood the controller falls behind its OWN output and the broker drops\r\n// controller frames (advancing nothing — `delivered_through` freezes), which\r\n// surfaces to the strict controller cursor as an `output gap`. The helper\r\n// RESUMES-FROM-FLOOR on that gap (re-fetch from `controller_resume_floor`, B2 — a\r\n// re-fetch, not a viewer-style snap).\r\n//\r\n// VIEWER-ISOLATION asserts the wedged viewer does not STALL the controller — and a\r\n// PROMPT CLEAN outcome proves that whichever way it lands (doyle ruling, the burst\r\n// is deliberately oversized so we assert non-stall robustly, not a fragile\r\n// ring-fit burst size). Three regimes (CONTROLLER_CHANNEL_DEPTH=4096, ring=4096):\r\n// (1) <4096 frames fit → no drop → renders ISOLATED; (2) 4096..8192 → drop → gap →\r\n// re-fetch, ring still retains → RECOVERABLE → renders ISOLATED; (3) >8192 (this\r\n// 200KB burst) → the ring rolls PAST the frozen delivered_through → the dropped\r\n// frames are GONE → exactly-once is genuinely impossible → a clean\r\n// `ControllerIrrecoverablyBehind` (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND). The\r\n// RECOVERABLE re-fetch (regime 2) is proven deterministically by the in-process\r\n// unit `controller_resume_from_floor_refetches_after_a_gap`; this int proves only\r\n// the non-stall property: the controller gets ISOLATED (Windows ConPTY floods\r\n// slower → regime 1/2) OR a clean irrecoverable marker (Linux forkpty → regime 3)\r\n// — NEVER a hang and NEVER the viewer's wedge propagating.\r\n#[test]\r\nfn wedged_viewer_does_not_stall_controller() {\r\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-isolation\");\r\n\r\n    let mut ctrl = connect_retry(&name);\r\n    ctrl.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"control\");\r\n    assert_eq!(read_outcome(&mut ctrl), SubscribeOutcome::Controller);\r\n\r\n    // A viewer attaches but then NEVER reads — its writer thread blocks on the\r\n    // socket, its bounded queue fills, and the drain evicts it.\r\n    let mut view = connect_retry(&name);\r\n    view.attach_as(sid, 0, AttachIntent::Viewer, Some(\"node-V\")).expect(\"view\");\r\n    assert_eq!(read_outcome(&mut view), SubscribeOutcome::Viewer);\r\n    // (deliberately drop the read side: `view` is never polled again)\r\n\r\n    // A big burst echoed by the child overflows the wedged viewer; a trailing\r\n    // marker proves the controller's stream kept flowing.\r\n    let big = vec![b'x'; 200_000];\r\n    ctrl.send_effect(MintedOp::new(Minter::Rc, 1), &big).expect(\"burst\");\r\n    ctrl.send_effect(MintedOp::new(Minter::Rc, 2), b\"\\rISOLATED\\r\").expect(\"marker\");\r\n\r\n    let mut got = Vec::new();\r\n    let mut irrecoverable = false;\r\n    let deadline = std::time::Instant::now() + Duration::from_secs(20);\r\n    while std::time::Instant::now() < deadline {\r\n        match read_controller_event_resuming(&mut ctrl, sid, Some(\"node-A\")) {\r\n            Ok(BrokerEvent::Output { bytes, .. }) => {\r\n                got.extend_from_slice(&bytes);\r\n                if count(&got, b\"ISOLATED\") >= 1 {\r\n                    break;\r\n                }\r\n            }\r\n            Ok(_) => continue,\r\n            // The controller fell PAST the ring on its OWN (the oversized 200KB burst\r\n            // exceeds CONTROLLER_CHANNEL_DEPTH + ring) — a PROMPT, CLEAN, MARKED\r\n            // outcome, NOT a hang and NOT the wedged viewer's stall propagating. This\r\n            // proves VIEWER-ISOLATION exactly as much as rendering ISOLATED does, so\r\n            // accept it (do NOT .expect()-panic on the irrecoverable marker).\r\n            Err(e) if e.get_ref().is_some_and(|i| i.is::<ControllerIrrecoverablyBehind>()) => {\r\n                irrecoverable = true;\r\n                break;\r\n            }\r\n            Err(e) => panic!(\"controller errored unexpectedly (not the irrecoverable marker): {e}\"),\r\n        }\r\n    }\r\n    assert!(\r\n        count(&got, b\"ISOLATED\") >= 1 || irrecoverable,\r\n        \"the wedged viewer must NOT stall the controller: the controller gets a prompt \\\r\n         clean outcome — either renders the ISOLATED marker (re-fetch succeeded) or \\\r\n         surfaces a clean ControllerIrrecoverablyBehind (it fell past the ring on its \\\r\n         own). Got NEITHER within the deadline = a hang / stall.\"\r\n    );\r\n    // Hold the viewer connection open until here so the eviction was the bounded-\r\n    // queue overflow path, not a connection drop.\r\n    drop(view);\r\n}\r\n\r\n/// Read a viewer's events until `needle` appears in its rendered output, PANICKING\r\n/// if a `Displaced` ever arrives (a viewer must never be displaced) — bounded by a\r\n/// deadline (the viewer rides a pump conn so the per-slice timeout is honored).\r\nfn viewer_reads_marker_no_displace(brain: &mut Brain, needle: &[u8]) -> bool {\r\n    let mut got = Vec::new();\r\n    let deadline = std::time::Instant::now() + Duration::from_secs(15);\r\n    while std::time::Instant::now() < deadline {\r\n        match brain.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200))) {\r\n            Ok(BrokerEvent::Output { bytes, .. }) => {\r\n                got.extend_from_slice(&bytes);\r\n                if count(&got, needle) >= 1 {\r\n                    return true;\r\n                }\r\n            }\r\n            Ok(BrokerEvent::Displaced { .. }) => panic!(\"a viewer must NEVER receive Displaced\"),\r\n            Ok(_) => continue,    // Size / other — keep reading\r\n            Err(_) => continue,   // slice timeout — keep waiting until the deadline\r\n        }\r\n    }\r\n    false\r\n}\r\n\r\n// [int->REQ-RCVIEW-1] GATE #7 COMBINED (doyle's named close-out): a controller +\r\n// a viewer are both attached when the controller's serving brain RESTARTS (its\r\n// connection drops; a successor re-serves with the SAME operator identity). The\r\n// successor silently re-takes the controller slot — NO `Displaced` to anyone — and\r\n// the VIEWER survives the churn and keeps receiving output. This is the self-kick\r\n// guard ∩ viewer survival, the one scenario the separate tests left uncombined.\r\n#[test]\r\nfn controller_restart_with_viewer_no_displace_and_viewer_survives() {\r\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-gate7-combined\");\r\n\r\n    // Controller on a pump conn (so a bounded read is possible).\r\n    let mut ctrl = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"ctrl conn\");\r\n    ctrl.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"ctrl control\");\r\n    assert_eq!(read_outcome(&mut ctrl), SubscribeOutcome::Controller);\r\n\r\n    // Viewer attached BEFORE the crash (pump conn).\r\n    let mut view = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"view conn\");\r\n    view.attach_as(sid, 0, AttachIntent::Viewer, Some(\"node-V\")).expect(\"view\");\r\n    assert_eq!(read_outcome(&mut view), SubscribeOutcome::Viewer);\r\n\r\n    // The viewer renders pre-crash output (it is genuinely live).\r\n    ctrl.send_effect(MintedOp::new(Minter::Rc, 1), b\"BEFORE\\r\").expect(\"before marker\");\r\n    assert!(\r\n        viewer_reads_marker_no_displace(&mut view, b\"BEFORE\"),\r\n        \"viewer renders pre-restart output\"\r\n    );\r\n\r\n    // CRASH the controller's serving brain — drop its connection. The broker keeps\r\n    // the session, the OutputLog, and the (separate) viewer connection.\r\n    drop(ctrl);\r\n\r\n    // SUCCESSOR re-serves with the SAME identity (node-A) — the brain-restart\r\n    // re-take. It silently becomes controller (NEVER displacing anyone): the\r\n    // gate-#7 self-kick guard on a real controller-conn restart.\r\n    let mut succ = Brain::cold_start_pump(&name, 2, Duration::from_secs(5)).expect(\"succ conn\");\r\n    succ.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"succ control\");\r\n    assert_eq!(\r\n        read_outcome(&mut succ),\r\n        SubscribeOutcome::Controller,\r\n        \"successor silently re-takes the controller slot — no self-kick (gate #7)\"\r\n    );\r\n\r\n    // The viewer SURVIVED the controller restart: output driven by the successor\r\n    // reaches it, and it NEVER saw a Displaced.\r\n    succ.send_effect(MintedOp::new(Minter::Rc, 2), b\"AFTER\\r\").expect(\"after marker\");\r\n    assert!(\r\n        viewer_reads_marker_no_displace(&mut view, b\"AFTER\"),\r\n        \"viewer survives the controller restart and keeps receiving (no Displaced)\"\r\n    );\r\n}\r\n\r\n// [int->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\r\n// v0.13.0 P1c — the handoff side of the fix (the defense-in-depth seed). The\r\n// controller-writer reorder is fixed in the BROKER (the epoch gate guarantees one\r\n// live `controller_writer` per connection), NOT by dropping handoff's eager\r\n// subscribe — that subscribe IS the resume mechanism every standalone handoff\r\n// caller (the brain-only update engine, `idempotent`/`daemon_e2e`/`handoff` int\r\n// tests) replays output through with no later `attach`. So `handoff` must (1)\r\n// re-subscribe for a gapless resume AND (2) seed `session_cursors` at\r\n// `prior.next_seq` so the consumer runs the production dedup-below + snap-above\r\n// path (resume mode), never the strict reject-gap legacy path that turns any\r\n// at-least-once boundary re-send into a fatal `output gap` (fix #3).\r\n//\r\n// We assert this against a REAL bound broker + a REAL session (so `handoff`\r\n// connects + handshakes + subscribes for real). The behavioral end-to-end proof\r\n// that the epoch gate eliminates the two-writer race lives in the broker int\r\n// keystone (`controller_writer_reorder_*`) and the restart carrier\r\n// (`attach_survives_target_brain_restart_exactly_once`); HERE we lock the\r\n// load-bearing post-conditions of `handoff` itself, deterministically.\r\n#[test]\r\nfn handoff_seeds_resume_cursor_and_resubscribes_for_resume() {\r\n    init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let name = unique_name();\r\n    // A non-net broker is sufficient — handoff is pure brain-IPC (connect +\r\n    // handshake + local cursor rehydrate); no QUIC needed.\r\n    let broker = Broker::bind_in(&name, dir.path().join(\"effects.log\")).expect(\"bind broker\");\r\n    let serve = Arc::clone(&broker);\r\n    thread::spawn(move || {\r\n        let _ = serve.serve();\r\n    });\r\n\r\n    // A driver brain spawns the session and reads exactly ONE output chunk, so its\r\n    // consumption cursor (`next_seq`) sits at 1 while the broker's ring holds more\r\n    // — the worst-case stale floor the bug hinged on (`prior.next_seq` is the\r\n    // CONSUMPTION cursor, not the successor connection's delivered state).\r\n    let mut driver = connect_retry(&name);\r\n    let sid = driver.spawn_session(echo_spawn_req()).expect(\"spawn echo\");\r\n    driver.send_effect(MintedOp::new(Minter::Rc, 1), b\"SEED_ONE\\r\").expect(\"seed input\");\r\n    // Read one Output event to advance the driver's next_seq to 1.\r\n    loop {\r\n        match driver.read_event().expect(\"driver event\") {\r\n            BrokerEvent::Output { .. } => break,\r\n            BrokerEvent::Error { message } => panic!(\"driver broker error: {message}\"),\r\n            _ => {}\r\n        }\r\n    }\r\n    let state: BrainState = driver.snapshot();\r\n    let prior_next_seq = state.next_seq;\r\n    let prior_generation = state.generation;\r\n    assert!(\r\n        prior_next_seq >= 1,\r\n        \"driver must have consumed at least seq 0 so the resume cursor is non-trivial \\\r\n         (got next_seq={prior_next_seq})\"\r\n    );\r\n    drop(driver); // the \"crash\" — broker, session, output log all survive\r\n\r\n    // THE HANDOFF. This connects + handshakes + rehydrates local state AND\r\n    // re-subscribes from the resume cursor (the gapless-resume mechanism). The\r\n    // reorder safety for a later `serve_attach` re-serve over this same socket is\r\n    // the broker's epoch gate, not the absence of this subscribe.\r\n    let succ = Brain::handoff(&name, state, 2).expect(\"handoff\");\r\n\r\n    // (fix #3) The resume cursor for this session is seeded at exactly the prior\r\n    // consumption point — so `read_event` routes it through the dedup-below +\r\n    // snap-above path (resume mode), never the reject-gap legacy trap.\r\n    assert_eq!(\r\n        succ.session_cursor(sid),\r\n        Some(prior_next_seq),\r\n        \"handoff seeds session_cursors at prior.next_seq (resume-mode dedup floor) — \\\r\n         this is what keeps a re-served at-least-once boundary re-send from becoming \\\r\n         a fatal `output gap` (REQ-HAZARD-CONTROLLER-WRITER-REORDER fix #3)\"\r\n    );\r\n    // The brain is in RESUME mode (the per-session map is populated), which is the\r\n    // structural precondition the fix depends on — a handoff that left the map\r\n    // empty (the pre-fix shape) would fall into the strict single-`next_seq`\r\n    // reject-gap path.\r\n    assert!(\r\n        succ.session_cursor(sid).is_some(),\r\n        \"handoff leaves the successor in resume mode for the carried session\"\r\n    );\r\n    // The local resume bookkeeping matches the snapshot (generation bumped, the\r\n    // session carried, the cursor rehydrated) alongside the resume subscribe.\r\n    assert_eq!(succ.session_id(), Some(sid), \"the carried session id is set\");\r\n    assert_eq!(\r\n        succ.next_seq(),\r\n        prior_next_seq,\r\n        \"the legacy resume cursor is rehydrated from the snapshot\"\r\n    );\r\n    assert_eq!(\r\n        succ.generation(),\r\n        prior_generation + 1,\r\n        \"handoff bumps the generation (a logic handoff, not a second reader)\"\r\n    );\r\n\r\n    // Cleanup: reap the echo child (the session id is carried on `succ`).\r\n    let mut succ = succ;\r\n    let _ = succ.kill_session();\r\n    drop(succ);\r\n    let _ = &broker; // hold the broker for the whole test, then drop\r","truncated":true,"truncatedBy":"lines","totalLines":512,"totalBytes":22103,"outputLines":420,"outputBytes":22103,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"use spt_daemon::msg::SubscribeOutcome;\r\n\r\n/// Spawn a LABELED echo session on a fresh broker; return (broker, name, brain,\r\n/// sid). The spawner is the broker's by=None local controller placeholder.\r\nfn labeled_session(label: &str) -> (Arc<Broker>, String, Brain, u64) {\r\n    init_home();\r\n    let dir = HOME.get().expect(\"home\").path().to_path_buf();\r\n    let name = unique_name();\r\n    let broker = net_broker(&name, &dir.join(label));\r\n    let mut spawner = connect_retry(&name);\r\n    let sid = spawner\r\n        .spawn_session(SpawnReq {\r\n            endpoint: label.to_string(),\r\n            ..echo_spawn_req()\r\n        })\r\n        .expect(\"spawn labeled echo\");\r\n    (broker, name, spawner, sid)\r\n}\r\n\r\n/// Read events until the broker's `Subscribed` outcome arrives (skipping any\r\n/// replayed output / size frames), returning the resolved role.\r\nfn read_outcome(brain: &mut Brain) -> SubscribeOutcome {\r\n    for _ in 0..2000 {\r\n        match brain.read_event().expect(\"event\") {\r\n            BrokerEvent::Subscribed { outcome, .. } => return outcome,\r\n            _ => continue,\r\n        }\r\n    }\r\n    panic!(\"no Subscribed outcome\");\r\n}\r\n\r\n// [int->REQ-RCVIEW-1] [int->REQ-KICK-1] The controller/viewer/take matrix and the\r\n// loud kick over the broker fan-out: a FREE endpoint's first Control becomes\r\n// controller; a DIFFERENT operator's Control is BUSY-REFUSED (never a silent\r\n// displace, gate #4); a Viewer coexists; an explicit Take displaces the\r\n// incumbent with a LOUD `Displaced{by}` and becomes controller.\r\n#[test]\r\nfn controller_viewer_matrix_and_loud_take() {\r\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-matrix\");\r\n\r\n    // Operator A drives an (undriven) endpoint → controller.\r\n    let mut a = connect_retry(&name);\r\n    a.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"A control\");\r\n    assert_eq!(read_outcome(&mut a), SubscribeOutcome::Controller, \"free → controller\");\r\n\r\n    // Operator B's plain Control hits a CONTROLLED endpoint → busy-refuse, NOT a\r\n    // silent displace (gate #4). A is untouched.\r\n    let mut b = connect_retry(&name);\r\n    b.attach_as(sid, 0, AttachIntent::Control, Some(\"node-B\")).expect(\"B control\");\r\n    assert_eq!(\r\n        read_outcome(&mut b),\r\n        SubscribeOutcome::BusyControlled { by: \"node-A\".to_string() },\r\n        \"controlled → busy-refuse with the controller's identity\"\r\n    );\r\n\r\n    // A viewer coexists (read-only; never displaces).\r\n    let mut v = connect_retry(&name);\r\n    v.attach_as(sid, 0, AttachIntent::Viewer, Some(\"node-V\")).expect(\"V view\");\r\n    assert_eq!(read_outcome(&mut v), SubscribeOutcome::Viewer, \"viewer attaches\");\r\n\r\n    // B's explicit Take displaces A: A gets a LOUD Displaced{by:node-B}, B becomes\r\n    // controller. This is the ONLY displace path.\r\n    b.attach_as(sid, 0, AttachIntent::Take, Some(\"node-B\")).expect(\"B take\");\r\n    assert_eq!(read_outcome(&mut b), SubscribeOutcome::TookControl, \"take → took control\");\r\n    let displaced = (0..2000).find_map(|_| match a.read_event().expect(\"A event\") {\r\n        BrokerEvent::Displaced { by, .. } => Some(by),\r\n        _ => None,\r\n    });\r\n    assert_eq!(displaced.as_deref(), Some(\"node-B\"), \"A receives the loud kick notice\");\r\n}\r\n\r\n// [int->REQ-RCVIEW-1] GATE #7 (the self-kick trap): a SAME-origin re-subscribe —\r\n// a successor re-taking the controller slot after a brain restart — silently\r\n// re-takes (Controller), and NEVER fires Displaced to the prior holder. Only a\r\n// DIFFERENT origin's Take is loud. Here the same `by` re-subscribes and we assert\r\n// no Displaced reaches the first connection.\r\n#[test]\r\nfn same_origin_re_subscribe_does_not_displace() {\r\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-gate7\");\r\n\r\n    // `first` rides the PUMP carrier so `read_event_until`'s deadline is honored\r\n    // (a Whole conn blocks forever — the deadline is a pump-only feature). We need\r\n    // a BOUNDED read to assert the absence of a Displaced frame.\r\n    let mut first =\r\n        Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"first pump conn\");\r\n    first.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"first control\");\r\n    assert_eq!(read_outcome(&mut first), SubscribeOutcome::Controller);\r\n\r\n    // The \"successor\" re-serves the SAME operator's stream (same by) on a new\r\n    // connection — the brain-restart re-take.\r\n    let mut successor = connect_retry(&name);\r\n    successor.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"successor control\");\r\n    assert_eq!(\r\n        read_outcome(&mut successor),\r\n        SubscribeOutcome::Controller,\r\n        \"same-origin re-subscribe silently re-takes the controller\"\r\n    );\r\n\r\n    // The first connection must NOT receive a Displaced — re-taking your own slot\r\n    // is never a kick. Drive a marker through the successor and confirm `first`\r\n    // saw no Displaced in the window.\r\n    successor.send_effect(MintedOp::new(Minter::Rc, 7), b\"GATE7\\r\").expect(\"drive\");\r\n    let mut saw_displaced = false;\r\n    for _ in 0..50 {\r\n        // Non-fatal drain: the first conn may see nothing at all (it was silently\r\n        // superseded); we only care that it is NEVER a Displaced.\r\n        match first.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20))) {\r\n            Ok(BrokerEvent::Displaced { .. }) => {\r\n                saw_displaced = true;\r\n                break;\r\n            }\r\n            Ok(_) => continue,\r\n            Err(_) => break, // timeout — nothing pending\r\n        }\r\n    }\r\n    assert!(!saw_displaced, \"a same-origin re-take must never kick the prior holder (gate #7)\");\r\n}\r\n\r\n// [int->REQ-RCVIEW-1] Resize is CONTROLLER-EXCLUSIVE: a resize arriving on a\r\n// viewer's connection is refused by the broker (gate #3); the controller's\r\n// resize is honored.\r\n#[test]\r\nfn resize_is_controller_exclusive() {\r\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-resize\");\r\n\r\n    // PUMP conns: this test does bounded reads expecting a TIMEOUT (the\r\n    // controller's resize is honored ⇒ NO Error frame arrives), and the\r\n    // `read_event_until` deadline is only honored on the pump carrier — a Whole\r\n    // conn blocks forever when nothing is incoming (hung the Linux runner: `cat`\r\n    // emits nothing on resize, unlike Windows `findstr`).\r\n    let mut ctrl = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"ctrl conn\");\r\n    ctrl.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"control\");\r\n    assert_eq!(read_outcome(&mut ctrl), SubscribeOutcome::Controller);\r\n\r\n    let mut view = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"view conn\");\r\n    view.attach_as(sid, 0, AttachIntent::Viewer, Some(\"node-V\")).expect(\"view\");\r\n    assert_eq!(read_outcome(&mut view), SubscribeOutcome::Viewer);\r\n\r\n    // The viewer's resize is rejected (it is not the controller).\r\n    view.resize(50, 100).expect(\"send resize\");\r\n    let refused = (0..200).find_map(|_| {\r\n        match view.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20))) {\r\n            Ok(BrokerEvent::Error { message }) => Some(message),\r\n            _ => None,\r\n        }\r\n    });\r\n    assert!(\r\n        refused.as_deref().is_some_and(|m| m.contains(\"controller-exclusive\")),\r\n        \"a viewer's resize is refused as controller-exclusive, got {refused:?}\"\r\n    );\r\n\r\n    // The controller's resize is honored (no error).\r\n    ctrl.resize(50, 100).expect(\"controller resize\");\r\n    let ctrl_err = ctrl.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(50)));\r\n    assert!(\r\n        !matches!(ctrl_err, Ok(BrokerEvent::Error { .. })),\r\n        \"the controller's resize is honored\"\r\n    );\r\n}\r\n\r\n// [int->REQ-HAZARD-VIEWER-ISOLATION] A wedged viewer (one that stops draining its\r\n// socket) must NEVER stall the controller or the child: the drain `try_send`s and\r\n// evicts the viewer, while the controller keeps receiving output. Drive a burst\r\n// large enough to overflow the viewer's bounded queue + socket buffer, then a\r\n// trailing marker — the controller (actively read) renders the marker regardless.\r\n//\r\n// [int->REQ-HAZARD-CONTROLLER-GAP-RESUME] The controller is driven through\r\n// `read_controller_event_resuming`, NOT raw `read_event`, because the b4\r\n// drop-don't-block fix decoupled controller backpressure from the drain: the\r\n// controller is now a NON-BLOCKING try_send that DROPS on a full channel\r\n// (CONTROLLER_CHANNEL_DEPTH=4096), no longer a blocking writer. Under the 200KB\r\n// echo flood the controller falls behind its OWN output and the broker drops\r\n// controller frames (advancing nothing — `delivered_through` freezes), which\r\n// surfaces to the strict controller cursor as an `output gap`. The helper\r\n// RESUMES-FROM-FLOOR on that gap (re-fetch from `controller_resume_floor`, B2 — a\r\n// re-fetch, not a viewer-style snap).\r\n//\r\n// VIEWER-ISOLATION asserts the wedged viewer does not STALL the controller — and a\r\n// PROMPT CLEAN outcome proves that whichever way it lands (doyle ruling, the burst\r\n// is deliberately oversized so we assert non-stall robustly, not a fragile\r\n// ring-fit burst size). Three regimes (CONTROLLER_CHANNEL_DEPTH=4096, ring=4096):\r\n// (1) <4096 frames fit → no drop → renders ISOLATED; (2) 4096..8192 → drop → gap →\r\n// re-fetch, ring still retains → RECOVERABLE → renders ISOLATED; (3) >8192 (this\r\n// 200KB burst) → the ring rolls PAST the frozen delivered_through → the dropped\r\n// frames are GONE → exactly-once is genuinely impossible → a clean\r\n// `ControllerIrrecoverablyBehind` (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND). The\r\n// RECOVERABLE re-fetch (regime 2) is proven deterministically by the in-process\r\n// unit `controller_resume_from_floor_refetches_after_a_gap`; this int proves only\r\n// the non-stall property: the controller gets ISOLATED (Windows ConPTY floods\r\n// slower → regime 1/2) OR a clean irrecoverable marker (Linux forkpty → regime 3)\r\n// — NEVER a hang and NEVER the viewer's wedge propagating.\r\n#[test]\r\nfn wedged_viewer_does_not_stall_controller() {\r\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-isolation\");\r\n\r\n    let mut ctrl = connect_retry(&name);\r\n    ctrl.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"control\");\r\n    assert_eq!(read_outcome(&mut ctrl), SubscribeOutcome::Controller);\r\n\r\n    // A viewer attaches but then NEVER reads — its writer thread blocks on the\r\n    // socket, its bounded queue fills, and the drain evicts it.\r\n    let mut view = connect_retry(&name);\r\n    view.attach_as(sid, 0, AttachIntent::Viewer, Some(\"node-V\")).expect(\"view\");\r\n    assert_eq!(read_outcome(&mut view), SubscribeOutcome::Viewer);\r\n    // (deliberately drop the read side: `view` is never polled again)\r\n\r\n    // A big burst echoed by the child overflows the wedged viewer; a trailing\r\n    // marker proves the controller's stream kept flowing.\r\n    let big = vec![b'x'; 200_000];\r\n    ctrl.send_effect(MintedOp::new(Minter::Rc, 1), &big).expect(\"burst\");\r\n    ctrl.send_effect(MintedOp::new(Minter::Rc, 2), b\"\\rISOLATED\\r\").expect(\"marker\");\r\n\r\n    let mut got = Vec::new();\r\n    let mut irrecoverable = false;\r\n    let deadline = std::time::Instant::now() + Duration::from_secs(20);\r\n    while std::time::Instant::now() < deadline {\r\n        match read_controller_event_resuming(&mut ctrl, sid, Some(\"node-A\")) {\r\n            Ok(BrokerEvent::Output { bytes, .. }) => {\r\n                got.extend_from_slice(&bytes);\r\n                if count(&got, b\"ISOLATED\") >= 1 {\r\n                    break;\r\n                }\r\n            }\r\n            Ok(_) => continue,\r\n            // The controller fell PAST the ring on its OWN (the oversized 200KB burst\r\n            // exceeds CONTROLLER_CHANNEL_DEPTH + ring) — a PROMPT, CLEAN, MARKED\r\n            // outcome, NOT a hang and NOT the wedged viewer's stall propagating. This\r\n            // proves VIEWER-ISOLATION exactly as much as rendering ISOLATED does, so\r\n            // accept it (do NOT .expect()-panic on the irrecoverable marker).\r\n            Err(e) if e.get_ref().is_some_and(|i| i.is::<ControllerIrrecoverablyBehind>()) => {\r\n                irrecoverable = true;\r\n                break;\r\n            }\r\n            Err(e) => panic!(\"controller errored unexpectedly (not the irrecoverable marker): {e}\"),\r\n        }\r\n    }\r\n    assert!(\r\n        count(&got, b\"ISOLATED\") >= 1 || irrecoverable,\r\n        \"the wedged viewer must NOT stall the controller: the controller gets a prompt \\\r\n         clean outcome — either renders the ISOLATED marker (re-fetch succeeded) or \\\r\n         surfaces a clean ControllerIrrecoverablyBehind (it fell past the ring on its \\\r\n         own). Got NEITHER within the deadline = a hang / stall.\"\r\n    );\r\n    // Hold the viewer connection open until here so the eviction was the bounded-\r\n    // queue overflow path, not a connection drop.\r\n    drop(view);\r\n}\r\n\r\n/// Read a viewer's events until `needle` appears in its rendered output, PANICKING\r\n/// if a `Displaced` ever arrives (a viewer must never be displaced) — bounded by a\r\n/// deadline (the viewer rides a pump conn so the per-slice timeout is honored).\r\nfn viewer_reads_marker_no_displace(brain: &mut Brain, needle: &[u8]) -> bool {\r\n    let mut got = Vec::new();\r\n    let deadline = std::time::Instant::now() + Duration::from_secs(15);\r\n    while std::time::Instant::now() < deadline {\r\n        match brain.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200))) {\r\n            Ok(BrokerEvent::Output { bytes, .. }) => {\r\n                got.extend_from_slice(&bytes);\r\n                if count(&got, needle) >= 1 {\r\n                    return true;\r\n                }\r\n            }\r\n            Ok(BrokerEvent::Displaced { .. }) => panic!(\"a viewer must NEVER receive Displaced\"),\r\n            Ok(_) => continue,    // Size / other — keep reading\r\n            Err(_) => continue,   // slice timeout — keep waiting until the deadline\r\n        }\r\n    }\r\n    false\r\n}\r\n\r\n// [int->REQ-RCVIEW-1] GATE #7 COMBINED (doyle's named close-out): a controller +\r\n// a viewer are both attached when the controller's serving brain RESTARTS (its\r\n// connection drops; a successor re-serves with the SAME operator identity). The\r\n// successor silently re-takes the controller slot — NO `Displaced` to anyone — and\r\n// the VIEWER survives the churn and keeps receiving output. This is the self-kick\r\n// guard ∩ viewer survival, the one scenario the separate tests left uncombined.\r\n#[test]\r\nfn controller_restart_with_viewer_no_displace_and_viewer_survives() {\r\n    let (_broker, name, _spawner, sid) = labeled_session(\"cv-gate7-combined\");\r\n\r\n    // Controller on a pump conn (so a bounded read is possible).\r\n    let mut ctrl = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"ctrl conn\");\r\n    ctrl.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"ctrl control\");\r\n    assert_eq!(read_outcome(&mut ctrl), SubscribeOutcome::Controller);\r\n\r\n    // Viewer attached BEFORE the crash (pump conn).\r\n    let mut view = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect(\"view conn\");\r\n    view.attach_as(sid, 0, AttachIntent::Viewer, Some(\"node-V\")).expect(\"view\");\r\n    assert_eq!(read_outcome(&mut view), SubscribeOutcome::Viewer);\r\n\r\n    // The viewer renders pre-crash output (it is genuinely live).\r\n    ctrl.send_effect(MintedOp::new(Minter::Rc, 1), b\"BEFORE\\r\").expect(\"before marker\");\r\n    assert!(\r\n        viewer_reads_marker_no_displace(&mut view, b\"BEFORE\"),\r\n        \"viewer renders pre-restart output\"\r\n    );\r\n\r\n    // CRASH the controller's serving brain — drop its connection. The broker keeps\r\n    // the session, the OutputLog, and the (separate) viewer connection.\r\n    drop(ctrl);\r\n\r\n    // SUCCESSOR re-serves with the SAME identity (node-A) — the brain-restart\r\n    // re-take. It silently becomes controller (NEVER displacing anyone): the\r\n    // gate-#7 self-kick guard on a real controller-conn restart.\r\n    let mut succ = Brain::cold_start_pump(&name, 2, Duration::from_secs(5)).expect(\"succ conn\");\r\n    succ.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"succ control\");\r\n    assert_eq!(\r\n        read_outcome(&mut succ),\r\n        SubscribeOutcome::Controller,\r\n        \"successor silently re-takes the controller slot — no self-kick (gate #7)\"\r\n    );\r\n\r\n    // The viewer SURVIVED the controller restart: output driven by the successor\r\n    // reaches it, and it NEVER saw a Displaced.\r\n    succ.send_effect(MintedOp::new(Minter::Rc, 2), b\"AFTER\\r\").expect(\"after marker\");\r\n    assert!(\r\n        viewer_reads_marker_no_displace(&mut view, b\"AFTER\"),\r\n        \"viewer survives the controller restart and keeps receiving (no Displaced)\"\r\n    );\r\n}\r\n\r\n// [int->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\r\n// v0.13.0 P1c — the handoff side of the fix (the defense-in-depth seed). The\r\n// controller-writer reorder is fixed in the BROKER (the epoch gate guarantees one\r\n// live `controller_writer` per connection), NOT by dropping handoff's eager\r\n// subscribe — that subscribe IS the resume mechanism every standalone handoff\r\n// caller (the brain-only update engine, `idempotent`/`daemon_e2e`/`handoff` int\r\n// tests) replays output through with no later `attach`. So `handoff` must (1)\r\n// re-subscribe for a gapless resume AND (2) seed `session_cursors` at\r\n// `prior.next_seq` so the consumer runs the production dedup-below + snap-above\r\n// path (resume mode), never the strict reject-gap legacy path that turns any\r\n// at-least-once boundary re-send into a fatal `output gap` (fix #3).\r\n//\r\n// We assert this against a REAL bound broker + a REAL session (so `handoff`\r\n// connects + handshakes + subscribes for real). The behavioral end-to-end proof\r\n// that the epoch gate eliminates the two-writer race lives in the broker int\r\n// keystone (`controller_writer_reorder_*`) and the restart carrier\r\n// (`attach_survives_target_brain_restart_exactly_once`); HERE we lock the\r\n// load-bearing post-conditions of `handoff` itself, deterministically.\r\n#[test]\r\nfn handoff_seeds_resume_cursor_and_resubscribes_for_resume() {\r\n    init_home();\r\n    let dir = tempfile::tempdir().expect(\"tempdir\");\r\n    let name = unique_name();\r\n    // A non-net broker is sufficient — handoff is pure brain-IPC (connect +\r\n    // handshake + local cursor rehydrate); no QUIC needed.\r\n    let broker = Broker::bind_in(&name, dir.path().join(\"effects.log\")).expect(\"bind broker\");\r\n    let serve = Arc::clone(&broker);\r\n    thread::spawn(move || {\r\n        let _ = serve.serve();\r\n    });\r\n\r\n    // A driver brain spawns the session and reads exactly ONE output chunk, so its\r\n    // consumption cursor (`next_seq`) sits at 1 while the broker's ring holds more\r\n    // — the worst-case stale floor the bug hinged on (`prior.next_seq` is the\r\n    // CONSUMPTION cursor, not the successor connection's delivered state).\r\n    let mut driver = connect_retry(&name);\r\n    let sid = driver.spawn_session(echo_spawn_req()).expect(\"spawn echo\");\r\n    driver.send_effect(MintedOp::new(Minter::Rc, 1), b\"SEED_ONE\\r\").expect(\"seed input\");\r\n    // Read one Output event to advance the driver's next_seq to 1.\r\n    loop {\r\n        match driver.read_event().expect(\"driver event\") {\r\n            BrokerEvent::Output { .. } => break,\r\n            BrokerEvent::Error { message } => panic!(\"driver broker error: {message}\"),\r\n            _ => {}\r\n        }\r\n    }\r\n    let state: BrainState = driver.snapshot();\r\n    let prior_next_seq = state.next_seq;\r\n    let prior_generation = state.generation;\r\n    assert!(\r\n        prior_next_seq >= 1,\r\n        \"driver must have consumed at least seq 0 so the resume cursor is non-trivial \\\r\n         (got next_seq={prior_next_seq})\"\r\n    );\r\n    drop(driver); // the \"crash\" — broker, session, output log all survive\r\n\r\n    // THE HANDOFF. This connects + handshakes + rehydrates local state AND\r\n    // re-subscribes from the resume cursor (the gapless-resume mechanism). The\r\n    // reorder safety for a later `serve_attach` re-serve over this same socket is\r\n    // the broker's epoch gate, not the absence of this subscribe.\r\n    let succ = Brain::handoff(&name, state, 2).expect(\"handoff\");\r\n\r\n    // (fix #3) The resume cursor for this session is seeded at exactly the prior\r\n    // consumption point — so `read_event` routes it through the dedup-below +\r\n    // snap-above path (resume mode), never the reject-gap legacy trap.\r\n    assert_eq!(\r\n        succ.session_cursor(sid),\r\n        Some(prior_next_seq),\r\n        \"handoff seeds session_cursors at prior.next_seq (resume-mode dedup floor) — \\\r\n         this is what keeps a re-served at-least-once boundary re-send from becoming \\\r\n         a fatal `output gap` (REQ-HAZARD-CONTROLLER-WRITER-REORDER fix #3)\"\r\n    );\r\n    // The brain is in RESUME mode (the per-session map is populated), which is the\r\n    // structural precondition the fix depends on — a handoff that left the map\r\n    // empty (the pre-fix shape) would fall into the strict single-`next_seq`\r\n    // reject-gap path.\r\n    assert!(\r\n        succ.session_cursor(sid).is_some(),\r\n        \"handoff leaves the successor in resume mode for the carried session\"\r\n    );\r\n    // The local resume bookkeeping matches the snapshot (generation bumped, the\r\n    // session carried, the cursor rehydrated) alongside the resume subscribe.\r\n    assert_eq!(succ.session_id(), Some(sid), \"the carried session id is set\");\r\n    assert_eq!(\r\n        succ.next_seq(),\r\n        prior_next_seq,\r\n        \"the legacy resume cursor is rehydrated from the snapshot\"\r\n    );\r\n    assert_eq!(\r\n        succ.generation(),\r\n        prior_generation + 1,\r\n        \"handoff bumps the generation (a logic handoff, not a second reader)\"\r\n    );\r\n\r\n    // Cleanup: reap the echo child (the session id is carried on `succ`).\r\n    let mut succ = succ;\r\n    let _ = succ.kill_session();\r\n    drop(succ);\r\n    let _ = &broker; // hold the broker for the whole test, then drop\r\n}","startLine":1039,"lineNumbers":[1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\attach.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1550,"totalBytes":22103,"outputLines":420,"outputBytes":22103,"shownRange":{"start":1039,"end":1458},"nextOffset":1459}}},"isError":false,"timestamp":1784195860066}}
{"type":"message","id":"38ec04bb","parentId":"c8f126d4","timestamp":"2026-07-16T09:57:55.572Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing refresh stall causes on PTYs**\n\n**Investigating brain dispatch connection epochs**"},{"type":"toolCall","id":"call_jy9x1Nit5ifcDqUXmSuoysVR|fc_047bb49263dfa303016a58ab2322c08198b96e101f531b521a","name":"grep","arguments":{"i":"Tracing dispatch stream workers","pattern":"run_dispatch_loop|net_streams|serve_attach|stream_id|active|worker|subscribe|epoch|generation|connection","path":"crates/spt-daemon/src/dispatch.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_qY5aJWy58ZC58ZjD6j0yokqO|fc_047bb49263dfa303016a58ab2322d88198b319d5c22684a6d3","name":"grep","arguments":{"i":"Tracing net stream lifecycle","pattern":"struct .*Stream|stream_id|subscribe|unsubscribe|eof|retentive|epoch|generation|initiated_locally|remove\\(|send_stream|open_stream|recv","path":"crates/spt-daemon/src/nethost.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_lhse7jkSs7HiVmhlpt5ViMrb|fc_047bb49263dfa303016a58ab2322e481988d780a0cbca0c4a0","name":"grep","arguments":{"i":"Finding dispatch lifecycle tests","pattern":"restart|resume|stream|worker|brain|serve_attach|detach|reconnect|generation|epoch","path":"crates/spt-daemon/tests;crates/spt/tests","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":7754,"output":579,"cacheRead":77312,"cacheWrite":0,"totalTokens":85645,"reasoningTokens":320,"cost":{"input":0.038770000000000006,"output":0.01737,"cacheRead":0.038655999999999996,"cacheWrite":0,"total":0.094796}},"stopReason":"toolUse","timestamp":1784195860073,"responseId":"resp_047bb49263dfa303016a58ab14b2fc819887d48c857049be9a","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58ab166fcc81989a23d4f977448c85","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKsjTGQsfTNyD_DQeX1ZohV2QrGodPq5asN8ZO6oAsNr-XtJvhrik_4QIFG3bcP0l9XE1y-qOQnWLRR9LC2LRnzpilhwH7PW2J-XWkchuhyTyLuqbkfdoO3SWQa-VvSXh0NO9vcCkuXgzVZiYfdn1wqfXt0yhTO6Uay_DquhoHeV-75dFg3xh3gq9dUflFi9PP508qT5iTfWHR6wnMVVRBWHef3Ayh5ZNLmAdcdfHPQPwNMy5E4QhY_dQmn73CXS1UXCgt4pZ5fA9IY-tRA1UDOIdrTWPz1Szm-osgNb4caUHcnUig683_htUQvdlkqQnb0tMS1J3O3IhnjyMjhxzGK2ICQxgbpN2SbnEnm0tzcoCsdh3uosuGppEeaYl3k1u8LSr0q2np9f5jRw2pRba-B4V0i3wK67T4pbNe0tWtU5SWUHXQr05Wx0L6k9HJMmp9YaC7T2GWAe65YMlL7Raxz5AF3lJRC15oTzKkRyJYOGFjqjjerDyxAcDDN2MDimv6IfHcfPrcjdmj4DNl9F8FxIJhZoKKUcqCTLyJHIQ-DKt5Q_OpNqzonKLjESrME9Wjr6u7YB_SRQceI141H0Yc87nWSxEGH2HJJeHBtwXSwqy5my5FTanE_SrL700us71eaUhRNfnvlH_0KB88pJnxrsO1ipYYszngZHCsfKPWuCD6U34Fsju_5s2lMBAwm90NcrUgUKpA2tQhWL1ZGaNb6BbT2_Dx4AkAwlD1laUGfRnt8a7eWUu8dKhzC2ie58LsObuj7GMWiF8djEnqOIK0Hbgopd6OY-9TG4jmDcqKM-QnmmEuRnID835R-DGKNMcImyG6jS-42lLSbWJ3PgJNoB3rkjVt68HLWAYZjlaoJGqJrKiQhSyVFDBDaNA745ZODCYZqnrwyz_m1nT7iFVTnyzzqwM1qnJNsCTvSefpbW8aellS1ScmWPB_FEubaP-Y4NFDhZbnM9Gg-ykgm06BXQ3YG3iOdz_ihor3mi0EX1WxXURBIwYJOLtRjvkjPNTbZjYzN1vsauz8yq8yrXfvZQKDz6Z2_XfFJqNBOc1VLjUFMq76LQYlVQ9IsCkNNPWF7Y_T98I-9_o1qFBBXPDppQ-ncUJZH78sWnXhI1fRtfbD6iLqvPivGt91GnpXlPsVq1SBU4G8MEKVRu6guvymEccbhNO6PYSOmgyhAMUF9BmUQ9QtqqOTfw6xkKFQTrOm8xSvMMxZbKq9AsU5u_ajMsJwCNqyHT3lRhJENvfNAlWWaunBq-lfbcw3ykUrU1TV_w0NBgQ3DlPnVV9Ex1UYZ4EceMi6aMLmvKCNzE29INdvvxOnIhC9sLXwlcPfyN0p6SZjG4-pF7EXWir6uQCH8XYrKj2EizNXqdLbCfCwHsbEheZJlXiNxixdAn3U8weYI5nnc1RntOpJ32Ilo9tBE_8npSHjXy1QTe6NVVTK280TOMk4QvVEUY-gOqp_HjTzh83l1DmbyThIAZNtvMo1wqpP_emBNvvY8Mns0ouzZUObB_SeozVn2PRuT4Jv7-C99rDmM7zQ54S1OU9vP8sJYn4d_R0zjzJJx1hs4v0zbGdvGyy-Mv-HRcb2TEcQIEFM8viZWL2VKF9Wi0r_oTS-EFiuYbkWeWqx7qXftqkiuMnNH_nTZmUh5el_C4RqdxxY-og4JVHhgc8BGXDPPNCD5x086FOevpzMRXiTU2MvB7FoEPmQP2T2N2Tmj1yr1HfjdB278gibSckAGI0fVWMDgI2keNq0ZG_de63Kwwi1idIP1NexVj7_jD8LSGr1Roz8v-6y1qHH8huCegAwAef1qtsk670H8q9t5UoTSPAqObhMTTBFhLQYCb8EHQFz3mHternMBu3df-1rItACgVtyT5t4mPboymGhPPqaNjXJfYNiPeXxVoUVZCY1AErFfCYNvDE9WVLZQ8vTHK8XAMAzz7nDEFrmL2lPj1Fv30Jr0FdK1PSsgl-Qof3_x-45iOI5KqUb3ZVQpjXNrlSRi-0l1T9HwoBFDhNr3dlABqMvRNjMyxWYAfvfmoLGGZFHyaUNTZBv93wlL3jqUkcX5vS0OqUwROQ6d-0rSD5-7Wc0kcRHb6fvwoavHaSUv2XW5kO7fcwClBN12A1l6wCXZeVRP0M6P0DlffmaZWMW2VG6PWYwRaCTuXTskDO0uoP1bvFHNei1SDiM2TXPyQ_94O9juZqXoNzJWTVYBw6m2aD9eaz-le_jXZ6d36LjDpBWsX8mkyOWL46yALruRdHuPD3gPycDMKreC1tw_YLyK93La93eMCdHReeHqgc3LrMcd0ta74LWmP-wo-lQ_cxJWcyIt9RftYWcV4fqJUyyG2gU_dAdW-awAYJW1D94yY4NoinQF14Cz52t55BotBm4kK0_tc0-VWTKEwX4cgwKVrgPuXDxTl_52CkT7BnljJG2JAxbFaJOj9G3GionrYJj-OxyQXSBtsirvSeybgC4sbBkc5nePwMqkCehGi2mHlJZ2zW8ZvYvCQLtJSJUXuL9IjT6xEn5ZIPjtHzd2SiKf2wad3pVZuegs5Io4oRL0GuIU79xHvRWsQMapMaMjhr1sQmfsxE3dVFeBaSe_O8sWKIadYAYtPRKiVNWvZkjaR21LU0dqOCyuFp4DSBQy3jpwj22dvS-CNyqfSMx07mtY3Yw3WNn4O7IcTGJ1gIO8GxGJR7hU7PYU4GJm9DwXWhvlH1MJ6zd-BYtt1GuvcUqq6Ga2BbMI9j9Tfn57ozIttnYoaEhJV7t8Z1LyCs7ZsEYRjlRmUoQ9HeVpT_pClsKcPJ5gI-ac0GNs8wPIVmFrVwioL-MIvNj7stRxgU7X2wMeuD7SyeBRElsy2GC-OrTDEyZXfDkijWvFf3Nax5imPs54pvGHscn6rPpNWZRQffEU8LgMU7j4__o4jvA-sm8a-vqdmce4nv2hAtcX3-RCGL4iyOwVQDERsIvrTQbKSE9gBHxqTfL9IpPogYIBzIHWu7JHercc--P66ptsV9cpiQO_knRzOiefovg06v_EIT5ilOHLu_DaYGjqwUiZw0EL7OFlSSd1OA7LBuV7msQ6UK5PM5uA-Yt3U7nKobcE0K_T9d28KcQ==","summary":[{"type":"summary_text","text":"**Analyzing refresh stall causes on PTYs**"},{"type":"summary_text","text":"**Investigating brain dispatch connection epochs**"}]},{"id":"fc_047bb49263dfa303016a58ab2322c08198b96e101f531b521a","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing dispatch stream workers\",\"pattern\":\"run_dispatch_loop|net_streams|serve_attach|stream_id|active|worker|subscribe|epoch|generation|connection\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_jy9x1Nit5ifcDqUXmSuoysVR","name":"grep"},{"id":"fc_047bb49263dfa303016a58ab2322d88198b319d5c22684a6d3","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing net stream lifecycle\",\"pattern\":\"struct .*Stream|stream_id|subscribe|unsubscribe|eof|retentive|epoch|generation|initiated_locally|remove\\\\(|send_stream|open_stream|recv\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_qY5aJWy58ZC58ZjD6j0yokqO","name":"grep"},{"id":"fc_047bb49263dfa303016a58ab2322e481988d780a0cbca0c4a0","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding dispatch lifecycle tests\",\"pattern\":\"restart|resume|stream|worker|brain|serve_attach|detach|reconnect|generation|epoch\",\"path\":\"crates/spt-daemon/tests;crates/spt/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_lhse7jkSs7HiVmhlpt5ViMrb","name":"grep"}]},"duration":15476.185899999924,"ttft":2342.0745999999344,"contextSnapshot":{"promptTokens":85066,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_jy9x1Nit5ifcDqUXmSuoysVR|fc_047bb49263dfa303016a58ab2322c08198b96e101f531b521a","toolName":"grep","startedAt":"2026-07-16T09:57:55.572Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Tracing dispatch stream workers"},"id":"e7b4bd15","parentId":"38ec04bb","timestamp":"2026-07-16T09:57:55.572Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qY5aJWy58ZC58ZjD6j0yokqO|fc_047bb49263dfa303016a58ab2322d88198b319d5c22684a6d3","toolName":"grep","startedAt":"2026-07-16T09:57:55.572Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Tracing net stream lifecycle"},"id":"83b7f996","parentId":"e7b4bd15","timestamp":"2026-07-16T09:57:55.572Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_lhse7jkSs7HiVmhlpt5ViMrb|fc_047bb49263dfa303016a58ab2322e481988d780a0cbca0c4a0","toolName":"grep","startedAt":"2026-07-16T09:57:55.572Z","args":{"path":"crates/spt-daemon/tests;crates/spt/tests"},"intent":"Finding dispatch lifecycle tests"},"id":"c963e00c","parentId":"83b7f996","timestamp":"2026-07-16T09:57:55.572Z"}
{"type":"message","id":"e341c873","parentId":"c963e00c","timestamp":"2026-07-16T09:57:55.581Z","message":{"role":"toolResult","toolCallId":"call_jy9x1Nit5ifcDqUXmSuoysVR|fc_047bb49263dfa303016a58ab2322c08198b96e101f531b521a","toolName":"grep","content":[{"type":"text","text":" 9|//! table for **peer-initiated** streams, classifies each by its first NDJSON\n*10|//! record, and hands it to the right serve loop on its own worker thread.\n 11|//!\n 12|//! ## Direction (the gotcha-#9 fact, now first-class)\n 13|//!\n...\n 43|//!\n*44|//! ## Worker model\n 45|//!\n*46|//! Each claimed stream gets its own worker thread with its **own** brain IPC\n*47|//! connection (the broker is multi-client by design; the probe-brain pattern\n*48|//! every E2E test uses). The peek and the serve use **separate** connections:\n*49|//! the peek brain subscribes from 0 to read the first line and is dropped;\n*50|//! the serve loop then subscribes from 0 on a fresh connection so it owns its\n 51|//! stream's event feed alone (no double-subscription interleave). Origin is\n 52|//! the table's `remote_id_hex` — handshake-proven, never payload\n 53|//! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.\n...\n 74|\n*75|use crate::attach::serve_attach;\n 76|use crate::brain::{Brain, BrokerEvent};\n 77|use crate::notifsync::{apply_notif_feed, NotifPolicy};\n 78|use crate::propagate::serve_update;\n...\n 85|/// Poll cadence for the stream-table watch (the dispatcher's only busy-wait;\n*86|/// workers block on their stream's event feed).\n 87|pub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n 88|\n 89|/// What protocol a peer-initiated stream speaks, by its first record's shape.\n...\n 157|\n*158|/// Why a dispatched stream's worker ended — loud + typed, never silent.\n 159|#[derive(Debug)]\n 160|pub enum DispatchOutcome {\n 161|    /// The family's serve loop ran to its own outcome (rendered for the log).\n...\n 164|    Unknown,\n*165|    /// The worker failed (IO against broker or store).\n 166|    Failed(String),\n 167|}\n 168|\n 169|/// The SPT_HOME-anchored paths the serve arms need. One value, cloned per\n*170|/// worker (policies/stores re-load from the canonical paths per request —\n 171|/// the access-gate stance, config edits take effect without a restart).\n 172|#[derive(Debug, Clone)]\n 173|pub struct DispatchPaths {\n...\n 194|/// Watch the broker's stream table and dispatch every **peer-initiated**\n*195|/// stream to a family worker, until `stop` is set. Blocks the calling thread\n*196|/// (the daemon spawns it on its own); workers detach (each ends with its\n 197|/// stream).\n*198|pub fn run_dispatch_loop(\n 199|    broker_name: &str,\n 200|    paths: &DispatchPaths,\n 201|    registry: Arc<RegistryHost>,\n...\n 208|    while !stop.load(Ordering::Relaxed) {\n*209|        let reply = brain.net_streams()?;\n 210|        for s in reply.streams {\n 211|            // Claim only peer-initiated rows with bytes (a QUIC stream is\n 212|            // invisible until its first write anyway — gotcha #6 — but the\n 213|            // next_seq guard also skips a row whose pump hasn't appended yet).\n*214|            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {\n 215|                continue;\n 216|            }\n 217|            let name = broker_name.to_string();\n 218|            let origin = s.remote_id_hex.clone();\n*219|            let stream_id = s.stream_id;\n 220|            let p = paths.clone();\n 221|            let reg = Arc::clone(&registry);\n 222|            thread::spawn(move || {\n*223|                let outcome = worker(&name, stream_id, &origin, &p, &reg);\n 224|                match outcome {\n 225|                    DispatchOutcome::Served(_) => {}\n*226|                    other => eprintln!(\"DISPATCH:{stream_id}:{other:?}\"),\n 227|                }\n 228|            });\n 229|        }\n...\n 249|    std::time::SystemTime::now()\n*250|        .duration_since(std::time::UNIX_EPOCH)\n 251|        .map(|d| d.as_millis() as u64)\n 252|        .unwrap_or(0)\n 253|}\n 254|\n*255|/// Peek the stream's first NDJSON line on a throwaway brain connection.\n*256|fn peek_first_line(broker_name: &str, stream_id: u64) -> io::Result<Option<serde_json::Value>> {\n 257|    let mut brain = connect(broker_name)?;\n*258|    brain.net_stream_subscribe(stream_id, 0)?;\n 259|    let mut buf: Vec<u8> = Vec::new();\n 260|    loop {\n 261|        match brain.read_event()? {\n 262|            BrokerEvent::NetStreamData {\n*263|                stream_id: sid,\n 264|                bytes,\n 265|                ..\n*266|            } if sid == stream_id => {\n 267|                buf.extend_from_slice(&bytes);\n 268|                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n 269|                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n...\n 271|            }\n*272|            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n 273|                // Finished without a complete line: classify what we have\n 274|                // (a line without a trailing newline before EOF still counts).\n 275|                return Ok(serde_json::from_slice(&buf).ok());\n...\n 282|\n*283|/// One claimed stream's worker: peek → classify → run the family's serve\n*284|/// loop on a fresh brain connection.\n*285|fn worker(\n 286|    broker_name: &str,\n*287|    stream_id: u64,\n 288|    origin: &str,\n 289|    paths: &DispatchPaths,\n 290|    registry: &RegistryHost,\n 291|) -> DispatchOutcome {\n*292|    let line = match peek_first_line(broker_name, stream_id) {\n 293|        Ok(Some(v)) => v,\n 294|        Ok(None) => return DispatchOutcome::Unknown,\n 295|        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n...\n 312|                &mut brain,\n*313|                stream_id,\n 314|                origin,\n 315|                0,\n 316|                &cs,\n...\n 326|            let roster = RosterStore::load();\n*327|            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\n 328|                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n 329|                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 330|            }\n 331|        }\n*332|        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n*333|        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n*334|        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n 335|        StreamFamily::Attach => {\n 336|            // The gate subject is the session's OWNING endpoint, resolved\n 337|            // from the broker's table (the spawn's label) — never from wire\n...\n 351|            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\n*352|            // before serve_attach subscribes — never mid-serve, which would swallow\n 353|            // interleaved attach records) and `None` on the local path.\n 354|            let (endpoint, remote_session) = match line\n 355|                .get(\"endpoint_id\")\n...\n 377|            match endpoint {\n*378|                Some(endpoint) => match serve_attach(\n 379|                    &mut brain,\n 380|                    broker_name,\n*381|                    stream_id,\n 382|                    &endpoint,\n 383|                    origin,\n 384|                    0,\n...\n 391|                    // Refuse politely: finish our send side, claim stays.\n*392|                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n 393|                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n 394|                }\n 395|            }\n...\n 409|                    match serve_xfer(\n*410|                        &mut brain, stream_id, &endpoint, origin, 0, &root, &progress,\n 411|                    ) {\n 412|                        Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n 413|                        Err(e) => DispatchOutcome::Failed(e.to_string()),\n...\n 416|                None => {\n*417|                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n 418|                    DispatchOutcome::Served(\"xfer refused: no endpoint named\".into())\n 419|                }\n 420|            }\n...\n 428|            let knob = crate::config::DaemonConfig::load().auto_suspend_after_ms;\n*429|            match crate::resthost::serve_rest(&mut brain, stream_id, origin, knob) {\n 430|                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n 431|                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 432|            }\n...\n 439|            // [impl->REQ-SHELL-2]\n*440|            match crate::linkhost::serve_shell_link(&mut brain, stream_id, origin, &paths.owlery) {\n 441|                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n 442|                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 443|            }\n...\n 449|            // [impl->REQ-SUBNET-5]\n*450|            match crate::serveprobe::serve_subnet_probe(&mut brain, stream_id) {\n 451|                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n 452|                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 453|            }\n...\n 463|/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer\n*464|/// instance of an id observed transitioning to `Active` — its node's daemon\n 465|/// just gave it a driver, so a *locally hosted* instance of the same id\n 466|/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`\n 467|/// dormant). The rest edge's transition echo is gate-armed\n 468|/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse\n*469|/// loop runs the bounded summarizer call, never this worker (the KH 7.4\n 470|/// non-blocking posture: N flips in one feed are N gate files, not N LLM\n 471|/// calls serialized here).\n 472|// [impl->REQ-INST-7]\n...\n 474|    brain: &mut Brain,\n*475|    stream_id: u64,\n 476|    origin: &str,\n 477|    registry: &RegistryHost,\n 478|) -> DispatchOutcome {\n 479|    let policy = RegistryGatePolicy::load();\n*480|    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n 481|        return DispatchOutcome::Failed(e.to_string());\n 482|    }\n 483|    let mut decoder = RegistryDecoder::new();\n...\n 487|            Ok(BrokerEvent::NetStreamData {\n*488|                stream_id: sid,\n 489|                bytes,\n 490|                ..\n*491|            }) if sid == stream_id => {\n 492|                let records = decoder.push(&bytes);\n 493|                if records.is_empty() {\n 494|                    continue;\n...\n 529|            }\n*530|            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n 531|                return DispatchOutcome::Served(format!(\"registry feed: {merged} record(s)\"));\n 532|            }\n 533|            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n...\n 551|// [impl->REQ-PRES-1]\n*552|fn serve_notif_feed(brain: &mut Brain, stream_id: u64, origin: &str) -> DispatchOutcome {\n 553|    let store = match NotifStore::open() {\n 554|        Ok(s) => s,\n 555|        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n...\n 564|        .unwrap_or_default();\n*565|    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n 566|        return DispatchOutcome::Failed(e.to_string());\n 567|    }\n 568|    let mut decoder = NotifDecoder::new();\n...\n 572|            Ok(BrokerEvent::NetStreamData {\n*573|                stream_id: sid,\n 574|                bytes,\n 575|                ..\n*576|            }) if sid == stream_id => {\n 577|                let records = decoder.push(&bytes);\n 578|                if records.is_empty() {\n 579|                    continue;\n...\n 588|            }\n*589|            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n 590|                return DispatchOutcome::Served(format!(\"notif feed: {applied} record(s)\"));\n 591|            }\n 592|            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n...\n 632|    brain: &mut Brain,\n*633|    stream_id: u64,\n 634|    origin: &str,\n 635|    paths: &DispatchPaths,\n 636|    registry: &RegistryHost,\n 637|) -> DispatchOutcome {\n*638|    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n 639|        return DispatchOutcome::Failed(e.to_string());\n 640|    }\n 641|    let mut decoder = WanDecoder::new();\n...\n 646|            Ok(BrokerEvent::NetStreamData {\n*647|                stream_id: sid,\n 648|                bytes,\n 649|                ..\n*650|            }) if sid == stream_id => {\n 651|                for msg in decoder.push(&bytes) {\n 652|                    let outcome = receive_wan(&msg, origin, &paths.owlery, registry);\n 653|                    handled += 1;\n...\n 664|                        let _ = brain.net_stream_send(\n*665|                            stream_id,\n 666|                            &reply.encode_line(),\n 667|                            None,\n 668|                            true,\n...\n 673|            }\n*674|            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n 675|                return DispatchOutcome::Served(format!(\"wan feed: {handled} record(s)\"));\n 676|            }\n 677|            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n...\n 693|        if let Err(e) =\n*694|            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n 695|        {\n 696|            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\n 697|        }\n...\n 736|        let reg = json!({\"subnet\": \"home\", \"endpoint_id\": \"ling\",\n*737|            \"instance\": {\"node\": \"n1\", \"status\": \"Active\", \"epoch\": 4}});\n 738|        assert_eq!(classify_first_line(&reg), StreamFamily::Registry);\n 739|    }\n 740|"}],"details":{"scopePath":"crates/spt-daemon/src/dispatch.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":66,"fileCount":1,"files":["crates/spt-daemon/src/dispatch.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/dispatch.rs","count":66}],"truncated":false,"displayContent":"   9│//! table for **peer-initiated** streams, classifies each by its first NDJSON\n *10│//! record, and hands it to the right serve loop on its own worker thread.\n  11│//!\n  12│//! ## Direction (the gotcha-#9 fact, now first-class)\n  13│//!\n    │...\n  43│//!\n *44│//! ## Worker model\n  45│//!\n *46│//! Each claimed stream gets its own worker thread with its **own** brain IPC\n *47│//! connection (the broker is multi-client by design; the probe-brain pattern\n *48│//! every E2E test uses). The peek and the serve use **separate** connections:\n *49│//! the peek brain subscribes from 0 to read the first line and is dropped;\n *50│//! the serve loop then subscribes from 0 on a fresh connection so it owns its\n  51│//! stream's event feed alone (no double-subscription interleave). Origin is\n  52│//! the table's `remote_id_hex` — handshake-proven, never payload\n  53│//! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.\n    │...\n  74│\n *75│use crate::attach::serve_attach;\n  76│use crate::brain::{Brain, BrokerEvent};\n  77│use crate::notifsync::{apply_notif_feed, NotifPolicy};\n  78│use crate::propagate::serve_update;\n    │...\n  85│/// Poll cadence for the stream-table watch (the dispatcher's only busy-wait;\n *86│/// workers block on their stream's event feed).\n  87│pub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n  88│\n  89│/// What protocol a peer-initiated stream speaks, by its first record's shape.\n    │...\n 157│\n*158│/// Why a dispatched stream's worker ended — loud + typed, never silent.\n 159│#[derive(Debug)]\n 160│pub enum DispatchOutcome {\n 161│    /// The family's serve loop ran to its own outcome (rendered for the log).\n    │...\n 164│    Unknown,\n*165│    /// The worker failed (IO against broker or store).\n 166│    Failed(String),\n 167│}\n 168│\n 169│/// The SPT_HOME-anchored paths the serve arms need. One value, cloned per\n*170│/// worker (policies/stores re-load from the canonical paths per request —\n 171│/// the access-gate stance, config edits take effect without a restart).\n 172│#[derive(Debug, Clone)]\n 173│pub struct DispatchPaths {\n    │...\n 194│/// Watch the broker's stream table and dispatch every **peer-initiated**\n*195│/// stream to a family worker, until `stop` is set. Blocks the calling thread\n*196│/// (the daemon spawns it on its own); workers detach (each ends with its\n 197│/// stream).\n*198│pub fn run_dispatch_loop(\n 199│    broker_name: &str,\n 200│    paths: &DispatchPaths,\n 201│    registry: Arc<RegistryHost>,\n    │...\n 208│    while !stop.load(Ordering::Relaxed) {\n*209│        let reply = brain.net_streams()?;\n 210│        for s in reply.streams {\n 211│            // Claim only peer-initiated rows with bytes (a QUIC stream is\n 212│            // invisible until its first write anyway — gotcha #6 — but the\n 213│            // next_seq guard also skips a row whose pump hasn't appended yet).\n*214│            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {\n 215│                continue;\n 216│            }\n 217│            let name = broker_name.to_string();\n 218│            let origin = s.remote_id_hex.clone();\n*219│            let stream_id = s.stream_id;\n 220│            let p = paths.clone();\n 221│            let reg = Arc::clone(&registry);\n 222│            thread::spawn(move || {\n*223│                let outcome = worker(&name, stream_id, &origin, &p, &reg);\n 224│                match outcome {\n 225│                    DispatchOutcome::Served(_) => {}\n*226│                    other => eprintln!(\"DISPATCH:{stream_id}:{other:?}\"),\n 227│                }\n 228│            });\n 229│        }\n    │...\n 249│    std::time::SystemTime::now()\n*250│        .duration_since(std::time::UNIX_EPOCH)\n 251│        .map(|d| d.as_millis() as u64)\n 252│        .unwrap_or(0)\n 253│}\n 254│\n*255│/// Peek the stream's first NDJSON line on a throwaway brain connection.\n*256│fn peek_first_line(broker_name: &str, stream_id: u64) -> io::Result<Option<serde_json::Value>> {\n 257│    let mut brain = connect(broker_name)?;\n*258│    brain.net_stream_subscribe(stream_id, 0)?;\n 259│    let mut buf: Vec<u8> = Vec::new();\n 260│    loop {\n 261│        match brain.read_event()? {\n 262│            BrokerEvent::NetStreamData {\n*263│                stream_id: sid,\n 264│                bytes,\n 265│                ..\n*266│            } if sid == stream_id => {\n 267│                buf.extend_from_slice(&bytes);\n 268│                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n 269│                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n    │...\n 271│            }\n*272│            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n 273│                // Finished without a complete line: classify what we have\n 274│                // (a line without a trailing newline before EOF still counts).\n 275│                return Ok(serde_json::from_slice(&buf).ok());\n    │...\n 282│\n*283│/// One claimed stream's worker: peek → classify → run the family's serve\n*284│/// loop on a fresh brain connection.\n*285│fn worker(\n 286│    broker_name: &str,\n*287│    stream_id: u64,\n 288│    origin: &str,\n 289│    paths: &DispatchPaths,\n 290│    registry: &RegistryHost,\n 291│) -> DispatchOutcome {\n*292│    let line = match peek_first_line(broker_name, stream_id) {\n 293│        Ok(Some(v)) => v,\n 294│        Ok(None) => return DispatchOutcome::Unknown,\n 295│        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    │...\n 312│                &mut brain,\n*313│                stream_id,\n 314│                origin,\n 315│                0,\n 316│                &cs,\n    │...\n 326│            let roster = RosterStore::load();\n*327│            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\n 328│                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n 329│                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 330│            }\n 331│        }\n*332│        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n*333│        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n*334│        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n 335│        StreamFamily::Attach => {\n 336│            // The gate subject is the session's OWNING endpoint, resolved\n 337│            // from the broker's table (the spawn's label) — never from wire\n    │...\n 351│            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\n*352│            // before serve_attach subscribes — never mid-serve, which would swallow\n 353│            // interleaved attach records) and `None` on the local path.\n 354│            let (endpoint, remote_session) = match line\n 355│                .get(\"endpoint_id\")\n    │...\n 377│            match endpoint {\n*378│                Some(endpoint) => match serve_attach(\n 379│                    &mut brain,\n 380│                    broker_name,\n*381│                    stream_id,\n 382│                    &endpoint,\n 383│                    origin,\n 384│                    0,\n    │...\n 391│                    // Refuse politely: finish our send side, claim stays.\n*392│                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n 393│                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n 394│                }\n 395│            }\n    │...\n 409│                    match serve_xfer(\n*410│                        &mut brain, stream_id, &endpoint, origin, 0, &root, &progress,\n 411│                    ) {\n 412│                        Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n 413│                        Err(e) => DispatchOutcome::Failed(e.to_string()),\n    │...\n 416│                None => {\n*417│                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n 418│                    DispatchOutcome::Served(\"xfer refused: no endpoint named\".into())\n 419│                }\n 420│            }\n    │...\n 428│            let knob = crate::config::DaemonConfig::load().auto_suspend_after_ms;\n*429│            match crate::resthost::serve_rest(&mut brain, stream_id, origin, knob) {\n 430│                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n 431│                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 432│            }\n    │...\n 439│            // [impl->REQ-SHELL-2]\n*440│            match crate::linkhost::serve_shell_link(&mut brain, stream_id, origin, &paths.owlery) {\n 441│                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n 442│                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 443│            }\n    │...\n 449│            // [impl->REQ-SUBNET-5]\n*450│            match crate::serveprobe::serve_subnet_probe(&mut brain, stream_id) {\n 451│                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n 452│                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 453│            }\n    │...\n 463│/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer\n*464│/// instance of an id observed transitioning to `Active` — its node's daemon\n 465│/// just gave it a driver, so a *locally hosted* instance of the same id\n 466│/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`\n 467│/// dormant). The rest edge's transition echo is gate-armed\n 468│/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse\n*469│/// loop runs the bounded summarizer call, never this worker (the KH 7.4\n 470│/// non-blocking posture: N flips in one feed are N gate files, not N LLM\n 471│/// calls serialized here).\n 472│// [impl->REQ-INST-7]\n    │...\n 474│    brain: &mut Brain,\n*475│    stream_id: u64,\n 476│    origin: &str,\n 477│    registry: &RegistryHost,\n 478│) -> DispatchOutcome {\n 479│    let policy = RegistryGatePolicy::load();\n*480│    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n 481│        return DispatchOutcome::Failed(e.to_string());\n 482│    }\n 483│    let mut decoder = RegistryDecoder::new();\n    │...\n 487│            Ok(BrokerEvent::NetStreamData {\n*488│                stream_id: sid,\n 489│                bytes,\n 490│                ..\n*491│            }) if sid == stream_id => {\n 492│                let records = decoder.push(&bytes);\n 493│                if records.is_empty() {\n 494│                    continue;\n    │...\n 529│            }\n*530│            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n 531│                return DispatchOutcome::Served(format!(\"registry feed: {merged} record(s)\"));\n 532│            }\n 533│            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n    │...\n 551│// [impl->REQ-PRES-1]\n*552│fn serve_notif_feed(brain: &mut Brain, stream_id: u64, origin: &str) -> DispatchOutcome {\n 553│    let store = match NotifStore::open() {\n 554│        Ok(s) => s,\n 555│        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    │...\n 564│        .unwrap_or_default();\n*565│    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n 566│        return DispatchOutcome::Failed(e.to_string());\n 567│    }\n 568│    let mut decoder = NotifDecoder::new();\n    │...\n 572│            Ok(BrokerEvent::NetStreamData {\n*573│                stream_id: sid,\n 574│                bytes,\n 575│                ..\n*576│            }) if sid == stream_id => {\n 577│                let records = decoder.push(&bytes);\n 578│                if records.is_empty() {\n 579│                    continue;\n    │...\n 588│            }\n*589│            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n 590│                return DispatchOutcome::Served(format!(\"notif feed: {applied} record(s)\"));\n 591│            }\n 592│            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n    │...\n 632│    brain: &mut Brain,\n*633│    stream_id: u64,\n 634│    origin: &str,\n 635│    paths: &DispatchPaths,\n 636│    registry: &RegistryHost,\n 637│) -> DispatchOutcome {\n*638│    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n 639│        return DispatchOutcome::Failed(e.to_string());\n 640│    }\n 641│    let mut decoder = WanDecoder::new();\n    │...\n 646│            Ok(BrokerEvent::NetStreamData {\n*647│                stream_id: sid,\n 648│                bytes,\n 649│                ..\n*650│            }) if sid == stream_id => {\n 651│                for msg in decoder.push(&bytes) {\n 652│                    let outcome = receive_wan(&msg, origin, &paths.owlery, registry);\n 653│                    handled += 1;\n    │...\n 664│                        let _ = brain.net_stream_send(\n*665│                            stream_id,\n 666│                            &reply.encode_line(),\n 667│                            None,\n 668│                            true,\n    │...\n 673│            }\n*674│            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n 675│                return DispatchOutcome::Served(format!(\"wan feed: {handled} record(s)\"));\n 676│            }\n 677│            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n    │...\n 693│        if let Err(e) =\n*694│            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n 695│        {\n 696│            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\n 697│        }\n    │...\n 736│        let reg = json!({\"subnet\": \"home\", \"endpoint_id\": \"ling\",\n*737│            \"instance\": {\"node\": \"n1\", \"status\": \"Active\", \"epoch\": 4}});\n 738│        assert_eq!(classify_first_line(&reg), StreamFamily::Registry);\n 739│    }\n 740│"},"isError":false,"timestamp":1784195875581}}
{"type":"message","id":"64b1450c","parentId":"e341c873","timestamp":"2026-07-16T09:57:55.590Z","message":{"role":"toolResult","toolCallId":"call_qY5aJWy58ZC58ZjD6j0yokqO|fc_047bb49263dfa303016a58ab2322d88198b319d5c22684a6d3","toolName":"grep","content":[{"type":"text","text":" 37|use spt_net::net::endpoint::{\n*38|    BindScope, Connection, EndpointAddr, LocalDiscovery, NetEndpoint, RecvStream, RelayPolicy,\n 39|    SendStream, SPT_NET_ALPN,\n 40|};\n 41|use spt_net::net::mesh::seedproof::ProofRole;\n...\n 48|use crate::msg::{\n*49|    net_presence_event_envelope, net_stream_data_envelope, net_stream_eof_envelope,\n 50|    NetPresenceEvent, NetStreamInfo, PRESENCE_CONNECTED, PRESENCE_DIAL_FAILED, PRESENCE_DISCONNECTED,\n 51|};\n 52|\n...\n 58|/// Broker-side QUIC-op deadline (REQ-HAZARD-BROKER-QUIC-DEADLINE), in ms. Each\n*59|/// brain-waiting QUIC await — `dial` / `open_stream` / `send_stream` — is bounded\n 60|/// by this so a black-holed or dead peer fails PROMPTLY with an ORDINARY error\n 61|/// the broker replies, never an unbounded await that wedges the single-threaded\n 62|/// pump (the B-half of REQ-HAZARD-PUMP-IPC-DEADLINE; the 2.2h hfenduleam wedge).\n...\n 108|/// the PTY `OutputLog`: large enough that a brain-restart window never evicts\n*109|/// unreplayed chunks in practice; bounded so an unsubscribed stream can't\n 110|/// buffer without limit.\n 111|const DEFAULT_STREAM_RING_CHUNKS: usize = 4096;\n 112|\n...\n 129|    /// A broker-minted in-process loopback (no iroh handle, no handshake). Its\n*130|    /// streams are minted eagerly in cross-wired pairs by [`NetHost::open_stream`];\n 131|    /// there is no accept loop and no closed-watcher (it lives for the host).\n 132|    Loopback,\n 133|}\n...\n 141|    remote_id_hex: String,\n*142|    /// The subnets the peer proved current-epoch seed-knowledge of at connect\n 143|    /// (Mesh-D2, REQ-MESH-1). Empty when seed-proof is disabled (`membership:\n 144|    /// None`). The D5 gate swap reads this in place of `is_trusted(subnet, …)`;\n 145|    /// at D2 it is cached but not yet consumed (the `is_trusted` gates still run\n...\n 151|/// discipline applied to the network (D4b): the stream's single read pump is\n*152|/// the only producer; a brain connection attaches as the subscriber; every\n*153|/// subscriber send happens under the enclosing `Mutex`, so live chunks and a\n 154|/// replay can never interleave out of `seq` order. Chunks produced while no\n*155|/// brain is attached sit in the ring and replay on the next subscribe — a\n 156|/// brain restart loses nothing (Spike #3 invariant B/D).\n 157|// [impl->REQ-DAEMON-2]\n*158|struct StreamLog {\n*159|    stream_id: u64,\n 160|    /// TRANSPORT read counter: bumped once per [`append`](Self::append) (one\n 161|    /// read-pump `read_chunk`), INDEPENDENT of byte size. NOT a logical-chunk\n 162|    /// count — QUIC may coalesce several sender writes into one receiver\n...\n 164|    /// chunks the sender issued for the SAME bytes. Correctness (byte-exact +\n*165|    /// exactly-once) holds via these transport seqs + the recv cursor regardless;\n 166|    /// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows\n 167|    /// coalescing flake) and must gate on BYTES instead.\n 168|    next_seq: u64,\n...\n 170|    cap_chunks: usize,\n*171|    subscriber: Option<SharedSend>,\n 172|    /// Read side finished cleanly (peer called finish / conn ended).\n 173|    finished: bool,\n*174|    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n*175|    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n 176|    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n*177|    /// congestion surfaces as lag, never loss.\" So a retentive log never cap-evicts\n 178|    /// on append (no loss), and the read pump REFUSES to pull the next chunk once\n 179|    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the\n 180|    /// transport's native flow-control then lags the *sender*. A chunk leaves the\n*181|    /// ring ONLY when [`drain`](Self::drain) (the recv cursor) pops it; the pump is\n 182|    /// woken to resume via the entry's `room` notifier. Ordinary streams keep\n*183|    /// `retentive_cap = 0` (a slow/absent brain must not OOM the broker via an\n*184|    /// unbounded ring) and are drained by subscribe, never the sync cursor.\n*185|    retentive_cap: usize,\n 186|}\n 187|\n 188|impl StreamLog {\n*189|    fn new(stream_id: u64, cap_chunks: usize) -> Self {\n 190|        StreamLog {\n*191|            stream_id,\n 192|            next_seq: 0,\n 193|            ring: VecDeque::new(),\n 194|            cap_chunks,\n*195|            subscriber: None,\n 196|            finished: false,\n*197|            retentive_cap: 0,\n 198|        }\n 199|    }\n 200|\n*201|    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\n*202|    fn set_retentive(&mut self, cap: usize) {\n*203|        self.retentive_cap = cap;\n 204|    }\n 205|\n 206|    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\n*207|    /// live-send to the current subscriber. Read-pump only. A non-retentive log\n*208|    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\n 209|    /// never evicts — bytes leave only via [`drain`](Self::drain).\n 210|    fn append(&mut self, bytes: &[u8]) {\n 211|        let seq = self.next_seq;\n...\n 213|        self.ring.push_back((seq, bytes.to_vec()));\n*214|        if self.retentive_cap == 0 {\n 215|            while self.ring.len() > self.cap_chunks {\n 216|                self.ring.pop_front();\n 217|            }\n 218|        }\n*219|        if let Some(sub) = &self.subscriber {\n*220|            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\n 221|            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n*222|            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n 223|            // the deadline instead of parking this producer.\n 224|            let _ = sub.write(&frame);\n 225|        }\n...\n 227|\n*228|    /// Drain every undrained chunk in order (the shell-tunnel recv cursor, W3).\n 229|    /// Pops the whole ring front-to-back (FIFO by `seq`) and concatenates the\n 230|    /// bytes — chunk boundaries are arbitrary read-pump splits, irrelevant to an\n 231|    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,\n 232|    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so\n*233|    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing\n 234|    /// pending yet.\n 235|    fn drain(&mut self) -> (Vec<u8>, bool) {\n 236|        let mut out = Vec::new();\n...\n 242|\n*243|    /// Mark the read side finished and tell the subscriber (with the final\n 244|    /// cursor, so it can verify completeness).\n 245|    fn finish(&mut self) {\n 246|        self.finished = true;\n*247|        if let Some(sub) = &self.subscriber {\n*248|            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n 249|            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n*250|            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n 251|            // the deadline instead of parking this producer.\n 252|            let _ = sub.write(&frame);\n 253|        }\n...\n 255|\n*256|    /// Attach `sub` as the subscriber, replaying every retained chunk with\n*257|    /// `seq >= from_seq` (then eof, if already finished) before live streaming.\n 258|    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n*259|        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\n 260|        // record — this is one of the two RCA-pinned shared-carrier classes.\n 261|        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n*262|        sub.describe(&format!(\"stream-subscriber stream={}\", self.stream_id));\n 263|        sub.lifecycle_event(\n 264|            \"stream-sub-attach\",\n*265|            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\n 266|        );\n*267|        self.subscriber = Some(Arc::clone(&sub));\n 268|        for (seq, bytes) in self.ring.iter().filter(|(s, _)| *s >= from_seq) {\n*269|            let frame = net_stream_data_envelope(self.stream_id, *seq, bytes);\n 270|            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n*271|            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n 272|            // the deadline instead of parking this producer.\n 273|            let _ = sub.write(&frame);\n 274|        }\n 275|        if self.finished {\n*276|            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n 277|            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n*278|            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n 279|            // the deadline instead of parking this producer.\n 280|            let _ = sub.write(&frame);\n 281|        }\n...\n 283|\n*284|    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n*285|    /// not unsubscribe a newer brain).\n 286|    fn detach_if(&mut self, sub: &SharedSend) {\n*287|        if let Some(cur) = &self.subscriber {\n 288|            if Arc::ptr_eq(cur, sub) {\n*289|                self.subscriber = None;\n 290|            }\n 291|        }\n 292|    }\n...\n 298|/// connection's closed-watcher appends `disconnected` after removing the row.\n*299|/// A brain subscribes with a cursor and replays events produced while it was\n 300|/// down, so presence survives a brain restart exactly like stream reads do.\n 301|// [impl->REQ-EP-4]\n 302|struct PresenceLog {\n...\n 305|    cap_events: usize,\n*306|    subscriber: Option<SharedSend>,\n 307|}\n 308|\n 309|impl PresenceLog {\n...\n 314|            cap_events,\n*315|            subscriber: None,\n 316|        }\n 317|    }\n 318|\n 319|    /// Append a liveness event: assign the next `seq`, retain it in the bounded\n*320|    /// ring, live-send to the current subscriber. Producers: `register_conn`\n 321|    /// (connected) and the per-conn closed-watchers (disconnected) — every\n*322|    /// append serializes on the enclosing `Mutex`, so subscriber frames arrive\n 323|    /// in strict `seq` order.\n 324|    fn append(&mut self, kind: &str, conn_id: u64, remote_id_hex: &str) {\n 325|        self.push(NetPresenceEvent {\n...\n 366|    /// Assign the next `seq`, retain in the bounded ring, live-send to the\n*367|    /// current subscriber. Every append serializes on the enclosing `Mutex`, so\n*368|    /// subscriber frames arrive in strict `seq` order.\n 369|    fn push(&mut self, mut ev: NetPresenceEvent) {\n 370|        ev.seq = self.next_seq;\n 371|        self.next_seq += 1;\n...\n 375|        }\n*376|        if let Some(sub) = &self.subscriber {\n 377|            let frame = net_presence_event_envelope(&ev);\n 378|            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n*379|            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n 380|            // the deadline instead of parking this producer.\n 381|            let _ = sub.write(&frame);\n 382|        }\n...\n 384|\n*385|    /// Attach `sub` as the subscriber, replaying every retained event with\n 386|    /// `seq >= from_seq` before live streaming (the gapless-resume attach).\n 387|    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n*388|        // Attribution (F-039 legs b+d): presence-subscriber role fact + attach\n 389|        // record — the OTHER RCA-pinned shared carrier (the per-dial-failure\n 390|        // presence push rides this conn). [impl->REQ-CONN-POISON-ATTRIBUTION]\n*391|        sub.describe(\"presence-subscriber\");\n 392|        sub.lifecycle_event(\"presence-sub-attach\", &format!(\"from_seq={from_seq}\"));\n*393|        self.subscriber = Some(Arc::clone(&sub));\n 394|        for ev in self.ring.iter().filter(|e| e.seq >= from_seq) {\n 395|            let frame = net_presence_event_envelope(ev);\n 396|            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n*397|            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n 398|            // the deadline instead of parking this producer.\n 399|            let _ = sub.write(&frame);\n 400|        }\n...\n 402|\n*403|    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n*404|    /// not unsubscribe a newer brain).\n 405|    fn detach_if(&mut self, sub: &SharedSend) {\n*406|        if let Some(cur) = &self.subscriber {\n 407|            if Arc::ptr_eq(cur, sub) {\n*408|                self.subscriber = None;\n 409|            }\n 410|        }\n 411|    }\n...\n 448|/// QUIC reads via quinn's chunk API; loopback via the read end of the duplex.\n*449|/// Both append to the same [`StreamLog`], so the subscriber (and everything\n 450|/// above it) cannot tell the transports apart.\n*451|enum RecvHalf {\n*452|    Quic(RecvStream),\n 453|    Loopback(ReadHalf<DuplexStream>),\n 454|}\n 455|\n...\n 457|/// writes ride it over IPC) + the sequenced read log.\n*458|struct StreamEntry {\n 459|    conn_id: u64,\n 460|    remote_id_hex: String,\n*461|    /// Whether THIS node opened the stream (`open_stream`) vs the peer\n 462|    /// (accept loop). The inbound dispatcher (D9-1) claims only\n 463|    /// peer-initiated streams; without this fact the table is ambiguous —\n 464|    /// it holds locally-opened outbound streams too (gotcha: a dispatcher\n 465|    /// that served its own requester stream would deadlock on itself).\n*466|    initiated_locally: bool,\n 467|    /// The send half, serialized behind an async mutex (writes happen on the\n 468|    /// host's runtime via `block_on`).\n 469|    send: tokio::sync::Mutex<SendHalf>,\n 470|    log: Arc<Mutex<StreamLog>>,\n*471|    /// Wakes the read pump when a retentive (tunnel) stream's ring drains below\n*472|    /// its backpressure cap (M11-W3, REQ-SHELL-4) — see [`StreamLog::retentive_cap`].\n*473|    /// Unused by ordinary (non-retentive) streams.\n 474|    room: Arc<tokio::sync::Notify>,\n 475|}\n 476|\n...\n 481|    conns: Mutex<HashMap<u64, ConnEntry>>,\n*482|    next_stream_id: AtomicU64,\n 483|    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,\n 484|    ring_cap: usize,\n 485|    /// The conn-liveness event log (D4c) — one per host, all conns interleaved.\n...\n 493|            conns: Mutex::new(HashMap::new()),\n*494|            next_stream_id: AtomicU64::new(1),\n 495|            streams: Mutex::new(HashMap::new()),\n 496|            ring_cap: DEFAULT_STREAM_RING_CHUNKS,\n 497|            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),\n...\n 604|        conn_close.closed().await;\n*605|        sh_close.conns.lock().unwrap().remove(&id);\n 606|        sh_close\n 607|            .presence\n 608|            .lock()\n...\n 618|    tokio::spawn(async move {\n*619|        while let Ok((send, recv)) = conn.accept_bi().await {\n 620|            register_stream(\n 621|                &sh,\n 622|                id,\n...\n 624|                SendHalf::Quic(send),\n*625|                RecvHalf::Quic(recv),\n 626|                false,\n 627|            );\n 628|        }\n...\n 635|/// its read pump. Must run inside the host's runtime context.\n*636|/// `initiated_locally` records the direction fact for the table (true only\n*637|/// from [`NetHost::open_stream`]).\n 638|fn register_stream(\n 639|    shared: &Arc<NetShared>,\n 640|    conn_id: u64,\n...\n 642|    send: SendHalf,\n*643|    recv: RecvHalf,\n*644|    initiated_locally: bool,\n 645|) -> u64 {\n*646|    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);\n 647|    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));\n 648|    let room = Arc::new(tokio::sync::Notify::new());\n 649|    shared.streams.lock().unwrap().insert(\n...\n 653|            remote_id_hex: remote_id_hex.to_string(),\n*654|            initiated_locally,\n 655|            send: tokio::sync::Mutex::new(send),\n 656|            log: Arc::clone(&log),\n 657|            room: Arc::clone(&room),\n...\n 666|    //\n*667|    // Backpressure (M11-W3, REQ-SHELL-4): a retentive (tunnel) log never evicts,\n 668|    // so before pulling the next chunk the pump WAITS while the undrained ring is\n*669|    // at its `retentive_cap`. The unread bytes stay in the transport's buffer, so\n 670|    // QUIC stream flow-control (or the loopback duplex's bounded buffer) lags the\n 671|    // SENDER — \"lag, never loss\" (CONTEXT:262). A `drain` wakes `room`. Ordinary\n*672|    // streams have `retentive_cap == 0`, so this gate is a no-op for them.\n 673|    tokio::spawn(async move {\n*674|        match recv {\n*675|            RecvHalf::Quic(mut recv) => loop {\n 676|                await_ring_room(&log, &room).await;\n*677|                match recv.read_chunk(STREAM_READ_CHUNK).await {\n 678|                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),\n 679|                    Ok(None) | Err(_) => {\n 680|                        log.lock().unwrap().finish();\n...\n 684|            },\n*685|            RecvHalf::Loopback(mut recv) => {\n 686|                let mut buf = vec![0u8; STREAM_READ_CHUNK];\n 687|                loop {\n 688|                    await_ring_room(&log, &room).await;\n*689|                    match recv.read(&mut buf).await {\n 690|                        Ok(0) | Err(_) => {\n 691|                            log.lock().unwrap().finish();\n 692|                            break;\n...\n 703|\n*704|/// Backpressure gate for a retentive (tunnel) read pump: park until the undrained\n*705|/// ring has room below `retentive_cap`, woken by a `drain` on `room`. A no-op for\n*706|/// an ordinary stream (`retentive_cap == 0`). Holds no lock across the await.\n 707|// [impl->REQ-SHELL-4]\n 708|async fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {\n 709|    loop {\n...\n 711|            let l = log.lock().unwrap();\n*712|            if l.retentive_cap == 0 || l.ring.len() < l.retentive_cap {\n 713|                return;\n 714|            }\n 715|        }\n...\n 732|    dial_ops: Mutex<HashMap<(Minter, u64), u64>>,\n*733|    /// `(minter, op_id) → stream_id` for exactly-once stream-open replies (D4b),\n 734|    /// same discipline as `dial_ops`. Minter-namespaced (ADR-0034), matching the\n 735|    /// journal key the broker builds for the same op.\n 736|    stream_ops: Mutex<HashMap<(Minter, u64), u64>>,\n...\n 776|    /// DIAGNOSTIC (UPDATE-WEDGE round-3, throwaway): reactor-liveness canary. A\n*777|    /// runtime task stamps epoch-ms here every ~25ms. If the 2 workers are\n 778|    /// MONOPOLIZED (a non-yielding dead-peer connect that never awaits back to\n 779|    /// the scheduler) the stamp goes STALE — the DIRECT worker-starvation\n 780|    /// discriminator the round-3 rig asserts on. A fresh stamp under a dead-peer\n...\n 785|\n*786|/// DIAGNOSTIC helper (round-3): epoch-ms wall clock for the canary stamp.\n*787|fn epoch_ms() -> u64 {\n 788|    std::time::SystemTime::now()\n*789|        .duration_since(std::time::UNIX_EPOCH)\n 790|        .map(|d| d.as_millis() as u64)\n 791|        .unwrap_or(0)\n 792|}\n...\n 918|        // DIAGNOSTIC (UPDATE-WEDGE round-3): the reactor-liveness canary. Stamps\n*919|        // epoch-ms every ~25ms FROM the 2-worker runtime, so a starved runtime\n 920|        // (workers monopolized by non-yielding dead-peer connects) shows as a\n 921|        // stale stamp. Throwaway probe — no REQ, gated by nothing (cheap store).\n*922|        let net_canary_tick_ms = Arc::new(AtomicU64::new(epoch_ms()));\n 923|        let canary = Arc::clone(&net_canary_tick_ms);\n 924|        runtime.spawn(async move {\n 925|            let mut ivl = tokio::time::interval(Duration::from_millis(25));\n...\n 927|                ivl.tick().await;\n*928|                canary.store(epoch_ms(), Ordering::Relaxed);\n 929|            }\n 930|        });\n 931|\n...\n 985|    /// frame, so the brain reconstructs an `ErrorKind::Other` (`Brain::net_dial` /\n*986|    /// `net_open_stream` / `net_stream_send` → `io::Error::other`) and\n 987|    /// `pump::peer_outcome` takes the ordinary per-peer arm (drop conn + redial,\n 988|    /// round continues, heartbeat advances) — DISTINCT from the brain's OWN\n 989|    /// read-deadline `TimedOut` (the A-half poison → supervised-restart signal).\n...\n 1075|    pub fn net_canary_age_ms(&self) -> u64 {\n*1076|        epoch_ms().saturating_sub(self.net_canary_tick_ms.load(Ordering::Relaxed))\n 1077|    }\n 1078|\n 1079|    /// DIAGNOSTIC (round-3): free permits on the shared dial-concurrency semaphore\n...\n 1196|\n*1197|    /// The subnets the peer of `conn_id` proved current-epoch seed-knowledge of\n 1198|    /// at connect (Mesh-D2, REQ-MESH-1). Empty when seed-proof was disabled or\n 1199|    /// the conn is gone. The seam the D5 gate swap reads in place of\n 1200|    /// `is_trusted(subnet, origin)`; unused by any gate at D2.\n...\n 1267|    /// so the open mints a CROSS-WIRED PAIR in-process: an operator row\n*1268|    /// (`initiated_locally = true`, the returned id) and a peer row\n*1269|    /// (`initiated_locally = false`) the inbound dispatcher claims and serves via\n 1270|    /// `serve_attach` — exactly as an `accept_bi` would surface a remote-opened\n 1271|    /// stream. One `tokio::io::duplex` wires both directions; each row's read\n 1272|    /// pump and `StreamLog` are the SAME machinery the QUIC path uses (the W1.5\n...\n 1276|    // [impl->REQ-RC-1]\n*1277|    pub fn open_stream(&self, conn_id: u64) -> io::Result<u64> {\n 1278|        enum Backing {\n 1279|            Quic(Connection),\n 1280|            Loopback,\n...\n 1297|            Backing::Quic(conn) => self.bounded_block_on(async move {\n*1298|                let (send, recv) = conn\n 1299|                    .open_bi()\n 1300|                    .await\n 1301|                    .map_err(|e| io::Error::other(e.to_string()))?;\n...\n 1306|                    SendHalf::Quic(send),\n*1307|                    RecvHalf::Quic(recv),\n 1308|                    true,\n 1309|                ))\n 1310|            }),\n...\n 1334|                    SendHalf::Loopback(peer_w),\n*1335|                    RecvHalf::Loopback(peer_r),\n 1336|                    false,\n 1337|                );\n 1338|                Ok(register_stream(\n...\n 1342|                    SendHalf::Loopback(op_w),\n*1343|                    RecvHalf::Loopback(op_r),\n 1344|                    true,\n 1345|                ))\n 1346|            }),\n...\n 1349|\n*1350|    /// Record that journaled open `(minter, op_id)` minted `stream_id` (inside the\n 1351|    /// `apply_once` closure — same discipline as [`Self::record_dial_op`]).\n*1352|    pub fn record_stream_op(&self, minter: Minter, op_id: u64, stream_id: u64) {\n 1353|        self.stream_ops\n 1354|            .lock()\n 1355|            .unwrap()\n*1356|            .insert((minter, op_id), stream_id);\n 1357|    }\n 1358|\n 1359|    /// The stream a journaled open `(minter, op_id)` minted, if still held.\n...\n 1368|    /// A held stream's `(conn_id, remote_id_hex)`.\n*1369|    pub fn stream_conn(&self, stream_id: u64) -> Option<(u64, String)> {\n 1370|        self.shared\n 1371|            .streams\n 1372|            .lock()\n 1373|            .unwrap()\n*1374|            .get(&stream_id)\n 1375|            .map(|e| (e.conn_id, e.remote_id_hex.clone()))\n 1376|    }\n 1377|\n...\n 1380|    // [impl->REQ-DAEMON-2]\n*1381|    pub fn send_stream(&self, stream_id: u64, bytes: &[u8], finish: bool) -> io::Result<()> {\n 1382|        let entry = {\n 1383|            let streams = self.shared.streams.lock().unwrap();\n*1384|            Arc::clone(streams.get(&stream_id).ok_or_else(|| {\n 1385|                io::Error::new(\n 1386|                    io::ErrorKind::NotFound,\n*1387|                    format!(\"no such stream {stream_id}\"),\n 1388|                )\n 1389|            })?)\n 1390|        };\n...\n 1415|                NetStreamInfo {\n*1416|                    stream_id: *id,\n 1417|                    conn_id: e.conn_id,\n 1418|                    remote_id_hex: e.remote_id_hex.clone(),\n 1419|                    next_seq: log.next_seq,\n 1420|                    finished: log.finished,\n*1421|                    initiated_locally: e.initiated_locally,\n 1422|                }\n 1423|            })\n 1424|            .collect()\n...\n 1426|\n*1427|    /// Mark a held stream **retentive** with backpressure `cap` chunks (M11-W3,\n 1428|    /// REQ-SHELL-4): its read log stops evicting (no byte loss) and its read pump\n 1429|    /// lags the sender once the undrained ring reaches `cap` (no unbounded growth).\n 1430|    /// Used for shell-tunnel streams; ordinary streams stay bounded-overwrite.\n...\n 1432|    // [impl->REQ-SHELL-4]\n*1433|    pub fn mark_stream_retentive(&self, stream_id: u64, cap: usize) {\n*1434|        if let Some(e) = self.shared.streams.lock().unwrap().get(&stream_id) {\n*1435|            e.log.lock().unwrap().set_retentive(cap);\n 1436|        }\n 1437|    }\n 1438|\n*1439|    /// Drain a retentive stream's undrained bytes (the shell-tunnel recv cursor):\n 1440|    /// pops the whole ring in order, concatenated, and wakes the read pump (which\n 1441|    /// may have been lagging the sender at the backpressure cap). Returns\n 1442|    /// `(bytes, finished)`; `finished` ⇒ the read side ended (the poller closes).\n...\n 1444|    // [impl->REQ-SHELL-4]\n*1445|    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {\n*1446|        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n 1447|        let drained = entry.log.lock().unwrap().drain();\n 1448|        // The drain freed ring room — wake a pump that was backpressure-parked.\n 1449|        entry.room.notify_one();\n...\n 1453|    /// A held stream's current undrained ring depth in chunks (test introspection\n*1454|    /// for the retentive backpressure bound). `None` ⇒ no such stream.\n 1455|    #[cfg(test)]\n*1456|    fn ring_len(&self, stream_id: u64) -> Option<usize> {\n 1457|        self.shared\n 1458|            .streams\n 1459|            .lock()\n 1460|            .unwrap()\n*1461|            .get(&stream_id)\n 1462|            .map(|e| e.log.lock().unwrap().ring.len())\n 1463|    }\n 1464|\n...\n 1473|            .iter()\n*1474|            .filter(|(_, e)| e.conn_id == conn_id && !e.initiated_locally)\n 1475|            .map(|(id, _)| *id)\n 1476|            .collect()\n 1477|    }\n...\n 1481|    /// drives and the cross-wired peer row the shell side drives. The loopback\n*1482|    /// `open_stream` mints both rows in-process (one `duplex` wires them, so a\n 1483|    /// write to either send appears on the other's read log). Serialized so the\n 1484|    /// before/after diff sees exactly the one peer row this open minted.\n 1485|    // [impl->REQ-SHELL-4]\n...\n 1489|        let before = self.inbound_rows_on(conn_id);\n*1490|        let owner_stream = self.open_stream(conn_id)?;\n 1491|        let shell_stream = self\n 1492|            .inbound_rows_on(conn_id)\n 1493|            .difference(&before)\n...\n 1499|\n*1500|    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`\n*1501|    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).\n 1502|    // [impl->REQ-DAEMON-2]\n*1503|    pub(crate) fn subscribe_stream(\n 1504|        &self,\n*1505|        stream_id: u64,\n 1506|        sub: SharedSend,\n 1507|        from_seq: u64,\n 1508|    ) -> io::Result<()> {\n...\n 1510|            let streams = self.shared.streams.lock().unwrap();\n*1511|            let e = streams.get(&stream_id).ok_or_else(|| {\n 1512|                io::Error::new(\n 1513|                    io::ErrorKind::NotFound,\n*1514|                    format!(\"no such stream {stream_id}\"),\n 1515|                )\n 1516|            })?;\n 1517|            Arc::clone(&e.log)\n...\n 1522|\n*1523|    /// Detach `sub` from a stream iff it is the current subscriber (brain\n 1524|    /// disconnect cleanup; the stream + its ring persist).\n*1525|    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {\n 1526|        let log = {\n 1527|            let streams = self.shared.streams.lock().unwrap();\n*1528|            streams.get(&stream_id).map(|e| Arc::clone(&e.log))\n 1529|        };\n 1530|        if let Some(log) = log {\n 1531|            log.lock().unwrap().detach_if(sub);\n...\n 1534|\n*1535|    /// Attach `sub` as the presence subscriber, replaying liveness events from\n*1536|    /// `from_seq` (D4c) — the stream `subscribe` discipline on the conn table.\n 1537|    // [impl->REQ-EP-4]\n*1538|    pub(crate) fn subscribe_presence(&self, sub: SharedSend, from_seq: u64) {\n 1539|        self.shared.presence.lock().unwrap().attach(sub, from_seq);\n 1540|    }\n 1541|\n*1542|    /// Detach `sub` from presence iff it is the current subscriber (brain\n 1543|    /// disconnect cleanup; the log + its ring persist).\n 1544|    pub(crate) fn detach_presence_sub(&self, sub: &SharedSend) {\n 1545|        self.shared.presence.lock().unwrap().detach_if(sub);\n...\n 1675|    // mints a REUSED singleton whose origin is this node's OWN id, and\n*1676|    // `open_stream` on it cross-wires a PAIR in-process — an operator row and a\n 1677|    // peer row (the one the dispatcher serves) — with the operator's writes\n 1678|    // crossing to the peer row's read pump. No iroh handshake, no QUIC.\n 1679|    #[test]\n...\n 1695|\n*1696|        // open_stream mints the cross-wired pair: operator row + peer row.\n*1697|        let op_stream = host.open_stream(c1).expect(\"open loopback stream\");\n 1698|        let infos = host.stream_infos();\n 1699|        assert_eq!(infos.len(), 2, \"operator + peer rows\");\n 1700|        let op = infos\n 1701|            .iter()\n*1702|            .find(|s| s.stream_id == op_stream)\n 1703|            .expect(\"operator row present\");\n*1704|        assert!(op.initiated_locally, \"the returned id is the operator row\");\n 1705|        let peer = infos\n 1706|            .iter()\n*1707|            .find(|s| !s.initiated_locally)\n 1708|            .expect(\"peer row present\");\n 1709|        assert_eq!(peer.conn_id, c1);\n 1710|        assert_eq!(\n...\n 1716|        // pump (its log advances) — entirely in-process, no QUIC.\n*1717|        host.send_stream(op_stream, b\"ping\", false)\n 1718|            .expect(\"write operator side\");\n*1719|        let peer_id = peer.stream_id;\n 1720|        let mut crossed = false;\n 1721|        for _ in 0..400 {\n 1722|            if host\n...\n 1724|                .iter()\n*1725|                .any(|s| s.stream_id == peer_id && s.next_seq > 0)\n 1726|            {\n 1727|                crossed = true;\n 1728|                break;\n...\n 1737|\n*1738|    // [unit->REQ-SHELL-4] the ring policy split (M11-W3): a RETENTIVE log (a shell\n 1739|    // tunnel) never evicts — `drain` returns every appended byte in order even past\n 1740|    // the would-be cap (lossless, CONTEXT:262 \"cannot drop frames\"); an ordinary\n*1741|    // (non-retentive) log keeps bounded-overwrite (a slow brain must not OOM the\n 1742|    // broker), evicting the oldest chunks past `cap_chunks`.\n 1743|    #[test]\n*1744|    fn retentive_log_is_lossless_ordered_while_ordinary_log_overwrites() {\n*1745|        // Retentive: cap 3, append 8 distinct chunks, NONE evicted — drain returns\n 1746|        // all 8 concatenated in order.\n 1747|        let mut ret = StreamLog::new(1, 1);\n*1748|        ret.set_retentive(3);\n 1749|        for i in 0..8u8 {\n 1750|            ret.append(&[i]);\n 1751|        }\n 1752|        let (bytes, finished) = ret.drain();\n*1753|        assert_eq!(bytes, (0..8u8).collect::<Vec<_>>(), \"retentive loses nothing, in order\");\n 1754|        assert!(!finished);\n 1755|        // …and a second drain is empty (the cursor consumed them exactly once).\n 1756|        assert_eq!(ret.drain().0, Vec::<u8>::new());\n...\n 1767|    // [unit->REQ-SHELL-4] the loopback tunnel pair under BACKPRESSURE (M11-W3,\n*1768|    // doyle gate condition): a retentive shell-side stream that is not drained while\n 1769|    // the owner keeps writing must NOT lose bytes AND must NOT grow unbounded — the\n 1770|    // read pump lags the sender at the cap. After the reader catches up, every byte\n 1771|    // arrives byte-exact and in order. Proves \"lag, never loss\".\n...\n 1778|        const CAP: usize = 4;\n*1779|        host.mark_stream_retentive(owner, CAP);\n*1780|        host.mark_stream_retentive(shell, CAP);\n 1781|\n 1782|        // Distinct, read-chunk-sized frames so each lands as its own ring entry\n 1783|        // (no coalescing) — lets us observe the cap exactly. 20 frames >> CAP.\n...\n 1793|                for f in &frames {\n*1794|                    host.send_stream(owner, f, false).expect(\"send\");\n 1795|                    sent.fetch_add(1, AtOrd::Relaxed);\n 1796|                }\n*1797|                host.send_stream(owner, &[], true).expect(\"finish\");\n 1798|            })\n 1799|        };\n 1800|\n...\n 1814|            host.ring_len(shell).unwrap() <= CAP,\n*1815|            \"the retentive ring never exceeds the backpressure cap (bounded)\"\n 1816|        );\n 1817|        assert!(\n 1818|            sent.load(AtOrd::Relaxed) < N,\n...\n 1835|        writer.join().expect(\"writer joins\");\n*1836|        assert!(finished, \"the tunnel saw EOF after the owner finished\");\n 1837|        let expected: Vec<u8> = frames.concat();\n 1838|        assert_eq!(got.len(), expected.len(), \"no byte lost or duplicated\");\n 1839|        assert_eq!(got, expected, \"byte-exact, in order, under backpressure\");"}],"details":{"scopePath":"crates/spt-daemon/src/nethost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":179,"fileCount":1,"files":["crates/spt-daemon/src/nethost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/nethost.rs","count":179}],"truncated":false,"displayContent":"   37│use spt_net::net::endpoint::{\n  *38│    BindScope, Connection, EndpointAddr, LocalDiscovery, NetEndpoint, RecvStream, RelayPolicy,\n   39│    SendStream, SPT_NET_ALPN,\n   40│};\n   41│use spt_net::net::mesh::seedproof::ProofRole;\n     │...\n   48│use crate::msg::{\n  *49│    net_presence_event_envelope, net_stream_data_envelope, net_stream_eof_envelope,\n   50│    NetPresenceEvent, NetStreamInfo, PRESENCE_CONNECTED, PRESENCE_DIAL_FAILED, PRESENCE_DISCONNECTED,\n   51│};\n   52│\n     │...\n   58│/// Broker-side QUIC-op deadline (REQ-HAZARD-BROKER-QUIC-DEADLINE), in ms. Each\n  *59│/// brain-waiting QUIC await — `dial` / `open_stream` / `send_stream` — is bounded\n   60│/// by this so a black-holed or dead peer fails PROMPTLY with an ORDINARY error\n   61│/// the broker replies, never an unbounded await that wedges the single-threaded\n   62│/// pump (the B-half of REQ-HAZARD-PUMP-IPC-DEADLINE; the 2.2h hfenduleam wedge).\n     │...\n  108│/// the PTY `OutputLog`: large enough that a brain-restart window never evicts\n *109│/// unreplayed chunks in practice; bounded so an unsubscribed stream can't\n  110│/// buffer without limit.\n  111│const DEFAULT_STREAM_RING_CHUNKS: usize = 4096;\n  112│\n     │...\n  129│    /// A broker-minted in-process loopback (no iroh handle, no handshake). Its\n *130│    /// streams are minted eagerly in cross-wired pairs by [`NetHost::open_stream`];\n  131│    /// there is no accept loop and no closed-watcher (it lives for the host).\n  132│    Loopback,\n  133│}\n     │...\n  141│    remote_id_hex: String,\n *142│    /// The subnets the peer proved current-epoch seed-knowledge of at connect\n  143│    /// (Mesh-D2, REQ-MESH-1). Empty when seed-proof is disabled (`membership:\n  144│    /// None`). The D5 gate swap reads this in place of `is_trusted(subnet, …)`;\n  145│    /// at D2 it is cached but not yet consumed (the `is_trusted` gates still run\n     │...\n  151│/// discipline applied to the network (D4b): the stream's single read pump is\n *152│/// the only producer; a brain connection attaches as the subscriber; every\n *153│/// subscriber send happens under the enclosing `Mutex`, so live chunks and a\n  154│/// replay can never interleave out of `seq` order. Chunks produced while no\n *155│/// brain is attached sit in the ring and replay on the next subscribe — a\n  156│/// brain restart loses nothing (Spike #3 invariant B/D).\n  157│// [impl->REQ-DAEMON-2]\n *158│struct StreamLog {\n *159│    stream_id: u64,\n  160│    /// TRANSPORT read counter: bumped once per [`append`](Self::append) (one\n  161│    /// read-pump `read_chunk`), INDEPENDENT of byte size. NOT a logical-chunk\n  162│    /// count — QUIC may coalesce several sender writes into one receiver\n     │...\n  164│    /// chunks the sender issued for the SAME bytes. Correctness (byte-exact +\n *165│    /// exactly-once) holds via these transport seqs + the recv cursor regardless;\n  166│    /// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows\n  167│    /// coalescing flake) and must gate on BYTES instead.\n  168│    next_seq: u64,\n     │...\n  170│    cap_chunks: usize,\n *171│    subscriber: Option<SharedSend>,\n  172│    /// Read side finished cleanly (peer called finish / conn ended).\n  173│    finished: bool,\n *174│    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n *175│    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n  176│    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n *177│    /// congestion surfaces as lag, never loss.\" So a retentive log never cap-evicts\n  178│    /// on append (no loss), and the read pump REFUSES to pull the next chunk once\n  179│    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the\n  180│    /// transport's native flow-control then lags the *sender*. A chunk leaves the\n *181│    /// ring ONLY when [`drain`](Self::drain) (the recv cursor) pops it; the pump is\n  182│    /// woken to resume via the entry's `room` notifier. Ordinary streams keep\n *183│    /// `retentive_cap = 0` (a slow/absent brain must not OOM the broker via an\n *184│    /// unbounded ring) and are drained by subscribe, never the sync cursor.\n *185│    retentive_cap: usize,\n  186│}\n  187│\n  188│impl StreamLog {\n *189│    fn new(stream_id: u64, cap_chunks: usize) -> Self {\n  190│        StreamLog {\n *191│            stream_id,\n  192│            next_seq: 0,\n  193│            ring: VecDeque::new(),\n  194│            cap_chunks,\n *195│            subscriber: None,\n  196│            finished: false,\n *197│            retentive_cap: 0,\n  198│        }\n  199│    }\n  200│\n *201│    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\n *202│    fn set_retentive(&mut self, cap: usize) {\n *203│        self.retentive_cap = cap;\n  204│    }\n  205│\n  206│    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\n *207│    /// live-send to the current subscriber. Read-pump only. A non-retentive log\n *208│    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\n  209│    /// never evicts — bytes leave only via [`drain`](Self::drain).\n  210│    fn append(&mut self, bytes: &[u8]) {\n  211│        let seq = self.next_seq;\n     │...\n  213│        self.ring.push_back((seq, bytes.to_vec()));\n *214│        if self.retentive_cap == 0 {\n  215│            while self.ring.len() > self.cap_chunks {\n  216│                self.ring.pop_front();\n  217│            }\n  218│        }\n *219│        if let Some(sub) = &self.subscriber {\n *220│            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\n  221│            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n *222│            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n  223│            // the deadline instead of parking this producer.\n  224│            let _ = sub.write(&frame);\n  225│        }\n     │...\n  227│\n *228│    /// Drain every undrained chunk in order (the shell-tunnel recv cursor, W3).\n  229│    /// Pops the whole ring front-to-back (FIFO by `seq`) and concatenates the\n  230│    /// bytes — chunk boundaries are arbitrary read-pump splits, irrelevant to an\n  231│    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,\n  232│    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so\n *233│    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing\n  234│    /// pending yet.\n  235│    fn drain(&mut self) -> (Vec<u8>, bool) {\n  236│        let mut out = Vec::new();\n     │...\n  242│\n *243│    /// Mark the read side finished and tell the subscriber (with the final\n  244│    /// cursor, so it can verify completeness).\n  245│    fn finish(&mut self) {\n  246│        self.finished = true;\n *247│        if let Some(sub) = &self.subscriber {\n *248│            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n  249│            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n *250│            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n  251│            // the deadline instead of parking this producer.\n  252│            let _ = sub.write(&frame);\n  253│        }\n     │...\n  255│\n *256│    /// Attach `sub` as the subscriber, replaying every retained chunk with\n *257│    /// `seq >= from_seq` (then eof, if already finished) before live streaming.\n  258│    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n *259│        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\n  260│        // record — this is one of the two RCA-pinned shared-carrier classes.\n  261│        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n *262│        sub.describe(&format!(\"stream-subscriber stream={}\", self.stream_id));\n  263│        sub.lifecycle_event(\n  264│            \"stream-sub-attach\",\n *265│            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\n  266│        );\n *267│        self.subscriber = Some(Arc::clone(&sub));\n  268│        for (seq, bytes) in self.ring.iter().filter(|(s, _)| *s >= from_seq) {\n *269│            let frame = net_stream_data_envelope(self.stream_id, *seq, bytes);\n  270│            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n *271│            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n  272│            // the deadline instead of parking this producer.\n  273│            let _ = sub.write(&frame);\n  274│        }\n  275│        if self.finished {\n *276│            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n  277│            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n *278│            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n  279│            // the deadline instead of parking this producer.\n  280│            let _ = sub.write(&frame);\n  281│        }\n     │...\n  283│\n *284│    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n *285│    /// not unsubscribe a newer brain).\n  286│    fn detach_if(&mut self, sub: &SharedSend) {\n *287│        if let Some(cur) = &self.subscriber {\n  288│            if Arc::ptr_eq(cur, sub) {\n *289│                self.subscriber = None;\n  290│            }\n  291│        }\n  292│    }\n     │...\n  298│/// connection's closed-watcher appends `disconnected` after removing the row.\n *299│/// A brain subscribes with a cursor and replays events produced while it was\n  300│/// down, so presence survives a brain restart exactly like stream reads do.\n  301│// [impl->REQ-EP-4]\n  302│struct PresenceLog {\n     │...\n  305│    cap_events: usize,\n *306│    subscriber: Option<SharedSend>,\n  307│}\n  308│\n  309│impl PresenceLog {\n     │...\n  314│            cap_events,\n *315│            subscriber: None,\n  316│        }\n  317│    }\n  318│\n  319│    /// Append a liveness event: assign the next `seq`, retain it in the bounded\n *320│    /// ring, live-send to the current subscriber. Producers: `register_conn`\n  321│    /// (connected) and the per-conn closed-watchers (disconnected) — every\n *322│    /// append serializes on the enclosing `Mutex`, so subscriber frames arrive\n  323│    /// in strict `seq` order.\n  324│    fn append(&mut self, kind: &str, conn_id: u64, remote_id_hex: &str) {\n  325│        self.push(NetPresenceEvent {\n     │...\n  366│    /// Assign the next `seq`, retain in the bounded ring, live-send to the\n *367│    /// current subscriber. Every append serializes on the enclosing `Mutex`, so\n *368│    /// subscriber frames arrive in strict `seq` order.\n  369│    fn push(&mut self, mut ev: NetPresenceEvent) {\n  370│        ev.seq = self.next_seq;\n  371│        self.next_seq += 1;\n     │...\n  375│        }\n *376│        if let Some(sub) = &self.subscriber {\n  377│            let frame = net_presence_event_envelope(&ev);\n  378│            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n *379│            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n  380│            // the deadline instead of parking this producer.\n  381│            let _ = sub.write(&frame);\n  382│        }\n     │...\n  384│\n *385│    /// Attach `sub` as the subscriber, replaying every retained event with\n  386│    /// `seq >= from_seq` before live streaming (the gapless-resume attach).\n  387│    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n *388│        // Attribution (F-039 legs b+d): presence-subscriber role fact + attach\n  389│        // record — the OTHER RCA-pinned shared carrier (the per-dial-failure\n  390│        // presence push rides this conn). [impl->REQ-CONN-POISON-ATTRIBUTION]\n *391│        sub.describe(\"presence-subscriber\");\n  392│        sub.lifecycle_event(\"presence-sub-attach\", &format!(\"from_seq={from_seq}\"));\n *393│        self.subscriber = Some(Arc::clone(&sub));\n  394│        for ev in self.ring.iter().filter(|e| e.seq >= from_seq) {\n  395│            let frame = net_presence_event_envelope(ev);\n  396│            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n *397│            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n  398│            // the deadline instead of parking this producer.\n  399│            let _ = sub.write(&frame);\n  400│        }\n     │...\n  402│\n *403│    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n *404│    /// not unsubscribe a newer brain).\n  405│    fn detach_if(&mut self, sub: &SharedSend) {\n *406│        if let Some(cur) = &self.subscriber {\n  407│            if Arc::ptr_eq(cur, sub) {\n *408│                self.subscriber = None;\n  409│            }\n  410│        }\n  411│    }\n     │...\n  448│/// QUIC reads via quinn's chunk API; loopback via the read end of the duplex.\n *449│/// Both append to the same [`StreamLog`], so the subscriber (and everything\n  450│/// above it) cannot tell the transports apart.\n *451│enum RecvHalf {\n *452│    Quic(RecvStream),\n  453│    Loopback(ReadHalf<DuplexStream>),\n  454│}\n  455│\n     │...\n  457│/// writes ride it over IPC) + the sequenced read log.\n *458│struct StreamEntry {\n  459│    conn_id: u64,\n  460│    remote_id_hex: String,\n *461│    /// Whether THIS node opened the stream (`open_stream`) vs the peer\n  462│    /// (accept loop). The inbound dispatcher (D9-1) claims only\n  463│    /// peer-initiated streams; without this fact the table is ambiguous —\n  464│    /// it holds locally-opened outbound streams too (gotcha: a dispatcher\n  465│    /// that served its own requester stream would deadlock on itself).\n *466│    initiated_locally: bool,\n  467│    /// The send half, serialized behind an async mutex (writes happen on the\n  468│    /// host's runtime via `block_on`).\n  469│    send: tokio::sync::Mutex<SendHalf>,\n  470│    log: Arc<Mutex<StreamLog>>,\n *471│    /// Wakes the read pump when a retentive (tunnel) stream's ring drains below\n *472│    /// its backpressure cap (M11-W3, REQ-SHELL-4) — see [`StreamLog::retentive_cap`].\n *473│    /// Unused by ordinary (non-retentive) streams.\n  474│    room: Arc<tokio::sync::Notify>,\n  475│}\n  476│\n     │...\n  481│    conns: Mutex<HashMap<u64, ConnEntry>>,\n *482│    next_stream_id: AtomicU64,\n  483│    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,\n  484│    ring_cap: usize,\n  485│    /// The conn-liveness event log (D4c) — one per host, all conns interleaved.\n     │...\n  493│            conns: Mutex::new(HashMap::new()),\n *494│            next_stream_id: AtomicU64::new(1),\n  495│            streams: Mutex::new(HashMap::new()),\n  496│            ring_cap: DEFAULT_STREAM_RING_CHUNKS,\n  497│            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),\n     │...\n  604│        conn_close.closed().await;\n *605│        sh_close.conns.lock().unwrap().remove(&id);\n  606│        sh_close\n  607│            .presence\n  608│            .lock()\n     │...\n  618│    tokio::spawn(async move {\n *619│        while let Ok((send, recv)) = conn.accept_bi().await {\n  620│            register_stream(\n  621│                &sh,\n  622│                id,\n     │...\n  624│                SendHalf::Quic(send),\n *625│                RecvHalf::Quic(recv),\n  626│                false,\n  627│            );\n  628│        }\n     │...\n  635│/// its read pump. Must run inside the host's runtime context.\n *636│/// `initiated_locally` records the direction fact for the table (true only\n *637│/// from [`NetHost::open_stream`]).\n  638│fn register_stream(\n  639│    shared: &Arc<NetShared>,\n  640│    conn_id: u64,\n     │...\n  642│    send: SendHalf,\n *643│    recv: RecvHalf,\n *644│    initiated_locally: bool,\n  645│) -> u64 {\n *646│    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);\n  647│    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));\n  648│    let room = Arc::new(tokio::sync::Notify::new());\n  649│    shared.streams.lock().unwrap().insert(\n     │...\n  653│            remote_id_hex: remote_id_hex.to_string(),\n *654│            initiated_locally,\n  655│            send: tokio::sync::Mutex::new(send),\n  656│            log: Arc::clone(&log),\n  657│            room: Arc::clone(&room),\n     │...\n  666│    //\n *667│    // Backpressure (M11-W3, REQ-SHELL-4): a retentive (tunnel) log never evicts,\n  668│    // so before pulling the next chunk the pump WAITS while the undrained ring is\n *669│    // at its `retentive_cap`. The unread bytes stay in the transport's buffer, so\n  670│    // QUIC stream flow-control (or the loopback duplex's bounded buffer) lags the\n  671│    // SENDER — \"lag, never loss\" (CONTEXT:262). A `drain` wakes `room`. Ordinary\n *672│    // streams have `retentive_cap == 0`, so this gate is a no-op for them.\n  673│    tokio::spawn(async move {\n *674│        match recv {\n *675│            RecvHalf::Quic(mut recv) => loop {\n  676│                await_ring_room(&log, &room).await;\n *677│                match recv.read_chunk(STREAM_READ_CHUNK).await {\n  678│                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),\n  679│                    Ok(None) | Err(_) => {\n  680│                        log.lock().unwrap().finish();\n     │...\n  684│            },\n *685│            RecvHalf::Loopback(mut recv) => {\n  686│                let mut buf = vec![0u8; STREAM_READ_CHUNK];\n  687│                loop {\n  688│                    await_ring_room(&log, &room).await;\n *689│                    match recv.read(&mut buf).await {\n  690│                        Ok(0) | Err(_) => {\n  691│                            log.lock().unwrap().finish();\n  692│                            break;\n     │...\n  703│\n *704│/// Backpressure gate for a retentive (tunnel) read pump: park until the undrained\n *705│/// ring has room below `retentive_cap`, woken by a `drain` on `room`. A no-op for\n *706│/// an ordinary stream (`retentive_cap == 0`). Holds no lock across the await.\n  707│// [impl->REQ-SHELL-4]\n  708│async fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {\n  709│    loop {\n     │...\n  711│            let l = log.lock().unwrap();\n *712│            if l.retentive_cap == 0 || l.ring.len() < l.retentive_cap {\n  713│                return;\n  714│            }\n  715│        }\n     │...\n  732│    dial_ops: Mutex<HashMap<(Minter, u64), u64>>,\n *733│    /// `(minter, op_id) → stream_id` for exactly-once stream-open replies (D4b),\n  734│    /// same discipline as `dial_ops`. Minter-namespaced (ADR-0034), matching the\n  735│    /// journal key the broker builds for the same op.\n  736│    stream_ops: Mutex<HashMap<(Minter, u64), u64>>,\n     │...\n  776│    /// DIAGNOSTIC (UPDATE-WEDGE round-3, throwaway): reactor-liveness canary. A\n *777│    /// runtime task stamps epoch-ms here every ~25ms. If the 2 workers are\n  778│    /// MONOPOLIZED (a non-yielding dead-peer connect that never awaits back to\n  779│    /// the scheduler) the stamp goes STALE — the DIRECT worker-starvation\n  780│    /// discriminator the round-3 rig asserts on. A fresh stamp under a dead-peer\n     │...\n  785│\n *786│/// DIAGNOSTIC helper (round-3): epoch-ms wall clock for the canary stamp.\n *787│fn epoch_ms() -> u64 {\n  788│    std::time::SystemTime::now()\n *789│        .duration_since(std::time::UNIX_EPOCH)\n  790│        .map(|d| d.as_millis() as u64)\n  791│        .unwrap_or(0)\n  792│}\n     │...\n  918│        // DIAGNOSTIC (UPDATE-WEDGE round-3): the reactor-liveness canary. Stamps\n *919│        // epoch-ms every ~25ms FROM the 2-worker runtime, so a starved runtime\n  920│        // (workers monopolized by non-yielding dead-peer connects) shows as a\n  921│        // stale stamp. Throwaway probe — no REQ, gated by nothing (cheap store).\n *922│        let net_canary_tick_ms = Arc::new(AtomicU64::new(epoch_ms()));\n  923│        let canary = Arc::clone(&net_canary_tick_ms);\n  924│        runtime.spawn(async move {\n  925│            let mut ivl = tokio::time::interval(Duration::from_millis(25));\n     │...\n  927│                ivl.tick().await;\n *928│                canary.store(epoch_ms(), Ordering::Relaxed);\n  929│            }\n  930│        });\n  931│\n     │...\n  985│    /// frame, so the brain reconstructs an `ErrorKind::Other` (`Brain::net_dial` /\n *986│    /// `net_open_stream` / `net_stream_send` → `io::Error::other`) and\n  987│    /// `pump::peer_outcome` takes the ordinary per-peer arm (drop conn + redial,\n  988│    /// round continues, heartbeat advances) — DISTINCT from the brain's OWN\n  989│    /// read-deadline `TimedOut` (the A-half poison → supervised-restart signal).\n     │...\n 1075│    pub fn net_canary_age_ms(&self) -> u64 {\n*1076│        epoch_ms().saturating_sub(self.net_canary_tick_ms.load(Ordering::Relaxed))\n 1077│    }\n 1078│\n 1079│    /// DIAGNOSTIC (round-3): free permits on the shared dial-concurrency semaphore\n     │...\n 1196│\n*1197│    /// The subnets the peer of `conn_id` proved current-epoch seed-knowledge of\n 1198│    /// at connect (Mesh-D2, REQ-MESH-1). Empty when seed-proof was disabled or\n 1199│    /// the conn is gone. The seam the D5 gate swap reads in place of\n 1200│    /// `is_trusted(subnet, origin)`; unused by any gate at D2.\n     │...\n 1267│    /// so the open mints a CROSS-WIRED PAIR in-process: an operator row\n*1268│    /// (`initiated_locally = true`, the returned id) and a peer row\n*1269│    /// (`initiated_locally = false`) the inbound dispatcher claims and serves via\n 1270│    /// `serve_attach` — exactly as an `accept_bi` would surface a remote-opened\n 1271│    /// stream. One `tokio::io::duplex` wires both directions; each row's read\n 1272│    /// pump and `StreamLog` are the SAME machinery the QUIC path uses (the W1.5\n     │...\n 1276│    // [impl->REQ-RC-1]\n*1277│    pub fn open_stream(&self, conn_id: u64) -> io::Result<u64> {\n 1278│        enum Backing {\n 1279│            Quic(Connection),\n 1280│            Loopback,\n     │...\n 1297│            Backing::Quic(conn) => self.bounded_block_on(async move {\n*1298│                let (send, recv) = conn\n 1299│                    .open_bi()\n 1300│                    .await\n 1301│                    .map_err(|e| io::Error::other(e.to_string()))?;\n     │...\n 1306│                    SendHalf::Quic(send),\n*1307│                    RecvHalf::Quic(recv),\n 1308│                    true,\n 1309│                ))\n 1310│            }),\n     │...\n 1334│                    SendHalf::Loopback(peer_w),\n*1335│                    RecvHalf::Loopback(peer_r),\n 1336│                    false,\n 1337│                );\n 1338│                Ok(register_stream(\n     │...\n 1342│                    SendHalf::Loopback(op_w),\n*1343│                    RecvHalf::Loopback(op_r),\n 1344│                    true,\n 1345│                ))\n 1346│            }),\n     │...\n 1349│\n*1350│    /// Record that journaled open `(minter, op_id)` minted `stream_id` (inside the\n 1351│    /// `apply_once` closure — same discipline as [`Self::record_dial_op`]).\n*1352│    pub fn record_stream_op(&self, minter: Minter, op_id: u64, stream_id: u64) {\n 1353│        self.stream_ops\n 1354│            .lock()\n 1355│            .unwrap()\n*1356│            .insert((minter, op_id), stream_id);\n 1357│    }\n 1358│\n 1359│    /// The stream a journaled open `(minter, op_id)` minted, if still held.\n     │...\n 1368│    /// A held stream's `(conn_id, remote_id_hex)`.\n*1369│    pub fn stream_conn(&self, stream_id: u64) -> Option<(u64, String)> {\n 1370│        self.shared\n 1371│            .streams\n 1372│            .lock()\n 1373│            .unwrap()\n*1374│            .get(&stream_id)\n 1375│            .map(|e| (e.conn_id, e.remote_id_hex.clone()))\n 1376│    }\n 1377│\n     │...\n 1380│    // [impl->REQ-DAEMON-2]\n*1381│    pub fn send_stream(&self, stream_id: u64, bytes: &[u8], finish: bool) -> io::Result<()> {\n 1382│        let entry = {\n 1383│            let streams = self.shared.streams.lock().unwrap();\n*1384│            Arc::clone(streams.get(&stream_id).ok_or_else(|| {\n 1385│                io::Error::new(\n 1386│                    io::ErrorKind::NotFound,\n*1387│                    format!(\"no such stream {stream_id}\"),\n 1388│                )\n 1389│            })?)\n 1390│        };\n     │...\n 1415│                NetStreamInfo {\n*1416│                    stream_id: *id,\n 1417│                    conn_id: e.conn_id,\n 1418│                    remote_id_hex: e.remote_id_hex.clone(),\n 1419│                    next_seq: log.next_seq,\n 1420│                    finished: log.finished,\n*1421│                    initiated_locally: e.initiated_locally,\n 1422│                }\n 1423│            })\n 1424│            .collect()\n     │...\n 1426│\n*1427│    /// Mark a held stream **retentive** with backpressure `cap` chunks (M11-W3,\n 1428│    /// REQ-SHELL-4): its read log stops evicting (no byte loss) and its read pump\n 1429│    /// lags the sender once the undrained ring reaches `cap` (no unbounded growth).\n 1430│    /// Used for shell-tunnel streams; ordinary streams stay bounded-overwrite.\n     │...\n 1432│    // [impl->REQ-SHELL-4]\n*1433│    pub fn mark_stream_retentive(&self, stream_id: u64, cap: usize) {\n*1434│        if let Some(e) = self.shared.streams.lock().unwrap().get(&stream_id) {\n*1435│            e.log.lock().unwrap().set_retentive(cap);\n 1436│        }\n 1437│    }\n 1438│\n*1439│    /// Drain a retentive stream's undrained bytes (the shell-tunnel recv cursor):\n 1440│    /// pops the whole ring in order, concatenated, and wakes the read pump (which\n 1441│    /// may have been lagging the sender at the backpressure cap). Returns\n 1442│    /// `(bytes, finished)`; `finished` ⇒ the read side ended (the poller closes).\n     │...\n 1444│    // [impl->REQ-SHELL-4]\n*1445│    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {\n*1446│        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n 1447│        let drained = entry.log.lock().unwrap().drain();\n 1448│        // The drain freed ring room — wake a pump that was backpressure-parked.\n 1449│        entry.room.notify_one();\n     │...\n 1453│    /// A held stream's current undrained ring depth in chunks (test introspection\n*1454│    /// for the retentive backpressure bound). `None` ⇒ no such stream.\n 1455│    #[cfg(test)]\n*1456│    fn ring_len(&self, stream_id: u64) -> Option<usize> {\n 1457│        self.shared\n 1458│            .streams\n 1459│            .lock()\n 1460│            .unwrap()\n*1461│            .get(&stream_id)\n 1462│            .map(|e| e.log.lock().unwrap().ring.len())\n 1463│    }\n 1464│\n     │...\n 1473│            .iter()\n*1474│            .filter(|(_, e)| e.conn_id == conn_id && !e.initiated_locally)\n 1475│            .map(|(id, _)| *id)\n 1476│            .collect()\n 1477│    }\n     │...\n 1481│    /// drives and the cross-wired peer row the shell side drives. The loopback\n*1482│    /// `open_stream` mints both rows in-process (one `duplex` wires them, so a\n 1483│    /// write to either send appears on the other's read log). Serialized so the\n 1484│    /// before/after diff sees exactly the one peer row this open minted.\n 1485│    // [impl->REQ-SHELL-4]\n     │...\n 1489│        let before = self.inbound_rows_on(conn_id);\n*1490│        let owner_stream = self.open_stream(conn_id)?;\n 1491│        let shell_stream = self\n 1492│            .inbound_rows_on(conn_id)\n 1493│            .difference(&before)\n     │...\n 1499│\n*1500│    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`\n*1501│    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).\n 1502│    // [impl->REQ-DAEMON-2]\n*1503│    pub(crate) fn subscribe_stream(\n 1504│        &self,\n*1505│        stream_id: u64,\n 1506│        sub: SharedSend,\n 1507│        from_seq: u64,\n 1508│    ) -> io::Result<()> {\n     │...\n 1510│            let streams = self.shared.streams.lock().unwrap();\n*1511│            let e = streams.get(&stream_id).ok_or_else(|| {\n 1512│                io::Error::new(\n 1513│                    io::ErrorKind::NotFound,\n*1514│                    format!(\"no such stream {stream_id}\"),\n 1515│                )\n 1516│            })?;\n 1517│            Arc::clone(&e.log)\n     │...\n 1522│\n*1523│    /// Detach `sub` from a stream iff it is the current subscriber (brain\n 1524│    /// disconnect cleanup; the stream + its ring persist).\n*1525│    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {\n 1526│        let log = {\n 1527│            let streams = self.shared.streams.lock().unwrap();\n*1528│            streams.get(&stream_id).map(|e| Arc::clone(&e.log))\n 1529│        };\n 1530│        if let Some(log) = log {\n 1531│            log.lock().unwrap().detach_if(sub);\n     │...\n 1534│\n*1535│    /// Attach `sub` as the presence subscriber, replaying liveness events from\n*1536│    /// `from_seq` (D4c) — the stream `subscribe` discipline on the conn table.\n 1537│    // [impl->REQ-EP-4]\n*1538│    pub(crate) fn subscribe_presence(&self, sub: SharedSend, from_seq: u64) {\n 1539│        self.shared.presence.lock().unwrap().attach(sub, from_seq);\n 1540│    }\n 1541│\n*1542│    /// Detach `sub` from presence iff it is the current subscriber (brain\n 1543│    /// disconnect cleanup; the log + its ring persist).\n 1544│    pub(crate) fn detach_presence_sub(&self, sub: &SharedSend) {\n 1545│        self.shared.presence.lock().unwrap().detach_if(sub);\n     │...\n 1675│    // mints a REUSED singleton whose origin is this node's OWN id, and\n*1676│    // `open_stream` on it cross-wires a PAIR in-process — an operator row and a\n 1677│    // peer row (the one the dispatcher serves) — with the operator's writes\n 1678│    // crossing to the peer row's read pump. No iroh handshake, no QUIC.\n 1679│    #[test]\n     │...\n 1695│\n*1696│        // open_stream mints the cross-wired pair: operator row + peer row.\n*1697│        let op_stream = host.open_stream(c1).expect(\"open loopback stream\");\n 1698│        let infos = host.stream_infos();\n 1699│        assert_eq!(infos.len(), 2, \"operator + peer rows\");\n 1700│        let op = infos\n 1701│            .iter()\n*1702│            .find(|s| s.stream_id == op_stream)\n 1703│            .expect(\"operator row present\");\n*1704│        assert!(op.initiated_locally, \"the returned id is the operator row\");\n 1705│        let peer = infos\n 1706│            .iter()\n*1707│            .find(|s| !s.initiated_locally)\n 1708│            .expect(\"peer row present\");\n 1709│        assert_eq!(peer.conn_id, c1);\n 1710│        assert_eq!(\n     │...\n 1716│        // pump (its log advances) — entirely in-process, no QUIC.\n*1717│        host.send_stream(op_stream, b\"ping\", false)\n 1718│            .expect(\"write operator side\");\n*1719│        let peer_id = peer.stream_id;\n 1720│        let mut crossed = false;\n 1721│        for _ in 0..400 {\n 1722│            if host\n     │...\n 1724│                .iter()\n*1725│                .any(|s| s.stream_id == peer_id && s.next_seq > 0)\n 1726│            {\n 1727│                crossed = true;\n 1728│                break;\n     │...\n 1737│\n*1738│    // [unit->REQ-SHELL-4] the ring policy split (M11-W3): a RETENTIVE log (a shell\n 1739│    // tunnel) never evicts — `drain` returns every appended byte in order even past\n 1740│    // the would-be cap (lossless, CONTEXT:262 \"cannot drop frames\"); an ordinary\n*1741│    // (non-retentive) log keeps bounded-overwrite (a slow brain must not OOM the\n 1742│    // broker), evicting the oldest chunks past `cap_chunks`.\n 1743│    #[test]\n*1744│    fn retentive_log_is_lossless_ordered_while_ordinary_log_overwrites() {\n*1745│        // Retentive: cap 3, append 8 distinct chunks, NONE evicted — drain returns\n 1746│        // all 8 concatenated in order.\n 1747│        let mut ret = StreamLog::new(1, 1);\n*1748│        ret.set_retentive(3);\n 1749│        for i in 0..8u8 {\n 1750│            ret.append(&[i]);\n 1751│        }\n 1752│        let (bytes, finished) = ret.drain();\n*1753│        assert_eq!(bytes, (0..8u8).collect::<Vec<_>>(), \"retentive loses nothing, in order\");\n 1754│        assert!(!finished);\n 1755│        // …and a second drain is empty (the cursor consumed them exactly once).\n 1756│        assert_eq!(ret.drain().0, Vec::<u8>::new());\n     │...\n 1767│    // [unit->REQ-SHELL-4] the loopback tunnel pair under BACKPRESSURE (M11-W3,\n*1768│    // doyle gate condition): a retentive shell-side stream that is not drained while\n 1769│    // the owner keeps writing must NOT lose bytes AND must NOT grow unbounded — the\n 1770│    // read pump lags the sender at the cap. After the reader catches up, every byte\n 1771│    // arrives byte-exact and in order. Proves \"lag, never loss\".\n     │...\n 1778│        const CAP: usize = 4;\n*1779│        host.mark_stream_retentive(owner, CAP);\n*1780│        host.mark_stream_retentive(shell, CAP);\n 1781│\n 1782│        // Distinct, read-chunk-sized frames so each lands as its own ring entry\n 1783│        // (no coalescing) — lets us observe the cap exactly. 20 frames >> CAP.\n     │...\n 1793│                for f in &frames {\n*1794│                    host.send_stream(owner, f, false).expect(\"send\");\n 1795│                    sent.fetch_add(1, AtOrd::Relaxed);\n 1796│                }\n*1797│                host.send_stream(owner, &[], true).expect(\"finish\");\n 1798│            })\n 1799│        };\n 1800│\n     │...\n 1814│            host.ring_len(shell).unwrap() <= CAP,\n*1815│            \"the retentive ring never exceeds the backpressure cap (bounded)\"\n 1816│        );\n 1817│        assert!(\n 1818│            sent.load(AtOrd::Relaxed) < N,\n     │...\n 1835│        writer.join().expect(\"writer joins\");\n*1836│        assert!(finished, \"the tunnel saw EOF after the owner finished\");\n 1837│        let expected: Vec<u8> = frames.concat();\n 1838│        assert_eq!(got.len(), expected.len(), \"no byte lost or duplicated\");\n 1839│        assert_eq!(got, expected, \"byte-exact, in order, under backpressure\");"},"isError":false,"timestamp":1784195875590}}
{"type":"message","id":"58e21912","parentId":"64b1450c","timestamp":"2026-07-16T09:57:55.652Z","message":{"role":"toolResult","toolCallId":"call_lhse7jkSs7HiVmhlpt5ViMrb|fc_047bb49263dfa303016a58ab2322e481988d780a0cbca0c4a0","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/tests/\n## access.rs\n 4|//! reply (stateful firewall), and admits a whitelisted node — with edits to\n*5|//! the store taking effect mid-stream (the gate re-reads per check).\n 6|//!\n 7|//! Two in-process brokers (hermetic loopback endpoints) play the two daemons,\n 8|//! the same harness as `wanmsg.rs`. The payload-origin spoof negative lives\n...\n 18|use spt_daemon::access::note_outbound;\n*19|use spt_daemon::brain::{Brain, BrokerEvent};\n 20|use spt_daemon::effect::{Minter, MintedOp};\n 21|use spt_daemon::nethost::{NetConfig, NetHost};\n 22|use spt_daemon::registryhost::RegistryHost;\n...\n 73|\n*74|fn connect_retry(name: &str) -> Brain {\n 75|    for _ in 0..300 {\n*76|        if let Ok(b) = Brain::cold_start(name, 1) {\n 77|            return b;\n 78|        }\n 79|        thread::sleep(Duration::from_millis(10));\n 80|    }\n*81|    panic!(\"brain could not connect\");\n 82|}\n 83|\n*84|fn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n 85|    for _ in 0..400 {\n*86|        let reply = brain.net_streams().expect(\"net-streams\");\n*87|        if let Some(s) = reply.streams.first() {\n*88|            return (s.stream_id, s.remote_id_hex.clone());\n 89|        }\n 90|        thread::sleep(Duration::from_millis(5));\n 91|    }\n*92|    panic!(\"stream never appeared at the receiver's broker\");\n 93|}\n 94|\n 95|fn offline_perch(id: &str) -> std::path::PathBuf {\n...\n 106|fn funnel_one(\n*107|    brain: &mut Brain,\n*108|    stream_id: u64,\n 109|    decoder: &mut WanDecoder,\n 110|    origin: &str,\n 111|    owlery: &std::path::Path,\n...\n 114|    loop {\n*115|        match brain.read_event().expect(\"read event\") {\n*116|            BrokerEvent::NetStreamData {\n*117|                stream_id: sid,\n 118|                bytes,\n 119|                ..\n*120|            } if sid == stream_id => {\n 121|                if let Some(rec) = decoder.push(&bytes).into_iter().next() {\n 122|                    return receive_wan(&rec, origin, owlery, registry);\n 123|                }\n 124|            }\n*125|            BrokerEvent::NetStreamEof { .. } => panic!(\"eof before the record arrived\"),\n 126|            BrokerEvent::Error { message } => panic!(\"broker error: {message}\"),\n 127|            _ => {}\n 128|        }\n...\n 145|// firewall, the whitelist untouched); (3) whitelisting the node admits its\n*146|// unsolicited traffic again — store edits take effect mid-stream, no daemon\n*147|// restart.\n 148|#[test]\n 149|fn whitelist_refuses_exempts_replies_and_admits_listed() {\n 150|    init_home();\n...\n 168|\n*169|    // Node A dials B and opens the message stream.\n 170|    let mut sender = connect_retry(&name_a);\n 171|    let a_node = sender\n 172|        .net_status()\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 5|//! Two in-process brokers (hermetic loopback endpoints, relays disabled) play\n...\n 9|//! - **remote drive**: keystrokes sent from B arrive at A's PTY through the\n*10|//!   journaled input path, and the session's output streams back to B as\n 11|//!   sequenced records — B sees its own marker echoed by A's child;\n 12|//! - **origin is transport truth** (KNOWN-HAZARDS 7.5): the serve loop gates\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 18|//!   dedups at the PTY-write journal (typed exactly once), re-transmitted\n 19|//!   output dedups at B's render cursor (rendered exactly once) — including\n 20|//!   input B sent INTO the dead window.\n...\n 27|use spt_daemon::attach::{\n*28|    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n 29|    AttachServeOutcome, ControllerIrrecoverablyBehind,\n 30|};\n 31|// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n...\n 33|use spt_daemon::attach::request_attach_endpoint;\n*34|use spt_daemon::brain::{Brain, BrainState, BrokerEvent};\n 35|use spt_daemon::effect::{Minter, MintedOp};\n 36|use spt_daemon::msg::{decode_bytes, SpawnReq};\n 37|use spt_daemon::nethost::{NetConfig, NetHost};\n...\n 45|/// Hold the temp home for the whole test binary so it isn't deleted mid-run.\n*46|/// Every test inits it: serve_attach best-effort-stamps the endpoint perch's\n 47|/// `driven_by` marker, and that resolution must never touch a real home.\n 48|static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n 49|\n...\n 84|\n*85|fn connect_retry(name: &str) -> Brain {\n 86|    for _ in 0..300 {\n*87|        if let Ok(b) = Brain::cold_start(name, 1) {\n 88|            return b;\n 89|        }\n 90|        thread::sleep(Duration::from_millis(10));\n 91|    }\n*92|    panic!(\"brain could not connect\");\n 93|}\n 94|\n 95|fn echo_spawn_req() -> SpawnReq {\n...\n 113|\n*114|/// Poll the brain's `net-streams` until a PEER-initiated stream appears; return\n*115|/// `(stream_id, remote_id_hex)`. Filtering on `!initiated_locally` is what the\n 116|/// production dispatch loop does (dispatch.rs) — and it is REQUIRED for the\n 117|/// single-broker loopback case, where the table holds BOTH the operator's own\n 118|/// row and the cross-wired peer row: the serve target is always the peer row.\n*119|fn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n 120|    for _ in 0..400 {\n*121|        let reply = brain.net_streams().expect(\"net-streams\");\n*122|        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {\n*123|            return (s.stream_id, s.remote_id_hex.clone());\n 124|        }\n 125|        thread::sleep(Duration::from_millis(5));\n 126|    }\n*127|    panic!(\"attach stream never appeared at the target's broker\");\n 128|}\n 129|\n 130|/// The operator+serve drive shared by EVERY attach transport — the M12 W1.5\n## brain_decouple.rs\n*1|//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\n 2|//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\n 3|//!\n 4|//! ## The bug this gate locks down (the update-wedge root)\n 5|//!\n*6|//! A controller's writer thread does a BLOCKING socket write to its brain\n*7|//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\n*8|//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\n 9|//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\n 10|//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\n 11|//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\n...\n 14|//!\n*15|//! ## Faithful in-process analog of a suspended brain\n 16|//!\n 17|//! A test process cannot `NtSuspendProcess` itself, so the rig reproduces the exact\n 18|//! MECHANISM: controller A stops reading its socket. The flood child fills A's recv\n 19|//! buffer, the controller channel fills, and A's writer thread parks INSIDE\n*20|//! `write_frame` — precisely the state a suspended brain leaves its broker conn in\n 21|//! (a peer that stopped draining). This is the same analog the 7.12 wedge gate uses.\n*22|//! `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15 s production deadline so the\n 23|//! structural outcome is provable in ~1 s (the deadline knob grants wall-clock only;\n 24|//! it never relaxes an assertion).\n 25|//!\n...\n 27|//!\n*28|//! With the wedged controller's writer parked past the brain-write deadline:\n 29|//! 1. an attached VIEWER keeps ticking (the fan-out never blocks on the wedged conn);\n 30|//! 2. a second operator's `Control` subscribe COMPLETES — the wedged incumbent is\n 31|//!    stall-evicted broker-side and the newcomer takes the freed slot (pre-W2 this\n...\n 36|//!\n*37|//! What this rig does NOT assert (covered elsewhere / manual): resume + cursor-replay\n 38|//! byte-identity is NOT re-proven here — W2's delta does not touch the replay path, so\n 39|//! output integrity stays covered by the pre-existing ring/cursor units (the effect-\n 40|//! journal replay dedup, the `delivered_through` no-rewind CAS `advanced_cursor`, and\n 41|//! the clean-repaint cold-attach batch). A live `NtSuspendProcess` / `SIGSTOP` run on a\n*42|//! real brain child remains the MANUAL field rig from the incident night — this CI test\n 43|//! is the deterministic in-process ANALOG, not that run.\n 44|//!\n 45|//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n 46|//!\n*47|//! Shares ONE process-global `SPT_HOME` + the `SPT_BRAIN_WRITE_DEADLINE_MS` env, and\n 48|//! spins a REAL broker + PTY child. Run isolated with\n*49|//! `cargo nextest run --test brain_decouple` (process-per-test — CI's path) or\n*50|//! `cargo test --test brain_decouple -- --test-threads=1`. Never a bare\n*51|//! `cargo test --test brain_decouple` on Windows.\n 52|\n 53|use std::sync::atomic::{AtomicU32, Ordering};\n 54|use std::sync::{Arc, OnceLock};\n...\n 57|\n*58|use interprocess::local_socket::Stream;\n 59|use spt_daemon::codec::{read_frame, write_frame};\n 60|use spt_daemon::frame::{Envelope, Role};\n 61|use spt_daemon::msg::{\n...\n 71|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n*72|    format!(\"spt-daemon-braindecouple-{}-{}.sock\", std::process::id(), n)\n 73|}\n 74|\n 75|static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n...\n 121|\n*122|fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 123|    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 124|}\n 125|\n*126|fn connect(name: &str) -> Stream {\n 127|    for _ in 0..200 {\n 128|        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n*129|            send_hello(&mut c, Role::Brain).expect(\"hello\");\n 130|            return c;\n 131|        }\n 132|        thread::sleep(Duration::from_millis(10));\n...\n 151|            Ok(mut c) => {\n*152|                if send_hello(&mut c, Role::Brain).is_err() {\n 153|                    let _ = tx.send(None);\n 154|                    return;\n 155|                }\n## brain_resume_conn_deadlock.rs\n*1|//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\n 2|//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\n 3|//!\n 4|//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n*5|//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n*6|//! onto the brain's OWN request/reply conn. A conn's send half is one\n 7|//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n 8|//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\n 9|//! `write_frame`, and the dispatch reply path (`send_frame`) needs the SAME lock.\n*10|//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\n 11|//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\n*12|//! calls. When an actively-streaming session backs the conn up, the brain's own\n 13|//! subscriber writer blocks holding `send.lock()` → the heartbeat REPLY is starved →\n*14|//! the heartbeat stalls → the brain never drains → SELF-DEADLOCK. That is the field\n 15|//! wedge (both severities: respawn replay-burst + steady-state output backup), with\n 16|//! NO dead peer required.\n 17|//!\n*18|//! RED-first discipline: these tests assert the FIXED (Option A) behavior — the brain\n 19|//! heartbeat stays LIVE while sessions flood. Against the CURRENT tree (where\n*20|//! `resume_sessions` still subscribes) they go RED (the heartbeat starves). Option A\n*21|//! (resume_sessions does NOT subscribe) turns them GREEN. The observable is the number\n 22|//! of heartbeat round-trips that COMPLETE inside a fixed window — robust to the\n 23|//! std `Mutex`'s unfairness (a slow-but-progressing heartbeat still fails the RED bar,\n 24|//! a truly deadlocked one completes ~zero).\n...\n 27|//! Shares one process-global `SPT_HOME`; spins a REAL broker + REAL flood PTY children.\n*28|//! `cargo nextest run --test brain_resume_conn_deadlock` (CI path) or\n*29|//! `cargo test --test brain_resume_conn_deadlock -- --test-threads=1`. Never a bare\n 30|//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\n 31|//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\n 32|\n...\n 37|\n*38|use interprocess::local_socket::Stream;\n*39|use spt_daemon::brain::Brain;\n 40|use spt_daemon::codec::{read_frame, write_frame};\n 41|use spt_daemon::frame::{Envelope, Role};\n 42|use spt_daemon::msg::{SpawnReq, Spawned, KIND_SPAWN, KIND_SPAWNED};\n...\n 48|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n*49|    format!(\"spt-daemon-resumedeadlock-{}-{}.sock\", std::process::id(), n)\n 50|}\n 51|\n 52|static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n...\n 101|\n*102|fn connect(name: &str) -> Stream {\n 103|    for _ in 0..200 {\n 104|        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n*105|            send_hello(&mut c, Role::Brain).expect(\"hello\");\n 106|            return c;\n 107|        }\n 108|        thread::sleep(Duration::from_millis(10));\n...\n 115|/// child floods the PTY regardless, so the broker's ring keeps advancing and the\n*116|/// brain's viewer subscription keeps receiving live output.\n*117|fn spawn_flood(name: &str, endpoint: &str) -> (u64, u32, Stream) {\n 118|    let mut c = connect(name);\n 119|    write_frame(\n 120|        &mut c,\n...\n 132|\n*133|/// How the probe brain resumes before running the heartbeat: the SHIPPING daemon\n*134|/// path — `CursorOnly` → `resume_session_cursors`, no subscribe. (The hazard path,\n## brain_swap.rs\n*1|//! C0 E2E — the M3c keystone: a **brain-only self-update** swaps the brain logic\n 2|//! through the update engine with **zero hosted-endpoint interruption**\n*3|//! (REQ-UPD-3, absolute for the brain-only class — ADR-0004 §A).\n 4|//!\n*5|//! This rides the same broker/brain substrate the B2 handoff test proves, but\n*6|//! drives the swap through the *update engine* (`plan_update` → `apply_brain_only`)\n*7|//! rather than calling `Brain::handoff` directly — the engine *triggers* a\n 8|//! handoff, it does not re-implement one (`M3c-PLAN.md` §C0, clean-room posture).\n 9|//!\n 10|//! Shape: one echo child runs under a broker PTY for the whole test. The outgoing\n*11|//! brain injects a marker it never reads, then the engine applies a brain-only\n*12|//! update: the new brain re-attaches via the supplied relaunch closure (which\n*13|//! stands for \"exec the verified new binary → `Brain::handoff`\") and must find\n 14|//! that marker in the *replayed* buffer — proving the broker buffered output\n*15|//! produced while no brain was attached. Throughout, the child's pid is unchanged\n 16|//! and exactly one session is hosted: the endpoint never noticed the swap.\n 17|//!\n*18|//! **Filename note:** this file is `brain_swap.rs`, NOT `update.rs`, on purpose.\n 19|//! Windows' installer-detection heuristic (UAC) forces elevation for any\n 20|//! executable whose name contains `update`/`setup`/`install`/`patch`; a test\n 21|//! binary `update-<hash>.exe` then fails to launch with `os error 740` (requires\n...\n 24|//! **REQ-UPD-3 `int` re-pointed (restoration D7-1).** This test proves the\n*25|//! *in-process* brain-only swap shape (the engine's `apply_brain_only` over an\n*26|//! in-thread `Brain::handoff` closure) — valuable engine-mechanics coverage, but\n 27|//! NOT process-level survival. The REQ-UPD-3 `int` evidence (no endpoint drops\n*28|//! across a brain-only update) now lives in `crates/spt/tests/brain_survive.rs`,\n*29|//! which swaps a real on-disk binary and respawns the real `spt daemon brain`\n 30|//! child process (ADR-0018 V5). This file keeps exercising the engine path; it is\n 31|//! no longer the REQ-UPD-3 int tag.\n 32|\n...\n 37|\n*38|use spt_daemon::brain::{Brain, BrainState, BrokerEvent};\n 39|use spt_daemon::msg::SpawnReq;\n*40|use spt_daemon::update::{apply_brain_only, plan_update, BrokerAbi, ReleaseSpec, UpdateClass};\n 41|use spt_daemon::Broker;\n 42|\n 43|static SEQ: AtomicU32 = AtomicU32::new(0);\n...\n 67|\n*68|fn cold_retry(name: &str, now_ms: u64) -> Brain {\n 69|    for _ in 0..300 {\n*70|        if let Ok(b) = Brain::cold_start(name, now_ms) {\n 71|            return b;\n 72|        }\n 73|        thread::sleep(Duration::from_millis(10));\n 74|    }\n*75|    panic!(\"brain cold-start could not connect\");\n 76|}\n 77|\n 78|/// The relaunch closure C0's engine drives: it stands for \"the verified new\n*79|/// binary's brain re-attaches over the existing substrate.\" Retries the connect\n## broker.rs\n 4|//! The child is an echo program (`cat` on Unix, `findstr \".\"` on Windows — the\n*5|//! same cross-OS shape spt-term's injection tests use): bytes the brain injects\n 6|//! round-trip through the real PTY and come back as `output` events. We then\n 7|//! kill it and observe the `exit` event. This exercises the whole B1 contract —\n*8|//! spawn under a broker PTY, stream output, accept input, report exit — through\n 9|//! the actual named-pipe / UDS transport.\n 10|//!\n*11|//! The brain is driven **single-threaded**: send a command, then read frames\n 12|//! until the expected reply/event arrives (accumulating any interleaved output).\n*13|//! interprocess `Stream::split()` shares the underlying fd, so a split client\n*14|//! would only signal EOF once *both* halves drop; keeping one `Stream` means\n 15|//! dropping it closes the fd, the broker's command loop hits EOF, and it tears\n 16|//! the session down deterministically — which we then assert.\n 17|// [int->REQ-DAEMON-2]\n...\n 23|\n*24|use interprocess::local_socket::Stream;\n 25|use spt_daemon::codec::{read_frame, write_frame};\n 26|use spt_daemon::effect::{EffectKey, Minter};\n 27|use spt_daemon::frame::{Envelope, Role};\n...\n 62|\n*63|fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 64|    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 65|}\n 66|\n...\n 70|\n*71|    // The broker accepts one brain and serves it on a handler thread.\n 72|    let server_broker = Arc::clone(&broker);\n 73|    let server = thread::spawn(move || {\n*74|        let handle = server_broker.serve_once().expect(\"accept brain\");\n 75|        handle.join().expect(\"handler thread\");\n 76|    });\n 77|\n*78|    // Brain side: connect (retry briefly in case accept lags the bind).\n 79|    let mut conn = None;\n 80|    for _ in 0..200 {\n 81|        match LocalSocketTransport::connect(&name) {\n...\n 93|\n*94|    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n 95|\n 96|    // Spawn the echo child under a broker PTY; read until the `spawned` reply,\n 97|    // accumulating any output that interleaves.\n...\n 159|\n*160|    // Disconnect: dropping the single Stream closes the fd, so the broker's\n 161|    // command loop hits EOF and tears the session down. Joining the handler\n 162|    // (via the server thread) lets us assert the teardown happened.\n 163|    drop(conn);\n...\n 181|#[test]\n*182|fn broker_hosts_pty_child_streams_output_and_accepts_input() {\n 183|    main_test();\n 184|}\n 185|\n...\n 203|    let server = thread::spawn(move || {\n*204|        let handle = server_broker.serve_once().expect(\"accept brain\");\n 205|        handle.join().expect(\"handler thread\");\n 206|    });\n 207|\n...\n 218|    let mut conn = conn.expect(\"connect to broker\");\n*219|    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n 220|\n 221|    // Spawn the echo child LABELLED with an endpoint id (the spawn.endpoint the\n 222|    // broker tracks per session — what KIND_ENDPOINT_INPUT resolves against).\n...\n 327|    let server = thread::spawn(move || {\n*328|        let handle = server_broker.serve_once().expect(\"accept brain\");\n 329|        handle.join().expect(\"handler thread\");\n 330|    });\n 331|    let mut conn = None;\n...\n 341|    let mut conn = conn.expect(\"connect to broker\");\n*342|    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n 343|\n 344|    // A child that prints the injected env var, cross-OS.\n 345|    #[cfg(unix)]\n...\n 398|            Err(_) => {\n*399|                // After EXIT a run of read errors means the stream is fully drained\n 400|                // (no trailing output is coming) — stop. Before EXIT the frame just\n 401|                // isn't here yet, so wait and retry.\n 402|                if exited {\n...\n 450|    let mut conn = conn.expect(\"connect to broker\");\n*451|    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n 452|\n 453|    // A child that exits immediately.\n 454|    #[cfg(unix)]\n...\n 565|        let mut c = conn.expect(\"connect to broker\");\n*566|        send_hello(&mut c, Role::Brain).expect(\"send hello\");\n 567|        c\n 568|    };\n 569|\n## budget.rs\n 42|\n*43|use spt_daemon::brain::Brain;\n 44|use spt_daemon::broker::Broker;\n 45|use spt_daemon::msg::SpawnReq;\n 46|use tempfile::TempDir;\n...\n 79|\n*80|    // One probe brain per seat (kill_session targets the brain's own session).\n*81|    let mut seats: Vec<(Brain, u64, u32)> = Vec::new(); // (brain, session, child pid)\n 82|    for i in 0..n {\n*83|        let mut brain = connect_retry(&name);\n 84|        let req = SpawnReq {\n 85|            program: program.clone(),\n 86|            args: args.clone(),\n...\n 95|        };\n*96|        let (session, pid) = brain.spawn_session_pid(req).expect(\"spawn seat\");\n 97|        let pid = pid.expect(\"seat child pid\");\n*98|        seats.push((brain, session, pid));\n 99|    }\n 100|\n 101|    thread::sleep(settle);\n...\n 155|    // would remain in production). The residual is the daemon baseline.\n*156|    for (brain, _, _) in seats.iter_mut() {\n*157|        let _ = brain.kill_session();\n 158|    }\n 159|    thread::sleep(Duration::from_millis(1_500));\n 160|    let self_after = sample_pid(std::process::id()).unwrap_or_default();\n...\n 204|\n*205|fn connect_retry(name: &str) -> Brain {\n 206|    for _ in 0..300 {\n*207|        if let Ok(b) = Brain::cold_start(name, 1) {\n 208|            return b;\n 209|        }\n 210|        thread::sleep(Duration::from_millis(10));\n 211|    }\n*212|    panic!(\"brain could not connect\");\n 213|}\n 214|\n 215|/// Sample one process: RSS bytes, handle/fd count, cumulative CPU seconds.\n## conn_blackhole_lifecycle.rs\n 42|//!\n*43|//! ## Relationship to `brain_decouple.rs`\n 44|//!\n*45|//! This harness GENERALIZES `brain_decouple.rs`'s r4 gate\n 46|//! (`non_draining_controller_stall_evict_releases_writer_and_connection`, the\n 47|//! Windows-mandatory physical-retirement leg for\n 48|//! REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, which stays the primary\n 49|//! gate for that hazard). It reuses the same SEED/trigger/burst choreography,\n*50|//! the hand-back-the-unread-Stream pattern, the `conn_handler_count` and\n 51|//! `stall_evict_count` seams, the old-client-write-fails probe, and the\n 52|//! teardown-before-assert discipline. It ADDS the two pieces the r4 gate does\n 53|//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n...\n 70|//!\n*71|//! Shares ONE process-global `SPT_HOME` + `SPT_BRAIN_WRITE_DEADLINE_MS`, spins a\n 72|//! REAL broker + TWO PTY children. HEAVY class. Run isolated with\n 73|//! `cargo nextest run -p spt-daemon --test conn_blackhole_lifecycle` (CI's path)\n 74|//! or `cargo test --test conn_blackhole_lifecycle -- --test-threads=1`. NEVER a\n...\n 85|\n*86|use interprocess::local_socket::Stream;\n 87|use spt_daemon::codec::{read_frame, write_frame};\n 88|use spt_daemon::frame::{Envelope, Role};\n 89|use spt_daemon::msg::{\n...\n 119|\n*120|fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 121|    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 122|}\n 123|\n*124|fn connect(name: &str) -> Stream {\n 125|    for _ in 0..200 {\n 126|        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n*127|            send_hello(&mut c, Role::Brain).expect(\"hello\");\n 128|            return c;\n 129|        }\n 130|        thread::sleep(Duration::from_millis(10));\n...\n 135|/// A child that FLOODS stdout forever (never reads stdin) — drives the UNRELATED\n*136|/// session so its viewer has a continuous frame stream to keep ticking on\n 137|/// throughout the black-hole incident (invariant 1).\n 138|fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139|    SpawnReq {\n...\n 160|/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n*161|/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162|fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n 163|    SpawnReq {\n 164|        program: \"powershell\".to_string(),\n...\n 183|/// Spawn a PTY child from a fresh conn and return `(spawn_conn, session_id)`.\n*184|fn spawn_session(name: &str, req: SpawnReq) -> (Stream, u64) {\n 185|    let mut c = connect(name);\n 186|    send(&mut c, KIND_SPAWN, serde_json::to_value(req).unwrap());\n 187|    let sid = loop {\n...\n 223|/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n*224|/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225|/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226|fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n 227|    let name = name.to_string();\n...\n 250|    init_home();\n*251|    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 252|    // instead of 15 s — the structural outcome, fast + deterministic (the knob\n 253|    // grants wall-clock only; it never relaxes an assertion).\n*254|    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n 255|\n 256|    let name = unique_name();\n 257|    let broker = Broker::bind(&name).expect(\"bind broker\");\n...\n 292|    // ── BLACK-HOLED session B: the real SEED+burst PTY child from conn A, then\n*293|    //    DROP A so the ONLY persistent brain conn is the black-holed controller R. ──\n 294|    let (a, sid_b) = spawn_session(&name, seed_burst_spawn_req(\"blackhole-victim-ep\"));\n 295|    drop(a);\n 296|\n...\n 300|    //    drains a few burst frames (a real seq>0 frame delivered), and STOPS\n*301|    //    reading — handing the Stream back to main UNREAD. Main holds it, never\n 302|    //    draining, so the broker's R-writer parks in `write_frame` exactly as\n*303|    //    against a suspended brain. R's Stream is NEVER dropped until the very end. ──\n 304|    let r_name = name.clone();\n 305|    let (seed_tx, seed_rx) = std::sync::mpsc::channel::<(bool, u64)>();\n 306|    let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();\n*307|    let (hand_tx, hand_rx) = std::sync::mpsc::channel::<(Stream, u64)>();\n 308|    thread::spawn(move || {\n 309|        let mut r = connect(&r_name);\n 310|        send(\n...\n 386|        }\n*387|        // Hand the Stream back to main UNREAD (main holds it = the black-holed\n 388|        // client). The broker's R-writer stays wedged on the remaining burst.\n 389|        let _ = hand_tx.send((r, max_seq));\n 390|    });\n...\n 398|    let _ = go_tx.send(());\n*399|    // Recv the handed-back black-holed Stream (held UNREAD for the rest of the test).\n 400|    let handed = hand_rx.recv_timeout(Duration::from_secs(25)).ok();\n## control_stamp_lifetime.rs\n 11|//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n*12|//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n 13|//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n 14|//! path dropped the OutputLog with the controller slot still occupied, so the\n 15|//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\n...\n 48|\n*49|use spt_daemon::brain::Brain;\n 50|use spt_daemon::msg::SpawnReq;\n 51|use spt_daemon::Broker;\n 52|\n...\n 89|\n*90|fn connect_brain(name: &str) -> Brain {\n 91|    for _ in 0..300 {\n*92|        if let Ok(b) = Brain::cold_start(name, 1) {\n 93|            return b;\n 94|        }\n 95|        thread::sleep(Duration::from_millis(10));\n 96|    }\n*97|    panic!(\"brain could not connect to {name}\");\n 98|}\n 99|\n 100|/// A long-lived stdin-reading PTY child so the spawned session stays LIVE until we\n...\n 145|    let broker = start_broker(&name);\n*146|    let mut spawner = connect_brain(&name);\n 147|    let sid = spawner\n 148|        .spawn_session(idle_spawn_req(&id))\n 149|        .expect(\"spawn live session for the endpoint\");\n...\n 183|    // ── Kill the PTY CHILD → the exit-waiter reaps the session (NOT a conn drop, so\n*184|    //    the controller-detach path never runs; only the reap path can unlatch). ──\n 185|    let pid = broker\n 186|        .session_pid(sid)\n 187|        .expect(\"live session must have a pid before kill\");\n...\n 247|    let broker = start_broker(&name);\n*248|    let mut spawner = connect_brain(&name);\n 249|    let sid = spawner\n 250|        .spawn_session(idle_spawn_req(&id))\n 251|        .expect(\"spawn live session before its perch exists\");\n...\n 266|    for _ in 0..50 {\n*267|        let mut brain = connect_brain(&name);\n*268|        let _ = brain.sessions().expect(\"KIND_SESSIONS reply\");\n 269|        thread::sleep(Duration::from_millis(50));\n 270|        if spt_store::info::read_info(&self_perch)\n 271|            .map(|i| i.controlled)\n...\n 304|// + `driven_by` on a spt-hosted online live_agent perch AFTER the harness exited,\n*305|// persisting >20min AND ACROSS A DAEMON RESTART. This is the DIFFERENT reap leg\n 306|// from `reap_clears_control_and_viewer_stamps` above (that drives the broker\n 307|// exit-waiter's `stamp_reaped`); B3 is the `reconcile_hosted_liveness` sweep — the\n 308|// tick that clears a sticky stamp on a perch whose broker session is simply GONE,\n*309|// regardless of status, so an already-offline perch left sticky across a restart is\n 310|// swept (path iv, the boot sweep), not only on the online→offline transition.\n 311|//\n 312|// This int drives the REAL reconcile entry (`spt_daemon::livehost::reconcile_hosted_liveness`)\n 313|// against the REAL on-disk owlery, with the live-session set DERIVED FROM A REAL\n*314|// BROKER (query_live_session_endpoints's exact shape: `brain.sessions()` →\n 315|// endpoint ids). A decoy live session keeps the broker set genuinely NON-empty, so\n 316|// the reap fires precisely because the broker reports NO session for the sticky\n 317|// perch — not because we hand it an empty set. Two arms:\n...\n 319|//   (B) already-OFFLINE + sticky → cleared REGARDLESS of status (the boot sweep; a\n*320|//       restarted daemon must not resurrect the stamps).\n 321|// [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n 322|\n 323|/// Seed a spt-hosted (`controllable=Some(true)`) live_agent self-perch at `status`,\n...\n 351|    let online_id = format!(\"b3-online-{pid}\");\n*352|    // Arm B: already-OFFLINE + sticky (survived a daemon restart — the boot sweep).\n 353|    let offline_id = format!(\"b3-boot-{pid}\");\n 354|    let online_perch = seed_sticky_hosted_perch(&online_id, spt_store::liveness::STATUS_ONLINE, node);\n 355|    let offline_perch =\n...\n 364|    let broker = start_broker(&name);\n*365|    let mut spawner = connect_brain(&name);\n 366|    let decoy_sid = spawner\n 367|        .spawn_session(idle_spawn_req(&decoy_id))\n 368|        .expect(\"spawn the decoy live session\");\n...\n 372|    // `query_live_session_endpoints`: KIND_SESSIONS reply → endpoint ids.\n*373|    let mut probe = connect_brain(&name);\n 374|    let reply = probe.sessions().expect(\"KIND_SESSIONS reply\");\n 375|    let live: BTreeSet<String> = reply\n 376|        .sessions\n...\n 428|    // of status. A naive reap gated on the online→offline transition would skip this\n*429|    // (the perri F-b bug: >20min + across a daemon restart). Presence stays OFFLINE.\n 430|    // [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n 431|    assert!(\n 432|        !after_offline.controlled,\n*433|        \"B3 arm B (boot sweep): a restarted daemon must clear a sticky controlled on an \\\n 434|         ALREADY-OFFLINE perch — not resurrect it\"\n 435|    );\n 436|    assert_eq!(\n## daemon_e2e.rs\n 2|//! architecture through the real IPC in one flow: a daemon hosts a LiveAgent's\n*3|//! lifecycle as in-process loops **and** survives a brain restart with its hosted\n*4|//! PTY child + output stream intact and gapless.\n 5|//!\n 6|//! Flow (the M3b done-definition, ADR-0004 / `M3b-PLAN.md` §B9):\n 7|//!\n*8|//! 1. **spawn** — a [`Broker`] hosts a real PTY child; a [`Brain`] cold-starts,\n 9|//!    spawns the session, and round-trips a marker through it (the session is live).\n*10|//! 2. **Psyche-loop** — a [`BrainLifecycle`] (the consolidated re-host of the\n 11|//!    interim `api listen` live half, [`REQ-DAEMON-1`]) spawns the Psyche companion;\n 12|//!    the daemon marks the hosted perch `online`, so its liveness is the daemon\n 13|//!    `status`, never the summarizer pid ([`REQ-HAZARD-DAEMON-HOSTED-LIVENESS`]).\n...\n 15|//!    tick folds it in (the daemon-hosted lifecycle loop, [`REQ-DAEMON-1`]).\n*16|//! 4. **brain-restart-survives** — the brain is dropped (a crash) and handed off\n 17|//!    repeatedly; across each handoff the child's pid is unchanged, exactly one\n*18|//!    session is hosted, the output stream replays gaplessly, and `gen_start`\n 19|//!    advances ([`REQ-DAEMON-2`] / [`REQ-HAZARD-GEN-START-NOW`]). The lifecycle is\n*20|//!    stateless on disk, so it keeps ingesting communes straight across the restart.\n 21|//! 5. **graceful-signoff** — the lifecycle's graceful teardown runs the load-bearing\n 22|//!    ordering — echo-commune BEFORE teardown (3.3) — and removes the Self's ready\n 23|//!    marker ([`REQ-DAEMON-4`]).\n...\n 27|//! echoes them, exactly as B8 documents (bare `\\n` echoes a lone `\\r` and collapses\n*28|//! boundaries — the Spike #4/#5 PTY stream-contract gotcha).\n 29|//!\n 30|//! Single `#[test]` on purpose: the flow owns the process-global `SPT_HOME`, so it\n 31|//! must not race a sibling test in this binary.\n*32|// REQ-DAEMON-1 `int` re-pointed to crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs\n*33|// (M11-W0.4 (i)): the REAL brain PROCESS (run_brain) hosts the Psyche, retiring\n*34|// this file's type-level BrainLifecycle::with_config-direct over-claim (it kept\n 35|// green coverage over an UNWIRED production path — the M3-era false-green). This\n 36|// file keeps its REQ-DAEMON-4 + hazard int evidence (the lifecycle machinery +\n 37|// daemon-hosted-liveness), which it still exercises directly.\n*38|// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration\n 39|// D7-1): the M3b-B9 flow proves the daemon-hosted lifecycle + an *in-process*\n*40|// brain-restart survival, not the process-level swap. Process-level endpoint\n*41|// survival across a brain-PROCESS restart onto a swapped binary is the D7-1 E2E.\n 42|// [int->REQ-DAEMON-4]\n 43|// [int->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n 44|\n...\n 49|\n*50|use spt_daemon::brain::BrainState;\n 51|use spt_daemon::msg::SpawnReq;\n*52|use spt_daemon::{Brain, BrainLifecycle, Broker, DaemonConfig};\n 53|use spt_runtime::Manifest;\n 54|use spt_store::perch::{self, ParentHint};\n 55|\n...\n 108|\n*109|fn cold_retry(name: &str, now_ms: u64) -> Brain {\n 110|    for _ in 0..300 {\n*111|        if let Ok(b) = Brain::cold_start(name, now_ms) {\n 112|            return b;\n 113|        }\n 114|        thread::sleep(Duration::from_millis(10));\n 115|    }\n*116|    panic!(\"brain cold-start could not connect\");\n 117|}\n 118|\n*119|fn handoff_retry(name: &str, prior: BrainState, now_ms: u64) -> Brain {\n 120|    for _ in 0..300 {\n## daemon_lifecycle_real_brain.rs\n 2|//! `[int->REQ-DAEMON-1]` evidence OFF the type-level `daemon_e2e` (which\n*3|//! constructs a [`BrainLifecycle`] directly — green coverage over an UNWIRED\n*4|//! production path, the M3-era false-green) ONTO the actual brain entry point:\n 5|//!\n*6|//! a [`Broker`] + `brainproc::run_brain` (the real brain's `main`-equivalent,\n 7|//! which since M11-W0.1 runs `livehost::spawn_live_host`) HOSTS a real ONLINE live\n 8|//! endpoint and DRIVES its pulse loop — proven by the loop ingesting a commune into\n*9|//! the live-context tier. The reconcile that hosts is the REAL brain's, not an in-\n*10|//! test `reconcile_once` call. A cold `run_brain` over an already-online perch IS\n*11|//! the post-brain-restart rehost: the perch's `status=online` survives the brain\n*12|//! process, and the fresh brain re-hosts the lifecycle at boot (daemon-authoritative\n 13|//! liveness, CONTEXT:194).\n 14|//!\n 15|//! **F-030 W1:** the Psyche is now a PER-EVENT TURN, not a resident process, so this\n 16|//! no longer waits for a resident `<id>-psyche` perch to bind (host_one spawns none)\n*17|//! and reaps no detached child. The DAEMON-1 evidence is the hosted pulse loop\n 18|//! ingesting the commune — unchanged and independent of the retired resident spawn.\n 19|//!\n 20|//! Single `#[test]`: it owns the process-global `SPT_HOME`.\n...\n 26|\n*27|use spt_daemon::brainproc::{run_brain, StartReason};\n 28|use spt_daemon::endpoint::broker_socket_name;\n 29|use spt_daemon::Broker;\n 30|use spt_store::liveness::STATUS_ONLINE;\n...\n 33|// A no-op `psyche_init` command. F-030 W1 host_one spawns NO resident psyche, but\n*34|// `BrainLifecycle::with_config_in` still REQUIRES the manifest to declare the role,\n 35|// so it stays present (never spawned). The pulse loop drives regardless — commune\n 36|// ingest is independent of any psyche process.\n 37|#[cfg(windows)]\n...\n 42|#[test]\n*43|fn real_brain_process_hosts_the_psyche_for_an_online_live_endpoint() {\n 44|    let home = tempfile::tempdir().unwrap();\n 45|    std::env::set_var(\"SPT_HOME\", home.path());\n 46|\n*47|    // A live-capable adapter, registered so the brain resolves the manifest from\n 48|    // disk by the perch's adapter (the production resolve path).\n 49|    let drops = home.path().join(\"drops\");\n 50|    std::fs::create_dir_all(&drops).unwrap();\n...\n 64|    // establish→online path is proven by live_firsthost/live_bind ints; here the\n*65|    // focus is the REAL brain hosting it). status=online survives the brain.\n 66|    let id = \"agent9\";\n 67|    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 68|    std::fs::create_dir_all(&perch_path).unwrap();\n...\n 73|\n*74|    // ── the REAL brain: a Broker + brainproc::run_brain on a thread (NOT a direct\n*75|    // BrainLifecycle::with_config). run_brain runs spawn_live_host, so the reconcile\n*76|    // hosting the Psyche is the real brain's, at boot.\n 77|    let broker = Broker::bind(&broker_socket_name()).expect(\"bind broker\");\n 78|    {\n 79|        let serve = Arc::clone(&broker);\n...\n 84|    thread::spawn(|| {\n*85|        let _ = run_brain(0, StartReason::Cold);\n 86|    });\n 87|\n*88|    // The real brain's pulse loop runs (F-030 W1: NO resident psyche is spawned —\n 89|    // the DAEMON-1 evidence is the hosted loop ticking, not a bound `<id>-psyche`\n 90|    // perch): a commune drop is ingested into the live context tier (proves the loop\n*91|    // ticks in the brain, not a test-driven tick).\n 92|    std::fs::write(\n 93|        drops.join(format!(\"{id}-commune.md\")),\n*94|        \"Source: self-commune\\nreal-brain note\",\n 95|    )\n 96|    .unwrap();\n 97|    let ctx = spt_store::contextstore::live_context_file(&home.path().join(\"tracked\"), id);\n## digest.rs\n 13|//!   (impossible under the superseded byte-parse shape);\n*14|//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows\n 15|//!   delivers the structured delta to a subscriber (the push-driven freshness\n 16|//!   the `api digest-entry` path also drives — autonomous file-watch is deferred\n 17|//!   to the consuming frontend milestone).\n...\n 37|/// A stdin pass-through \"extractor\": the source bytes (piped in) round-trip to\n*38|/// stdout unchanged, so the fixture JSONL *is* the contract-record stream.\n 39|#[cfg(unix)]\n 40|const PASSTHROUGH: &str = \"cat\";\n 41|#[cfg(windows)]\n...\n 130|\n*131|    // ── Delta-stream: a reproject after the log grows delivers a delta ───────\n 132|    let fixture2 = home.path().join(\"cc2-session.jsonl\");\n 133|    std::fs::write(&fixture2, \"{\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"first\\\"}\\n\").unwrap();\n 134|    establish_cc_endpoint(\"cc2\", &fixture2);\n## dispatch.rs\n 1|//! Inbound dispatch E2E (D9-1-1): the serve half of every wire protocol runs\n*2|//! **undriven** — no probe brain hand-wires `serve_*`; the daemon-side\n*3|//! dispatcher claims each peer-initiated stream, classifies it by first\n 4|//! record, and serves it. This is the production behavior the D5–D8 E2E\n 5|//! suites deferred (\"the daemon dispatch\").\n 6|//!\n...\n 17|\n*18|use spt_daemon::brain::Brain;\n 19|use spt_daemon::broker::Broker;\n 20|use spt_daemon::effect::{Minter, MintedOp};\n 21|use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};\n...\n 84|\n*85|fn connect_retry(name: &str) -> Brain {\n 86|    for _ in 0..300 {\n*87|        if let Ok(b) = Brain::cold_start(name, 1) {\n 88|            return b;\n 89|        }\n 90|        thread::sleep(Duration::from_millis(10));\n 91|    }\n*92|    panic!(\"brain could not connect\");\n 93|}\n 94|\n 95|/// Stand B's dispatcher up exactly as the daemon does, returning its stop\n...\n 132|        last_seen: String::new(),\n*133|        lease_epoch: 1,\n 134|    });\n 135|    roster.save().expect(\"save roster\");\n 136|    let mut vis = VisibilityStore::load();\n...\n 142|\n*143|fn node_hex(brain: &mut Brain) -> String {\n*144|    brain\n 145|        .net_status()\n 146|        .expect(\"status\")\n 147|        .node_id_hex\n...\n 230|        artifact_sha256: sha256_hex(&artifact),\n*231|        brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,\n 232|        broker_resource_abi: BROKER_RESOURCE_ABI,\n 233|        product_version: String::new(),\n 234|    };\n...\n 316|    let conn = a.net_dial(addr, Some(MintedOp::new(Minter::Cli, op()))).expect(\"dial\");\n*317|    let opened = a.net_open_stream(conn.conn_id, Some(MintedOp::new(Minter::Cli, op()))).expect(\"open\");\n*318|    a.net_stream_send(opened.stream_id, &line, None, true)\n 319|        .expect(\"send feed\");\n 320|\n 321|    // The dispatcher applies asynchronously — poll B's canonical store.\n...\n 343|// and the direction fact is recorded right: A's table shows its opened\n*344|// stream as locally-initiated; B's shows the same stream as peer-initiated\n 345|// (what the dispatcher claimed it by).\n 346|#[test]\n 347|fn dispatcher_funnels_wan_messages_undriven() {\n...\n 367|    let conn = a.net_dial(addr, Some(MintedOp::new(Minter::Cli, op()))).expect(\"dial\");\n*368|    let opened = a.net_open_stream(conn.conn_id, Some(MintedOp::new(Minter::Cli, op()))).expect(\"open\");\n 369|    let msg = WanMessage {\n 370|        target: target.to_string(),\n 371|        from: \"doyle\".to_string(),\n...\n 374|    };\n*375|    a.net_stream_send(\n*376|        opened.stream_id,\n 377|        &spt_net::net::ndjson::encode_line(&msg),\n 378|        None,\n 379|        true,\n...\n 382|\n*383|    // Direction fact, both sides of the same stream.\n*384|    let a_rows = a.net_streams().expect(\"A streams\").streams;\n 385|    assert!(\n 386|        a_rows\n 387|            .iter()\n*388|            .any(|s| s.stream_id == opened.stream_id && s.initiated_locally),\n 389|        \"A's own open is locally-initiated in A's table\"\n 390|    );\n*391|    let b_rows = b_probe.net_streams().expect(\"B streams\").streams;\n 392|    assert!(\n 393|        b_rows.iter().all(|s| !s.initiated_locally),\n 394|        \"every row at B is peer-initiated (B opened nothing)\"\n## docs_server_e2e.rs\n 8|//!\n*9|//! Hermetic: port 0 (ephemeral), raw std TcpStream client — no HTTP client\n 10|//! dev-dep. The server thread is the production `docshost::start`.\n 11|//\n 12|// [int->REQ-DOCS-LOCAL-SERVER]\n...\n 14|use std::io::{Read, Write};\n*15|use std::net::TcpStream;\n 16|use std::path::Path;\n 17|\n 18|/// Minimal HTTP/1.1 request over loopback; returns (status-line, headers,\n...\n 20|fn http(port: u16, request_line: &str) -> (String, String, Vec<u8>) {\n*21|    let mut stream = TcpStream::connect((\"127.0.0.1\", port)).expect(\"connect docs server\");\n 22|    write!(\n*23|        stream,\n 24|        \"{request_line}\\r\\nHost: localhost:{port}\\r\\nConnection: close\\r\\n\\r\\n\"\n 25|    )\n 26|    .unwrap();\n 27|    let mut raw = Vec::new();\n*28|    stream.read_to_end(&mut raw).expect(\"read response\");\n 29|    let split = raw\n 30|        .windows(4)\n 31|        .position(|w| w == b\"\\r\\n\\r\\n\")\n## driven_by_selfheal.rs\n 6|//! An spt-hosted endpoint can latch `ONLINE+CONTROLLED` — `info.json`\n*7|//! `driven_by = Some(node)` — and never clear it when a controller's detach is\n 8|//! LOST (no clean disconnect, and no output to trigger the W1 drain-evict\n 9|//! `mark_controller_gone`). The picker then renders a phantom \"controlled by X\"\n 10|//! forever. `reconcile_hosted_liveness` (`livehost.rs`) is the pull-primary\n...\n 35|//!   an idle session whose ABANDONED REMOTE controller wedged without a clean\n*36|//!   detach — keeps `controller_by==Some(origin)`, which a reconcile read can't\n 37|//!   detect at all. That case is its own deferred requirement,\n 38|//!   `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT` (broker-side controller eviction —\n 39|//!   NOT a reconcile `controller_by` clear). See `inject_control_wedge.rs` `w5_a2`\n...\n 52|//! It also captures the broker's OWN self-heal on an OBSERVED controller\n*53|//! disconnect (per-conn `detach_if`→`clear_controller`→`stamp_driven_by`).\n 54|//!\n 55|//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n 56|//!\n...\n 73|\n*74|use spt_daemon::brain::Brain;\n 75|use spt_daemon::msg::SpawnReq;\n 76|use spt_daemon::Broker;\n 77|use spt_daemon::livehost::reconcile_hosted_liveness;\n...\n 120|\n*121|/// Connect a Brain to the broker with bounded retry (cold_start — the request-reply\n*122|/// carrier `Brain::sessions()` rides; KIND_SESSIONS is a single reply, no streaming,\n 123|/// so a plain Whole conn is fine here).\n*124|fn connect_brain(name: &str) -> Brain {\n 125|    for _ in 0..300 {\n*126|        if let Ok(b) = Brain::cold_start(name, 1) {\n 127|            return b;\n 128|        }\n 129|        thread::sleep(Duration::from_millis(10));\n 130|    }\n*131|    panic!(\"brain could not connect to {name}\");\n 132|}\n 133|\n 134|/// A trivial long-lived PTY child so a spawned session stays LIVE (Gap A needs a\n...\n 156|/// spt-hosted (`controllable=Some(true)`) `live_agent` self-perch, `status=online`,\n*157|/// pre-stamped `driven_by=Some(node)` (the lost-detach latch). Mirrors the\n 158|/// livehost-bootgate-e2e stamping exactly, plus the `driven_by` marker.\n 159|fn stamp_latched_perch(id: &str, node: &str) -> std::path::PathBuf {\n 160|    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n...\n 166|    spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_ONLINE).unwrap();\n*167|    // The lost-detach latch the self-heal must clear.\n 168|    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\n 169|    self_perch\n 170|}\n...\n 191|    let _broker = start_broker(&name);\n*192|    let mut brain = connect_brain(&name);\n*193|    let reply = brain.sessions().expect(\"KIND_SESSIONS reply\");\n 194|    let live_sessions: BTreeSet<String> =\n 195|        reply.sessions.iter().map(|s| s.endpoint.clone()).collect();\n 196|\n...\n 270|    // ── A REAL broker hosting a LIVE session for THIS endpoint. The spawning\n*271|    //    brain is pre-attached as the LOCAL controller (dispatch_spawn →\n 272|    //    become_controller(by=None)). The PTY child keeps the session live in the\n 273|    //    broker table. ──\n 274|    let name = unique_name();\n 275|    let broker = start_broker(&name);\n*276|    let mut spawner = connect_brain(&name);\n 277|    let sid = spawner\n 278|        .spawn_session(idle_spawn_req(&id))\n 279|        .expect(\"spawn live session for the endpoint\");\n...\n 281|    // FINDING (the broker's OWN self-heal on a controller disconnect): dropping the\n*282|    // spawner socket makes the broker's per-conn handler run `detach_if` →\n 283|    // `clear_controller` → `stamp_driven_by`, which (since controller_by() is now\n 284|    // None) RE-STAMPS the perch's driven_by to None. So an OBSERVED controller\n 285|    // disconnect already unlatches via the broker single-writer — even without a\n*286|    // clean detach. The Gap-A the W5 RECONCILE must heal is the path where that\n*287|    // re-stamp NEVER fires: a brain restart re-adopts the live session but the\n 288|    // in-memory controller (and thus the clear_controller re-stamp) never existed,\n 289|    // so the STALE on-disk driven_by marker persists against a controller_by==None\n 290|    // session. We reproduce exactly THAT state below.\n...\n 300|         driven_by_after_spawner_drop={driven_by_after_disconnect:?} \\\n*301|         (None = the broker's detach_if→clear_controller→stamp_driven_by already \\\n 302|         unlatched the OBSERVED disconnect — so the RECONCILE Gap-A is only the \\\n*303|         NEVER-RE-STAMPED brain-restart leftover, reproduced next) ===\"\n 304|    );\n 305|\n## endpoint_survival.rs\n*1|//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) — daemon-restart endpoint\n 2|//! survival, against a REAL broker over the real local IPC.\n 3|//!\n 4|//! ## The bug this gate locks down (seed #6, the field-accept blocker)\n...\n 8|//! though their records survived (info.json status + adapter + cwd + a ledger row). The\n*9|//! fix: `livehost::resume_restart_orphaned_endpoints` runs ONCE at the fresh broker's\n 10|//! live-host start and RE-RUNS the harness of every previously-ONLINE spt-hosted\n*11|//! endpoint the restart orphaned, resuming from its last ledger session.\n 12|//!\n 13|//! ## What this rig proves\n 14|//!\n...\n 18|//! session. RED-first: WITHOUT the pass the broker has no session for it (it stays\n*19|//! offline). The two belts are unit-proven in `livehost::restart_resume_gate_*`; this\n 20|//! rig proves the end-to-end re-run against the real broker + a real spawned harness.\n 21|//!\n 22|//! ## RUNNER CONTRACT — process-per-test (nextest) or single-threaded\n...\n 30|\n*31|use spt_daemon::brain::now_ms;\n*32|use spt_daemon::{Brain, Broker};\n 33|\n*34|/// A long-lived, quiet harness the `[session.self]` resume spawns — it stays running\n 35|/// (so the broker session persists for the assertion) and produces no output.\n 36|#[cfg(windows)]\n 37|const HARNESS_CMD: &str = \"cmd /c ping -n 600 127.0.0.1 >nul\";\n...\n 40|\n*41|/// Register a live-capable adapter whose `[session.self]` (the is_resume fallback when\n*42|/// no `[session.resume]`) spawns the long-lived quiet harness, so\n 43|/// `launch_harness_brokered_in` has a real program to run.\n*44|fn register_resume_adapter(name: &str) {\n 45|    let toml = format!(\n 46|        \"[adapter]\\nname=\\\"{name}\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\n 47|         min_spt_core_version=\\\"0\\\"\\n\\n\\\n...\n 53|    spt_runtime::registry::register(&spt_store::perch::adapters_dir(), &src, 1000)\n*54|        .expect(\"register the resume adapter\");\n 55|}\n 56|\n 57|/// Seed a previously-ONLINE, spt-hosted (controllable), relay-less endpoint with a\n*58|/// ledger row to resume from — the daemon-restart-orphaned shape.\n 59|fn seed_orphaned_online_endpoint(id: &str, adapter: &str) {\n\n[Showing lines 1-1080 of 1452 (49.9KB limit). Use :1081 to continue]"}],"details":{"scopePath":"crates/spt-daemon/tests, crates/spt/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":358,"fileCount":20,"files":["crates/spt-daemon/tests/access.rs","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/brain_decouple.rs","crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","crates/spt-daemon/tests/brain_swap.rs","crates/spt-daemon/tests/broker.rs","crates/spt-daemon/tests/budget.rs","crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","crates/spt-daemon/tests/control_stamp_lifetime.rs","crates/spt-daemon/tests/daemon_e2e.rs","crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs","crates/spt-daemon/tests/digest.rs","crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/tests/docs_server_e2e.rs","crates/spt-daemon/tests/driven_by_selfheal.rs","crates/spt-daemon/tests/endpoint_survival.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt-daemon/tests/handoff.rs","crates/spt-daemon/tests/idempotent.rs","crates/spt-daemon/tests/inject_control_wedge.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/access.rs","count":20},{"path":"crates/spt-daemon/tests/attach.rs","count":20},{"path":"crates/spt-daemon/tests/brain_decouple.rs","count":20},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","count":20},{"path":"crates/spt-daemon/tests/brain_swap.rs","count":20},{"path":"crates/spt-daemon/tests/broker.rs","count":20},{"path":"crates/spt-daemon/tests/budget.rs","count":11},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","count":20},{"path":"crates/spt-daemon/tests/control_stamp_lifetime.rs","count":19},{"path":"crates/spt-daemon/tests/daemon_e2e.rs","count":20},{"path":"crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs","count":20},{"path":"crates/spt-daemon/tests/digest.rs","count":3},{"path":"crates/spt-daemon/tests/dispatch.rs","count":20},{"path":"crates/spt-daemon/tests/docs_server_e2e.rs","count":5},{"path":"crates/spt-daemon/tests/driven_by_selfheal.rs","count":20},{"path":"crates/spt-daemon/tests/endpoint_survival.rs","count":20},{"path":"crates/spt-daemon/tests/false_promote.rs","count":20},{"path":"crates/spt-daemon/tests/handoff.rs","count":20},{"path":"crates/spt-daemon/tests/idempotent.rs","count":20},{"path":"crates/spt-daemon/tests/inject_control_wedge.rs","count":20}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/tests/\n## access.rs\n   4│//! reply (stateful firewall), and admits a whitelisted node — with edits to\n  *5│//! the store taking effect mid-stream (the gate re-reads per check).\n   6│//!\n   7│//! Two in-process brokers (hermetic loopback endpoints) play the two daemons,\n   8│//! the same harness as `wanmsg.rs`. The payload-origin spoof negative lives\n    │...\n  18│use spt_daemon::access::note_outbound;\n *19│use spt_daemon::brain::{Brain, BrokerEvent};\n  20│use spt_daemon::effect::{Minter, MintedOp};\n  21│use spt_daemon::nethost::{NetConfig, NetHost};\n  22│use spt_daemon::registryhost::RegistryHost;\n    │...\n  73│\n *74│fn connect_retry(name: &str) -> Brain {\n  75│    for _ in 0..300 {\n *76│        if let Ok(b) = Brain::cold_start(name, 1) {\n  77│            return b;\n  78│        }\n  79│        thread::sleep(Duration::from_millis(10));\n  80│    }\n *81│    panic!(\"brain could not connect\");\n  82│}\n  83│\n *84│fn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n  85│    for _ in 0..400 {\n *86│        let reply = brain.net_streams().expect(\"net-streams\");\n *87│        if let Some(s) = reply.streams.first() {\n *88│            return (s.stream_id, s.remote_id_hex.clone());\n  89│        }\n  90│        thread::sleep(Duration::from_millis(5));\n  91│    }\n *92│    panic!(\"stream never appeared at the receiver's broker\");\n  93│}\n  94│\n  95│fn offline_perch(id: &str) -> std::path::PathBuf {\n    │...\n 106│fn funnel_one(\n*107│    brain: &mut Brain,\n*108│    stream_id: u64,\n 109│    decoder: &mut WanDecoder,\n 110│    origin: &str,\n 111│    owlery: &std::path::Path,\n    │...\n 114│    loop {\n*115│        match brain.read_event().expect(\"read event\") {\n*116│            BrokerEvent::NetStreamData {\n*117│                stream_id: sid,\n 118│                bytes,\n 119│                ..\n*120│            } if sid == stream_id => {\n 121│                if let Some(rec) = decoder.push(&bytes).into_iter().next() {\n 122│                    return receive_wan(&rec, origin, owlery, registry);\n 123│                }\n 124│            }\n*125│            BrokerEvent::NetStreamEof { .. } => panic!(\"eof before the record arrived\"),\n 126│            BrokerEvent::Error { message } => panic!(\"broker error: {message}\"),\n 127│            _ => {}\n 128│        }\n    │...\n 145│// firewall, the whitelist untouched); (3) whitelisting the node admits its\n*146│// unsolicited traffic again — store edits take effect mid-stream, no daemon\n*147│// restart.\n 148│#[test]\n 149│fn whitelist_refuses_exempts_replies_and_admits_listed() {\n 150│    init_home();\n    │...\n 168│\n*169│    // Node A dials B and opens the message stream.\n 170│    let mut sender = connect_retry(&name_a);\n 171│    let a_node = sender\n 172│        .net_status()\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   5│//! Two in-process brokers (hermetic loopback endpoints, relays disabled) play\n    │...\n   9│//! - **remote drive**: keystrokes sent from B arrive at A's PTY through the\n *10│//!   journaled input path, and the session's output streams back to B as\n  11│//!   sequenced records — B sees its own marker echoed by A's child;\n  12│//! - **origin is transport truth** (KNOWN-HAZARDS 7.5): the serve loop gates\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  18│//!   dedups at the PTY-write journal (typed exactly once), re-transmitted\n  19│//!   output dedups at B's render cursor (rendered exactly once) — including\n  20│//!   input B sent INTO the dead window.\n    │...\n  27│use spt_daemon::attach::{\n *28│    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n  29│    AttachServeOutcome, ControllerIrrecoverablyBehind,\n  30│};\n  31│// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n    │...\n  33│use spt_daemon::attach::request_attach_endpoint;\n *34│use spt_daemon::brain::{Brain, BrainState, BrokerEvent};\n  35│use spt_daemon::effect::{Minter, MintedOp};\n  36│use spt_daemon::msg::{decode_bytes, SpawnReq};\n  37│use spt_daemon::nethost::{NetConfig, NetHost};\n    │...\n  45│/// Hold the temp home for the whole test binary so it isn't deleted mid-run.\n *46│/// Every test inits it: serve_attach best-effort-stamps the endpoint perch's\n  47│/// `driven_by` marker, and that resolution must never touch a real home.\n  48│static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n  49│\n    │...\n  84│\n *85│fn connect_retry(name: &str) -> Brain {\n  86│    for _ in 0..300 {\n *87│        if let Ok(b) = Brain::cold_start(name, 1) {\n  88│            return b;\n  89│        }\n  90│        thread::sleep(Duration::from_millis(10));\n  91│    }\n *92│    panic!(\"brain could not connect\");\n  93│}\n  94│\n  95│fn echo_spawn_req() -> SpawnReq {\n    │...\n 113│\n*114│/// Poll the brain's `net-streams` until a PEER-initiated stream appears; return\n*115│/// `(stream_id, remote_id_hex)`. Filtering on `!initiated_locally` is what the\n 116│/// production dispatch loop does (dispatch.rs) — and it is REQUIRED for the\n 117│/// single-broker loopback case, where the table holds BOTH the operator's own\n 118│/// row and the cross-wired peer row: the serve target is always the peer row.\n*119│fn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n 120│    for _ in 0..400 {\n*121│        let reply = brain.net_streams().expect(\"net-streams\");\n*122│        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {\n*123│            return (s.stream_id, s.remote_id_hex.clone());\n 124│        }\n 125│        thread::sleep(Duration::from_millis(5));\n 126│    }\n*127│    panic!(\"attach stream never appeared at the target's broker\");\n 128│}\n 129│\n 130│/// The operator+serve drive shared by EVERY attach transport — the M12 W1.5\n## brain_decouple.rs\n  *1│//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\n   2│//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\n   3│//!\n   4│//! ## The bug this gate locks down (the update-wedge root)\n   5│//!\n  *6│//! A controller's writer thread does a BLOCKING socket write to its brain\n  *7│//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\n  *8│//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\n   9│//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\n  10│//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\n  11│//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\n    │...\n  14│//!\n *15│//! ## Faithful in-process analog of a suspended brain\n  16│//!\n  17│//! A test process cannot `NtSuspendProcess` itself, so the rig reproduces the exact\n  18│//! MECHANISM: controller A stops reading its socket. The flood child fills A's recv\n  19│//! buffer, the controller channel fills, and A's writer thread parks INSIDE\n *20│//! `write_frame` — precisely the state a suspended brain leaves its broker conn in\n  21│//! (a peer that stopped draining). This is the same analog the 7.12 wedge gate uses.\n *22│//! `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15 s production deadline so the\n  23│//! structural outcome is provable in ~1 s (the deadline knob grants wall-clock only;\n  24│//! it never relaxes an assertion).\n  25│//!\n    │...\n  27│//!\n *28│//! With the wedged controller's writer parked past the brain-write deadline:\n  29│//! 1. an attached VIEWER keeps ticking (the fan-out never blocks on the wedged conn);\n  30│//! 2. a second operator's `Control` subscribe COMPLETES — the wedged incumbent is\n  31│//!    stall-evicted broker-side and the newcomer takes the freed slot (pre-W2 this\n    │...\n  36│//!\n *37│//! What this rig does NOT assert (covered elsewhere / manual): resume + cursor-replay\n  38│//! byte-identity is NOT re-proven here — W2's delta does not touch the replay path, so\n  39│//! output integrity stays covered by the pre-existing ring/cursor units (the effect-\n  40│//! journal replay dedup, the `delivered_through` no-rewind CAS `advanced_cursor`, and\n  41│//! the clean-repaint cold-attach batch). A live `NtSuspendProcess` / `SIGSTOP` run on a\n *42│//! real brain child remains the MANUAL field rig from the incident night — this CI test\n  43│//! is the deterministic in-process ANALOG, not that run.\n  44│//!\n  45│//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n  46│//!\n *47│//! Shares ONE process-global `SPT_HOME` + the `SPT_BRAIN_WRITE_DEADLINE_MS` env, and\n  48│//! spins a REAL broker + PTY child. Run isolated with\n *49│//! `cargo nextest run --test brain_decouple` (process-per-test — CI's path) or\n *50│//! `cargo test --test brain_decouple -- --test-threads=1`. Never a bare\n *51│//! `cargo test --test brain_decouple` on Windows.\n  52│\n  53│use std::sync::atomic::{AtomicU32, Ordering};\n  54│use std::sync::{Arc, OnceLock};\n    │...\n  57│\n *58│use interprocess::local_socket::Stream;\n  59│use spt_daemon::codec::{read_frame, write_frame};\n  60│use spt_daemon::frame::{Envelope, Role};\n  61│use spt_daemon::msg::{\n    │...\n  71│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n *72│    format!(\"spt-daemon-braindecouple-{}-{}.sock\", std::process::id(), n)\n  73│}\n  74│\n  75│static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n    │...\n 121│\n*122│fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 123│    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 124│}\n 125│\n*126│fn connect(name: &str) -> Stream {\n 127│    for _ in 0..200 {\n 128│        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n*129│            send_hello(&mut c, Role::Brain).expect(\"hello\");\n 130│            return c;\n 131│        }\n 132│        thread::sleep(Duration::from_millis(10));\n    │...\n 151│            Ok(mut c) => {\n*152│                if send_hello(&mut c, Role::Brain).is_err() {\n 153│                    let _ = tx.send(None);\n 154│                    return;\n 155│                }\n## brain_resume_conn_deadlock.rs\n  *1│//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\n   2│//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\n   3│//!\n   4│//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n  *5│//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n  *6│//! onto the brain's OWN request/reply conn. A conn's send half is one\n   7│//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n   8│//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\n   9│//! `write_frame`, and the dispatch reply path (`send_frame`) needs the SAME lock.\n *10│//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\n  11│//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\n *12│//! calls. When an actively-streaming session backs the conn up, the brain's own\n  13│//! subscriber writer blocks holding `send.lock()` → the heartbeat REPLY is starved →\n *14│//! the heartbeat stalls → the brain never drains → SELF-DEADLOCK. That is the field\n  15│//! wedge (both severities: respawn replay-burst + steady-state output backup), with\n  16│//! NO dead peer required.\n  17│//!\n *18│//! RED-first discipline: these tests assert the FIXED (Option A) behavior — the brain\n  19│//! heartbeat stays LIVE while sessions flood. Against the CURRENT tree (where\n *20│//! `resume_sessions` still subscribes) they go RED (the heartbeat starves). Option A\n *21│//! (resume_sessions does NOT subscribe) turns them GREEN. The observable is the number\n  22│//! of heartbeat round-trips that COMPLETE inside a fixed window — robust to the\n  23│//! std `Mutex`'s unfairness (a slow-but-progressing heartbeat still fails the RED bar,\n  24│//! a truly deadlocked one completes ~zero).\n    │...\n  27│//! Shares one process-global `SPT_HOME`; spins a REAL broker + REAL flood PTY children.\n *28│//! `cargo nextest run --test brain_resume_conn_deadlock` (CI path) or\n *29│//! `cargo test --test brain_resume_conn_deadlock -- --test-threads=1`. Never a bare\n  30│//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\n  31│//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\n  32│\n    │...\n  37│\n *38│use interprocess::local_socket::Stream;\n *39│use spt_daemon::brain::Brain;\n  40│use spt_daemon::codec::{read_frame, write_frame};\n  41│use spt_daemon::frame::{Envelope, Role};\n  42│use spt_daemon::msg::{SpawnReq, Spawned, KIND_SPAWN, KIND_SPAWNED};\n    │...\n  48│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n *49│    format!(\"spt-daemon-resumedeadlock-{}-{}.sock\", std::process::id(), n)\n  50│}\n  51│\n  52│static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n    │...\n 101│\n*102│fn connect(name: &str) -> Stream {\n 103│    for _ in 0..200 {\n 104│        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n*105│            send_hello(&mut c, Role::Brain).expect(\"hello\");\n 106│            return c;\n 107│        }\n 108│        thread::sleep(Duration::from_millis(10));\n    │...\n 115│/// child floods the PTY regardless, so the broker's ring keeps advancing and the\n*116│/// brain's viewer subscription keeps receiving live output.\n*117│fn spawn_flood(name: &str, endpoint: &str) -> (u64, u32, Stream) {\n 118│    let mut c = connect(name);\n 119│    write_frame(\n 120│        &mut c,\n    │...\n 132│\n*133│/// How the probe brain resumes before running the heartbeat: the SHIPPING daemon\n*134│/// path — `CursorOnly` → `resume_session_cursors`, no subscribe. (The hazard path,\n## brain_swap.rs\n *1│//! C0 E2E — the M3c keystone: a **brain-only self-update** swaps the brain logic\n  2│//! through the update engine with **zero hosted-endpoint interruption**\n *3│//! (REQ-UPD-3, absolute for the brain-only class — ADR-0004 §A).\n  4│//!\n *5│//! This rides the same broker/brain substrate the B2 handoff test proves, but\n *6│//! drives the swap through the *update engine* (`plan_update` → `apply_brain_only`)\n *7│//! rather than calling `Brain::handoff` directly — the engine *triggers* a\n  8│//! handoff, it does not re-implement one (`M3c-PLAN.md` §C0, clean-room posture).\n  9│//!\n 10│//! Shape: one echo child runs under a broker PTY for the whole test. The outgoing\n*11│//! brain injects a marker it never reads, then the engine applies a brain-only\n*12│//! update: the new brain re-attaches via the supplied relaunch closure (which\n*13│//! stands for \"exec the verified new binary → `Brain::handoff`\") and must find\n 14│//! that marker in the *replayed* buffer — proving the broker buffered output\n*15│//! produced while no brain was attached. Throughout, the child's pid is unchanged\n 16│//! and exactly one session is hosted: the endpoint never noticed the swap.\n 17│//!\n*18│//! **Filename note:** this file is `brain_swap.rs`, NOT `update.rs`, on purpose.\n 19│//! Windows' installer-detection heuristic (UAC) forces elevation for any\n 20│//! executable whose name contains `update`/`setup`/`install`/`patch`; a test\n 21│//! binary `update-<hash>.exe` then fails to launch with `os error 740` (requires\n   │...\n 24│//! **REQ-UPD-3 `int` re-pointed (restoration D7-1).** This test proves the\n*25│//! *in-process* brain-only swap shape (the engine's `apply_brain_only` over an\n*26│//! in-thread `Brain::handoff` closure) — valuable engine-mechanics coverage, but\n 27│//! NOT process-level survival. The REQ-UPD-3 `int` evidence (no endpoint drops\n*28│//! across a brain-only update) now lives in `crates/spt/tests/brain_survive.rs`,\n*29│//! which swaps a real on-disk binary and respawns the real `spt daemon brain`\n 30│//! child process (ADR-0018 V5). This file keeps exercising the engine path; it is\n 31│//! no longer the REQ-UPD-3 int tag.\n 32│\n   │...\n 37│\n*38│use spt_daemon::brain::{Brain, BrainState, BrokerEvent};\n 39│use spt_daemon::msg::SpawnReq;\n*40│use spt_daemon::update::{apply_brain_only, plan_update, BrokerAbi, ReleaseSpec, UpdateClass};\n 41│use spt_daemon::Broker;\n 42│\n 43│static SEQ: AtomicU32 = AtomicU32::new(0);\n   │...\n 67│\n*68│fn cold_retry(name: &str, now_ms: u64) -> Brain {\n 69│    for _ in 0..300 {\n*70│        if let Ok(b) = Brain::cold_start(name, now_ms) {\n 71│            return b;\n 72│        }\n 73│        thread::sleep(Duration::from_millis(10));\n 74│    }\n*75│    panic!(\"brain cold-start could not connect\");\n 76│}\n 77│\n 78│/// The relaunch closure C0's engine drives: it stands for \"the verified new\n*79│/// binary's brain re-attaches over the existing substrate.\" Retries the connect\n## broker.rs\n   4│//! The child is an echo program (`cat` on Unix, `findstr \".\"` on Windows — the\n  *5│//! same cross-OS shape spt-term's injection tests use): bytes the brain injects\n   6│//! round-trip through the real PTY and come back as `output` events. We then\n   7│//! kill it and observe the `exit` event. This exercises the whole B1 contract —\n  *8│//! spawn under a broker PTY, stream output, accept input, report exit — through\n   9│//! the actual named-pipe / UDS transport.\n  10│//!\n *11│//! The brain is driven **single-threaded**: send a command, then read frames\n  12│//! until the expected reply/event arrives (accumulating any interleaved output).\n *13│//! interprocess `Stream::split()` shares the underlying fd, so a split client\n *14│//! would only signal EOF once *both* halves drop; keeping one `Stream` means\n  15│//! dropping it closes the fd, the broker's command loop hits EOF, and it tears\n  16│//! the session down deterministically — which we then assert.\n  17│// [int->REQ-DAEMON-2]\n    │...\n  23│\n *24│use interprocess::local_socket::Stream;\n  25│use spt_daemon::codec::{read_frame, write_frame};\n  26│use spt_daemon::effect::{EffectKey, Minter};\n  27│use spt_daemon::frame::{Envelope, Role};\n    │...\n  62│\n *63│fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n  64│    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n  65│}\n  66│\n    │...\n  70│\n *71│    // The broker accepts one brain and serves it on a handler thread.\n  72│    let server_broker = Arc::clone(&broker);\n  73│    let server = thread::spawn(move || {\n *74│        let handle = server_broker.serve_once().expect(\"accept brain\");\n  75│        handle.join().expect(\"handler thread\");\n  76│    });\n  77│\n *78│    // Brain side: connect (retry briefly in case accept lags the bind).\n  79│    let mut conn = None;\n  80│    for _ in 0..200 {\n  81│        match LocalSocketTransport::connect(&name) {\n    │...\n  93│\n *94│    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n  95│\n  96│    // Spawn the echo child under a broker PTY; read until the `spawned` reply,\n  97│    // accumulating any output that interleaves.\n    │...\n 159│\n*160│    // Disconnect: dropping the single Stream closes the fd, so the broker's\n 161│    // command loop hits EOF and tears the session down. Joining the handler\n 162│    // (via the server thread) lets us assert the teardown happened.\n 163│    drop(conn);\n    │...\n 181│#[test]\n*182│fn broker_hosts_pty_child_streams_output_and_accepts_input() {\n 183│    main_test();\n 184│}\n 185│\n    │...\n 203│    let server = thread::spawn(move || {\n*204│        let handle = server_broker.serve_once().expect(\"accept brain\");\n 205│        handle.join().expect(\"handler thread\");\n 206│    });\n 207│\n    │...\n 218│    let mut conn = conn.expect(\"connect to broker\");\n*219│    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n 220│\n 221│    // Spawn the echo child LABELLED with an endpoint id (the spawn.endpoint the\n 222│    // broker tracks per session — what KIND_ENDPOINT_INPUT resolves against).\n    │...\n 327│    let server = thread::spawn(move || {\n*328│        let handle = server_broker.serve_once().expect(\"accept brain\");\n 329│        handle.join().expect(\"handler thread\");\n 330│    });\n 331│    let mut conn = None;\n    │...\n 341│    let mut conn = conn.expect(\"connect to broker\");\n*342│    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n 343│\n 344│    // A child that prints the injected env var, cross-OS.\n 345│    #[cfg(unix)]\n    │...\n 398│            Err(_) => {\n*399│                // After EXIT a run of read errors means the stream is fully drained\n 400│                // (no trailing output is coming) — stop. Before EXIT the frame just\n 401│                // isn't here yet, so wait and retry.\n 402│                if exited {\n    │...\n 450│    let mut conn = conn.expect(\"connect to broker\");\n*451│    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n 452│\n 453│    // A child that exits immediately.\n 454│    #[cfg(unix)]\n    │...\n 565│        let mut c = conn.expect(\"connect to broker\");\n*566│        send_hello(&mut c, Role::Brain).expect(\"send hello\");\n 567│        c\n 568│    };\n 569│\n## budget.rs\n  42│\n *43│use spt_daemon::brain::Brain;\n  44│use spt_daemon::broker::Broker;\n  45│use spt_daemon::msg::SpawnReq;\n  46│use tempfile::TempDir;\n    │...\n  79│\n *80│    // One probe brain per seat (kill_session targets the brain's own session).\n *81│    let mut seats: Vec<(Brain, u64, u32)> = Vec::new(); // (brain, session, child pid)\n  82│    for i in 0..n {\n *83│        let mut brain = connect_retry(&name);\n  84│        let req = SpawnReq {\n  85│            program: program.clone(),\n  86│            args: args.clone(),\n    │...\n  95│        };\n *96│        let (session, pid) = brain.spawn_session_pid(req).expect(\"spawn seat\");\n  97│        let pid = pid.expect(\"seat child pid\");\n *98│        seats.push((brain, session, pid));\n  99│    }\n 100│\n 101│    thread::sleep(settle);\n    │...\n 155│    // would remain in production). The residual is the daemon baseline.\n*156│    for (brain, _, _) in seats.iter_mut() {\n*157│        let _ = brain.kill_session();\n 158│    }\n 159│    thread::sleep(Duration::from_millis(1_500));\n 160│    let self_after = sample_pid(std::process::id()).unwrap_or_default();\n    │...\n 204│\n*205│fn connect_retry(name: &str) -> Brain {\n 206│    for _ in 0..300 {\n*207│        if let Ok(b) = Brain::cold_start(name, 1) {\n 208│            return b;\n 209│        }\n 210│        thread::sleep(Duration::from_millis(10));\n 211│    }\n*212│    panic!(\"brain could not connect\");\n 213│}\n 214│\n 215│/// Sample one process: RSS bytes, handle/fd count, cumulative CPU seconds.\n## conn_blackhole_lifecycle.rs\n  42│//!\n *43│//! ## Relationship to `brain_decouple.rs`\n  44│//!\n *45│//! This harness GENERALIZES `brain_decouple.rs`'s r4 gate\n  46│//! (`non_draining_controller_stall_evict_releases_writer_and_connection`, the\n  47│//! Windows-mandatory physical-retirement leg for\n  48│//! REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, which stays the primary\n  49│//! gate for that hazard). It reuses the same SEED/trigger/burst choreography,\n *50│//! the hand-back-the-unread-Stream pattern, the `conn_handler_count` and\n  51│//! `stall_evict_count` seams, the old-client-write-fails probe, and the\n  52│//! teardown-before-assert discipline. It ADDS the two pieces the r4 gate does\n  53│//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n    │...\n  70│//!\n *71│//! Shares ONE process-global `SPT_HOME` + `SPT_BRAIN_WRITE_DEADLINE_MS`, spins a\n  72│//! REAL broker + TWO PTY children. HEAVY class. Run isolated with\n  73│//! `cargo nextest run -p spt-daemon --test conn_blackhole_lifecycle` (CI's path)\n  74│//! or `cargo test --test conn_blackhole_lifecycle -- --test-threads=1`. NEVER a\n    │...\n  85│\n *86│use interprocess::local_socket::Stream;\n  87│use spt_daemon::codec::{read_frame, write_frame};\n  88│use spt_daemon::frame::{Envelope, Role};\n  89│use spt_daemon::msg::{\n    │...\n 119│\n*120│fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 121│    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 122│}\n 123│\n*124│fn connect(name: &str) -> Stream {\n 125│    for _ in 0..200 {\n 126│        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n*127│            send_hello(&mut c, Role::Brain).expect(\"hello\");\n 128│            return c;\n 129│        }\n 130│        thread::sleep(Duration::from_millis(10));\n    │...\n 135│/// A child that FLOODS stdout forever (never reads stdin) — drives the UNRELATED\n*136│/// session so its viewer has a continuous frame stream to keep ticking on\n 137│/// throughout the black-hole incident (invariant 1).\n 138│fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139│    SpawnReq {\n    │...\n 160│/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n*161│/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162│fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n 163│    SpawnReq {\n 164│        program: \"powershell\".to_string(),\n    │...\n 183│/// Spawn a PTY child from a fresh conn and return `(spawn_conn, session_id)`.\n*184│fn spawn_session(name: &str, req: SpawnReq) -> (Stream, u64) {\n 185│    let mut c = connect(name);\n 186│    send(&mut c, KIND_SPAWN, serde_json::to_value(req).unwrap());\n 187│    let sid = loop {\n    │...\n 223│/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n*224│/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225│/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226│fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n 227│    let name = name.to_string();\n    │...\n 250│    init_home();\n*251│    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 252│    // instead of 15 s — the structural outcome, fast + deterministic (the knob\n 253│    // grants wall-clock only; it never relaxes an assertion).\n*254│    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n 255│\n 256│    let name = unique_name();\n 257│    let broker = Broker::bind(&name).expect(\"bind broker\");\n    │...\n 292│    // ── BLACK-HOLED session B: the real SEED+burst PTY child from conn A, then\n*293│    //    DROP A so the ONLY persistent brain conn is the black-holed controller R. ──\n 294│    let (a, sid_b) = spawn_session(&name, seed_burst_spawn_req(\"blackhole-victim-ep\"));\n 295│    drop(a);\n 296│\n    │...\n 300│    //    drains a few burst frames (a real seq>0 frame delivered), and STOPS\n*301│    //    reading — handing the Stream back to main UNREAD. Main holds it, never\n 302│    //    draining, so the broker's R-writer parks in `write_frame` exactly as\n*303│    //    against a suspended brain. R's Stream is NEVER dropped until the very end. ──\n 304│    let r_name = name.clone();\n 305│    let (seed_tx, seed_rx) = std::sync::mpsc::channel::<(bool, u64)>();\n 306│    let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();\n*307│    let (hand_tx, hand_rx) = std::sync::mpsc::channel::<(Stream, u64)>();\n 308│    thread::spawn(move || {\n 309│        let mut r = connect(&r_name);\n 310│        send(\n    │...\n 386│        }\n*387│        // Hand the Stream back to main UNREAD (main holds it = the black-holed\n 388│        // client). The broker's R-writer stays wedged on the remaining burst.\n 389│        let _ = hand_tx.send((r, max_seq));\n 390│    });\n    │...\n 398│    let _ = go_tx.send(());\n*399│    // Recv the handed-back black-holed Stream (held UNREAD for the rest of the test).\n 400│    let handed = hand_rx.recv_timeout(Duration::from_secs(25)).ok();\n## control_stamp_lifetime.rs\n  11│//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n *12│//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n  13│//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n  14│//! path dropped the OutputLog with the controller slot still occupied, so the\n  15│//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\n    │...\n  48│\n *49│use spt_daemon::brain::Brain;\n  50│use spt_daemon::msg::SpawnReq;\n  51│use spt_daemon::Broker;\n  52│\n    │...\n  89│\n *90│fn connect_brain(name: &str) -> Brain {\n  91│    for _ in 0..300 {\n *92│        if let Ok(b) = Brain::cold_start(name, 1) {\n  93│            return b;\n  94│        }\n  95│        thread::sleep(Duration::from_millis(10));\n  96│    }\n *97│    panic!(\"brain could not connect to {name}\");\n  98│}\n  99│\n 100│/// A long-lived stdin-reading PTY child so the spawned session stays LIVE until we\n    │...\n 145│    let broker = start_broker(&name);\n*146│    let mut spawner = connect_brain(&name);\n 147│    let sid = spawner\n 148│        .spawn_session(idle_spawn_req(&id))\n 149│        .expect(\"spawn live session for the endpoint\");\n    │...\n 183│    // ── Kill the PTY CHILD → the exit-waiter reaps the session (NOT a conn drop, so\n*184│    //    the controller-detach path never runs; only the reap path can unlatch). ──\n 185│    let pid = broker\n 186│        .session_pid(sid)\n 187│        .expect(\"live session must have a pid before kill\");\n    │...\n 247│    let broker = start_broker(&name);\n*248│    let mut spawner = connect_brain(&name);\n 249│    let sid = spawner\n 250│        .spawn_session(idle_spawn_req(&id))\n 251│        .expect(\"spawn live session before its perch exists\");\n    │...\n 266│    for _ in 0..50 {\n*267│        let mut brain = connect_brain(&name);\n*268│        let _ = brain.sessions().expect(\"KIND_SESSIONS reply\");\n 269│        thread::sleep(Duration::from_millis(50));\n 270│        if spt_store::info::read_info(&self_perch)\n 271│            .map(|i| i.controlled)\n    │...\n 304│// + `driven_by` on a spt-hosted online live_agent perch AFTER the harness exited,\n*305│// persisting >20min AND ACROSS A DAEMON RESTART. This is the DIFFERENT reap leg\n 306│// from `reap_clears_control_and_viewer_stamps` above (that drives the broker\n 307│// exit-waiter's `stamp_reaped`); B3 is the `reconcile_hosted_liveness` sweep — the\n 308│// tick that clears a sticky stamp on a perch whose broker session is simply GONE,\n*309│// regardless of status, so an already-offline perch left sticky across a restart is\n 310│// swept (path iv, the boot sweep), not only on the online→offline transition.\n 311│//\n 312│// This int drives the REAL reconcile entry (`spt_daemon::livehost::reconcile_hosted_liveness`)\n 313│// against the REAL on-disk owlery, with the live-session set DERIVED FROM A REAL\n*314│// BROKER (query_live_session_endpoints's exact shape: `brain.sessions()` →\n 315│// endpoint ids). A decoy live session keeps the broker set genuinely NON-empty, so\n 316│// the reap fires precisely because the broker reports NO session for the sticky\n 317│// perch — not because we hand it an empty set. Two arms:\n    │...\n 319│//   (B) already-OFFLINE + sticky → cleared REGARDLESS of status (the boot sweep; a\n*320│//       restarted daemon must not resurrect the stamps).\n 321│// [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n 322│\n 323│/// Seed a spt-hosted (`controllable=Some(true)`) live_agent self-perch at `status`,\n    │...\n 351│    let online_id = format!(\"b3-online-{pid}\");\n*352│    // Arm B: already-OFFLINE + sticky (survived a daemon restart — the boot sweep).\n 353│    let offline_id = format!(\"b3-boot-{pid}\");\n 354│    let online_perch = seed_sticky_hosted_perch(&online_id, spt_store::liveness::STATUS_ONLINE, node);\n 355│    let offline_perch =\n    │...\n 364│    let broker = start_broker(&name);\n*365│    let mut spawner = connect_brain(&name);\n 366│    let decoy_sid = spawner\n 367│        .spawn_session(idle_spawn_req(&decoy_id))\n 368│        .expect(\"spawn the decoy live session\");\n    │...\n 372│    // `query_live_session_endpoints`: KIND_SESSIONS reply → endpoint ids.\n*373│    let mut probe = connect_brain(&name);\n 374│    let reply = probe.sessions().expect(\"KIND_SESSIONS reply\");\n 375│    let live: BTreeSet<String> = reply\n 376│        .sessions\n    │...\n 428│    // of status. A naive reap gated on the online→offline transition would skip this\n*429│    // (the perri F-b bug: >20min + across a daemon restart). Presence stays OFFLINE.\n 430│    // [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n 431│    assert!(\n 432│        !after_offline.controlled,\n*433│        \"B3 arm B (boot sweep): a restarted daemon must clear a sticky controlled on an \\\n 434│         ALREADY-OFFLINE perch — not resurrect it\"\n 435│    );\n 436│    assert_eq!(\n## daemon_e2e.rs\n   2│//! architecture through the real IPC in one flow: a daemon hosts a LiveAgent's\n  *3│//! lifecycle as in-process loops **and** survives a brain restart with its hosted\n  *4│//! PTY child + output stream intact and gapless.\n   5│//!\n   6│//! Flow (the M3b done-definition, ADR-0004 / `M3b-PLAN.md` §B9):\n   7│//!\n  *8│//! 1. **spawn** — a [`Broker`] hosts a real PTY child; a [`Brain`] cold-starts,\n   9│//!    spawns the session, and round-trips a marker through it (the session is live).\n *10│//! 2. **Psyche-loop** — a [`BrainLifecycle`] (the consolidated re-host of the\n  11│//!    interim `api listen` live half, [`REQ-DAEMON-1`]) spawns the Psyche companion;\n  12│//!    the daemon marks the hosted perch `online`, so its liveness is the daemon\n  13│//!    `status`, never the summarizer pid ([`REQ-HAZARD-DAEMON-HOSTED-LIVENESS`]).\n    │...\n  15│//!    tick folds it in (the daemon-hosted lifecycle loop, [`REQ-DAEMON-1`]).\n *16│//! 4. **brain-restart-survives** — the brain is dropped (a crash) and handed off\n  17│//!    repeatedly; across each handoff the child's pid is unchanged, exactly one\n *18│//!    session is hosted, the output stream replays gaplessly, and `gen_start`\n  19│//!    advances ([`REQ-DAEMON-2`] / [`REQ-HAZARD-GEN-START-NOW`]). The lifecycle is\n *20│//!    stateless on disk, so it keeps ingesting communes straight across the restart.\n  21│//! 5. **graceful-signoff** — the lifecycle's graceful teardown runs the load-bearing\n  22│//!    ordering — echo-commune BEFORE teardown (3.3) — and removes the Self's ready\n  23│//!    marker ([`REQ-DAEMON-4`]).\n    │...\n  27│//! echoes them, exactly as B8 documents (bare `\\n` echoes a lone `\\r` and collapses\n *28│//! boundaries — the Spike #4/#5 PTY stream-contract gotcha).\n  29│//!\n  30│//! Single `#[test]` on purpose: the flow owns the process-global `SPT_HOME`, so it\n  31│//! must not race a sibling test in this binary.\n *32│// REQ-DAEMON-1 `int` re-pointed to crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs\n *33│// (M11-W0.4 (i)): the REAL brain PROCESS (run_brain) hosts the Psyche, retiring\n *34│// this file's type-level BrainLifecycle::with_config-direct over-claim (it kept\n  35│// green coverage over an UNWIRED production path — the M3-era false-green). This\n  36│// file keeps its REQ-DAEMON-4 + hazard int evidence (the lifecycle machinery +\n  37│// daemon-hosted-liveness), which it still exercises directly.\n *38│// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration\n  39│// D7-1): the M3b-B9 flow proves the daemon-hosted lifecycle + an *in-process*\n *40│// brain-restart survival, not the process-level swap. Process-level endpoint\n *41│// survival across a brain-PROCESS restart onto a swapped binary is the D7-1 E2E.\n  42│// [int->REQ-DAEMON-4]\n  43│// [int->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n  44│\n    │...\n  49│\n *50│use spt_daemon::brain::BrainState;\n  51│use spt_daemon::msg::SpawnReq;\n *52│use spt_daemon::{Brain, BrainLifecycle, Broker, DaemonConfig};\n  53│use spt_runtime::Manifest;\n  54│use spt_store::perch::{self, ParentHint};\n  55│\n    │...\n 108│\n*109│fn cold_retry(name: &str, now_ms: u64) -> Brain {\n 110│    for _ in 0..300 {\n*111│        if let Ok(b) = Brain::cold_start(name, now_ms) {\n 112│            return b;\n 113│        }\n 114│        thread::sleep(Duration::from_millis(10));\n 115│    }\n*116│    panic!(\"brain cold-start could not connect\");\n 117│}\n 118│\n*119│fn handoff_retry(name: &str, prior: BrainState, now_ms: u64) -> Brain {\n 120│    for _ in 0..300 {\n## daemon_lifecycle_real_brain.rs\n  2│//! `[int->REQ-DAEMON-1]` evidence OFF the type-level `daemon_e2e` (which\n *3│//! constructs a [`BrainLifecycle`] directly — green coverage over an UNWIRED\n *4│//! production path, the M3-era false-green) ONTO the actual brain entry point:\n  5│//!\n *6│//! a [`Broker`] + `brainproc::run_brain` (the real brain's `main`-equivalent,\n  7│//! which since M11-W0.1 runs `livehost::spawn_live_host`) HOSTS a real ONLINE live\n  8│//! endpoint and DRIVES its pulse loop — proven by the loop ingesting a commune into\n *9│//! the live-context tier. The reconcile that hosts is the REAL brain's, not an in-\n*10│//! test `reconcile_once` call. A cold `run_brain` over an already-online perch IS\n*11│//! the post-brain-restart rehost: the perch's `status=online` survives the brain\n*12│//! process, and the fresh brain re-hosts the lifecycle at boot (daemon-authoritative\n 13│//! liveness, CONTEXT:194).\n 14│//!\n 15│//! **F-030 W1:** the Psyche is now a PER-EVENT TURN, not a resident process, so this\n 16│//! no longer waits for a resident `<id>-psyche` perch to bind (host_one spawns none)\n*17│//! and reaps no detached child. The DAEMON-1 evidence is the hosted pulse loop\n 18│//! ingesting the commune — unchanged and independent of the retired resident spawn.\n 19│//!\n 20│//! Single `#[test]`: it owns the process-global `SPT_HOME`.\n   │...\n 26│\n*27│use spt_daemon::brainproc::{run_brain, StartReason};\n 28│use spt_daemon::endpoint::broker_socket_name;\n 29│use spt_daemon::Broker;\n 30│use spt_store::liveness::STATUS_ONLINE;\n   │...\n 33│// A no-op `psyche_init` command. F-030 W1 host_one spawns NO resident psyche, but\n*34│// `BrainLifecycle::with_config_in` still REQUIRES the manifest to declare the role,\n 35│// so it stays present (never spawned). The pulse loop drives regardless — commune\n 36│// ingest is independent of any psyche process.\n 37│#[cfg(windows)]\n   │...\n 42│#[test]\n*43│fn real_brain_process_hosts_the_psyche_for_an_online_live_endpoint() {\n 44│    let home = tempfile::tempdir().unwrap();\n 45│    std::env::set_var(\"SPT_HOME\", home.path());\n 46│\n*47│    // A live-capable adapter, registered so the brain resolves the manifest from\n 48│    // disk by the perch's adapter (the production resolve path).\n 49│    let drops = home.path().join(\"drops\");\n 50│    std::fs::create_dir_all(&drops).unwrap();\n   │...\n 64│    // establish→online path is proven by live_firsthost/live_bind ints; here the\n*65│    // focus is the REAL brain hosting it). status=online survives the brain.\n 66│    let id = \"agent9\";\n 67│    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 68│    std::fs::create_dir_all(&perch_path).unwrap();\n   │...\n 73│\n*74│    // ── the REAL brain: a Broker + brainproc::run_brain on a thread (NOT a direct\n*75│    // BrainLifecycle::with_config). run_brain runs spawn_live_host, so the reconcile\n*76│    // hosting the Psyche is the real brain's, at boot.\n 77│    let broker = Broker::bind(&broker_socket_name()).expect(\"bind broker\");\n 78│    {\n 79│        let serve = Arc::clone(&broker);\n   │...\n 84│    thread::spawn(|| {\n*85│        let _ = run_brain(0, StartReason::Cold);\n 86│    });\n 87│\n*88│    // The real brain's pulse loop runs (F-030 W1: NO resident psyche is spawned —\n 89│    // the DAEMON-1 evidence is the hosted loop ticking, not a bound `<id>-psyche`\n 90│    // perch): a commune drop is ingested into the live context tier (proves the loop\n*91│    // ticks in the brain, not a test-driven tick).\n 92│    std::fs::write(\n 93│        drops.join(format!(\"{id}-commune.md\")),\n*94│        \"Source: self-commune\\nreal-brain note\",\n 95│    )\n 96│    .unwrap();\n 97│    let ctx = spt_store::contextstore::live_context_file(&home.path().join(\"tracked\"), id);\n## digest.rs\n  13│//!   (impossible under the superseded byte-parse shape);\n *14│//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows\n  15│//!   delivers the structured delta to a subscriber (the push-driven freshness\n  16│//!   the `api digest-entry` path also drives — autonomous file-watch is deferred\n  17│//!   to the consuming frontend milestone).\n    │...\n  37│/// A stdin pass-through \"extractor\": the source bytes (piped in) round-trip to\n *38│/// stdout unchanged, so the fixture JSONL *is* the contract-record stream.\n  39│#[cfg(unix)]\n  40│const PASSTHROUGH: &str = \"cat\";\n  41│#[cfg(windows)]\n    │...\n 130│\n*131│    // ── Delta-stream: a reproject after the log grows delivers a delta ───────\n 132│    let fixture2 = home.path().join(\"cc2-session.jsonl\");\n 133│    std::fs::write(&fixture2, \"{\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"first\\\"}\\n\").unwrap();\n 134│    establish_cc_endpoint(\"cc2\", &fixture2);\n## dispatch.rs\n   1│//! Inbound dispatch E2E (D9-1-1): the serve half of every wire protocol runs\n  *2│//! **undriven** — no probe brain hand-wires `serve_*`; the daemon-side\n  *3│//! dispatcher claims each peer-initiated stream, classifies it by first\n   4│//! record, and serves it. This is the production behavior the D5–D8 E2E\n   5│//! suites deferred (\"the daemon dispatch\").\n   6│//!\n    │...\n  17│\n *18│use spt_daemon::brain::Brain;\n  19│use spt_daemon::broker::Broker;\n  20│use spt_daemon::effect::{Minter, MintedOp};\n  21│use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};\n    │...\n  84│\n *85│fn connect_retry(name: &str) -> Brain {\n  86│    for _ in 0..300 {\n *87│        if let Ok(b) = Brain::cold_start(name, 1) {\n  88│            return b;\n  89│        }\n  90│        thread::sleep(Duration::from_millis(10));\n  91│    }\n *92│    panic!(\"brain could not connect\");\n  93│}\n  94│\n  95│/// Stand B's dispatcher up exactly as the daemon does, returning its stop\n    │...\n 132│        last_seen: String::new(),\n*133│        lease_epoch: 1,\n 134│    });\n 135│    roster.save().expect(\"save roster\");\n 136│    let mut vis = VisibilityStore::load();\n    │...\n 142│\n*143│fn node_hex(brain: &mut Brain) -> String {\n*144│    brain\n 145│        .net_status()\n 146│        .expect(\"status\")\n 147│        .node_id_hex\n    │...\n 230│        artifact_sha256: sha256_hex(&artifact),\n*231│        brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,\n 232│        broker_resource_abi: BROKER_RESOURCE_ABI,\n 233│        product_version: String::new(),\n 234│    };\n    │...\n 316│    let conn = a.net_dial(addr, Some(MintedOp::new(Minter::Cli, op()))).expect(\"dial\");\n*317│    let opened = a.net_open_stream(conn.conn_id, Some(MintedOp::new(Minter::Cli, op()))).expect(\"open\");\n*318│    a.net_stream_send(opened.stream_id, &line, None, true)\n 319│        .expect(\"send feed\");\n 320│\n 321│    // The dispatcher applies asynchronously — poll B's canonical store.\n    │...\n 343│// and the direction fact is recorded right: A's table shows its opened\n*344│// stream as locally-initiated; B's shows the same stream as peer-initiated\n 345│// (what the dispatcher claimed it by).\n 346│#[test]\n 347│fn dispatcher_funnels_wan_messages_undriven() {\n    │...\n 367│    let conn = a.net_dial(addr, Some(MintedOp::new(Minter::Cli, op()))).expect(\"dial\");\n*368│    let opened = a.net_open_stream(conn.conn_id, Some(MintedOp::new(Minter::Cli, op()))).expect(\"open\");\n 369│    let msg = WanMessage {\n 370│        target: target.to_string(),\n 371│        from: \"doyle\".to_string(),\n    │...\n 374│    };\n*375│    a.net_stream_send(\n*376│        opened.stream_id,\n 377│        &spt_net::net::ndjson::encode_line(&msg),\n 378│        None,\n 379│        true,\n    │...\n 382│\n*383│    // Direction fact, both sides of the same stream.\n*384│    let a_rows = a.net_streams().expect(\"A streams\").streams;\n 385│    assert!(\n 386│        a_rows\n 387│            .iter()\n*388│            .any(|s| s.stream_id == opened.stream_id && s.initiated_locally),\n 389│        \"A's own open is locally-initiated in A's table\"\n 390│    );\n*391│    let b_rows = b_probe.net_streams().expect(\"B streams\").streams;\n 392│    assert!(\n 393│        b_rows.iter().all(|s| !s.initiated_locally),\n 394│        \"every row at B is peer-initiated (B opened nothing)\"\n## docs_server_e2e.rs\n  8│//!\n *9│//! Hermetic: port 0 (ephemeral), raw std TcpStream client — no HTTP client\n 10│//! dev-dep. The server thread is the production `docshost::start`.\n 11│//\n 12│// [int->REQ-DOCS-LOCAL-SERVER]\n   │...\n 14│use std::io::{Read, Write};\n*15│use std::net::TcpStream;\n 16│use std::path::Path;\n 17│\n 18│/// Minimal HTTP/1.1 request over loopback; returns (status-line, headers,\n   │...\n 20│fn http(port: u16, request_line: &str) -> (String, String, Vec<u8>) {\n*21│    let mut stream = TcpStream::connect((\"127.0.0.1\", port)).expect(\"connect docs server\");\n 22│    write!(\n*23│        stream,\n 24│        \"{request_line}\\r\\nHost: localhost:{port}\\r\\nConnection: close\\r\\n\\r\\n\"\n 25│    )\n 26│    .unwrap();\n 27│    let mut raw = Vec::new();\n*28│    stream.read_to_end(&mut raw).expect(\"read response\");\n 29│    let split = raw\n 30│        .windows(4)\n 31│        .position(|w| w == b\"\\r\\n\\r\\n\")\n## driven_by_selfheal.rs\n   6│//! An spt-hosted endpoint can latch `ONLINE+CONTROLLED` — `info.json`\n  *7│//! `driven_by = Some(node)` — and never clear it when a controller's detach is\n   8│//! LOST (no clean disconnect, and no output to trigger the W1 drain-evict\n   9│//! `mark_controller_gone`). The picker then renders a phantom \"controlled by X\"\n  10│//! forever. `reconcile_hosted_liveness` (`livehost.rs`) is the pull-primary\n    │...\n  35│//!   an idle session whose ABANDONED REMOTE controller wedged without a clean\n *36│//!   detach — keeps `controller_by==Some(origin)`, which a reconcile read can't\n  37│//!   detect at all. That case is its own deferred requirement,\n  38│//!   `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT` (broker-side controller eviction —\n  39│//!   NOT a reconcile `controller_by` clear). See `inject_control_wedge.rs` `w5_a2`\n    │...\n  52│//! It also captures the broker's OWN self-heal on an OBSERVED controller\n *53│//! disconnect (per-conn `detach_if`→`clear_controller`→`stamp_driven_by`).\n  54│//!\n  55│//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n  56│//!\n    │...\n  73│\n *74│use spt_daemon::brain::Brain;\n  75│use spt_daemon::msg::SpawnReq;\n  76│use spt_daemon::Broker;\n  77│use spt_daemon::livehost::reconcile_hosted_liveness;\n    │...\n 120│\n*121│/// Connect a Brain to the broker with bounded retry (cold_start — the request-reply\n*122│/// carrier `Brain::sessions()` rides; KIND_SESSIONS is a single reply, no streaming,\n 123│/// so a plain Whole conn is fine here).\n*124│fn connect_brain(name: &str) -> Brain {\n 125│    for _ in 0..300 {\n*126│        if let Ok(b) = Brain::cold_start(name, 1) {\n 127│            return b;\n 128│        }\n 129│        thread::sleep(Duration::from_millis(10));\n 130│    }\n*131│    panic!(\"brain could not connect to {name}\");\n 132│}\n 133│\n 134│/// A trivial long-lived PTY child so a spawned session stays LIVE (Gap A needs a\n    │...\n 156│/// spt-hosted (`controllable=Some(true)`) `live_agent` self-perch, `status=online`,\n*157│/// pre-stamped `driven_by=Some(node)` (the lost-detach latch). Mirrors the\n 158│/// livehost-bootgate-e2e stamping exactly, plus the `driven_by` marker.\n 159│fn stamp_latched_perch(id: &str, node: &str) -> std::path::PathBuf {\n 160│    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n    │...\n 166│    spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_ONLINE).unwrap();\n*167│    // The lost-detach latch the self-heal must clear.\n 168│    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\n 169│    self_perch\n 170│}\n    │...\n 191│    let _broker = start_broker(&name);\n*192│    let mut brain = connect_brain(&name);\n*193│    let reply = brain.sessions().expect(\"KIND_SESSIONS reply\");\n 194│    let live_sessions: BTreeSet<String> =\n 195│        reply.sessions.iter().map(|s| s.endpoint.clone()).collect();\n 196│\n    │...\n 270│    // ── A REAL broker hosting a LIVE session for THIS endpoint. The spawning\n*271│    //    brain is pre-attached as the LOCAL controller (dispatch_spawn →\n 272│    //    become_controller(by=None)). The PTY child keeps the session live in the\n 273│    //    broker table. ──\n 274│    let name = unique_name();\n 275│    let broker = start_broker(&name);\n*276│    let mut spawner = connect_brain(&name);\n 277│    let sid = spawner\n 278│        .spawn_session(idle_spawn_req(&id))\n 279│        .expect(\"spawn live session for the endpoint\");\n    │...\n 281│    // FINDING (the broker's OWN self-heal on a controller disconnect): dropping the\n*282│    // spawner socket makes the broker's per-conn handler run `detach_if` →\n 283│    // `clear_controller` → `stamp_driven_by`, which (since controller_by() is now\n 284│    // None) RE-STAMPS the perch's driven_by to None. So an OBSERVED controller\n 285│    // disconnect already unlatches via the broker single-writer — even without a\n*286│    // clean detach. The Gap-A the W5 RECONCILE must heal is the path where that\n*287│    // re-stamp NEVER fires: a brain restart re-adopts the live session but the\n 288│    // in-memory controller (and thus the clear_controller re-stamp) never existed,\n 289│    // so the STALE on-disk driven_by marker persists against a controller_by==None\n 290│    // session. We reproduce exactly THAT state below.\n    │...\n 300│         driven_by_after_spawner_drop={driven_by_after_disconnect:?} \\\n*301│         (None = the broker's detach_if→clear_controller→stamp_driven_by already \\\n 302│         unlatched the OBSERVED disconnect — so the RECONCILE Gap-A is only the \\\n*303│         NEVER-RE-STAMPED brain-restart leftover, reproduced next) ===\"\n 304│    );\n 305│\n## endpoint_survival.rs\n  *1│//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) — daemon-restart endpoint\n   2│//! survival, against a REAL broker over the real local IPC.\n   3│//!\n   4│//! ## The bug this gate locks down (seed #6, the field-accept blocker)\n    │...\n   8│//! though their records survived (info.json status + adapter + cwd + a ledger row). The\n  *9│//! fix: `livehost::resume_restart_orphaned_endpoints` runs ONCE at the fresh broker's\n  10│//! live-host start and RE-RUNS the harness of every previously-ONLINE spt-hosted\n *11│//! endpoint the restart orphaned, resuming from its last ledger session.\n  12│//!\n  13│//! ## What this rig proves\n  14│//!\n    │...\n  18│//! session. RED-first: WITHOUT the pass the broker has no session for it (it stays\n *19│//! offline). The two belts are unit-proven in `livehost::restart_resume_gate_*`; this\n  20│//! rig proves the end-to-end re-run against the real broker + a real spawned harness.\n  21│//!\n  22│//! ## RUNNER CONTRACT — process-per-test (nextest) or single-threaded\n    │...\n  30│\n *31│use spt_daemon::brain::now_ms;\n *32│use spt_daemon::{Brain, Broker};\n  33│\n *34│/// A long-lived, quiet harness the `[session.self]` resume spawns — it stays running\n  35│/// (so the broker session persists for the assertion) and produces no output.\n  36│#[cfg(windows)]\n  37│const HARNESS_CMD: &str = \"cmd /c ping -n 600 127.0.0.1 >nul\";\n    │...\n  40│\n *41│/// Register a live-capable adapter whose `[session.self]` (the is_resume fallback when\n *42│/// no `[session.resume]`) spawns the long-lived quiet harness, so\n  43│/// `launch_harness_brokered_in` has a real program to run.\n *44│fn register_resume_adapter(name: &str) {\n  45│    let toml = format!(\n  46│        \"[adapter]\\nname=\\\"{name}\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\n  47│         min_spt_core_version=\\\"0\\\"\\n\\n\\\n    │...\n  53│    spt_runtime::registry::register(&spt_store::perch::adapters_dir(), &src, 1000)\n *54│        .expect(\"register the resume adapter\");\n  55│}\n  56│\n  57│/// Seed a previously-ONLINE, spt-hosted (controllable), relay-less endpoint with a\n *58│/// ledger row to resume from — the daemon-restart-orphaned shape.\n  59│fn seed_orphaned_online_endpoint(id: &str, adapter: &str) {\n  60│    use spt_store::perch::{resolve_perch_path, resolve_ready_file, ParentHint};\n  61│    let perch = resolve_perch_path(id, ParentHint::Infer);\n    │...\n  71│    std::fs::write(resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\n *72│    // The ledger row the resume restores (session + adapter + cwd).\n  73│    let entry = spt_store::sessions::SessionEntry {\n  74│        ts: \"0\".to_string(),\n  75│        session_id: \"prev-session\".to_string(),\n    │...\n  81│    spt_store::sessions::append(&perch, &entry).unwrap();\n *82│    // NO resume.pid file → custody pid is not alive → the dup guard permits the re-run.\n  83│}\n  84│\n  85│// [int->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n  86│#[test]\n *87│fn daemon_restart_reruns_a_previously_online_orphaned_endpoint() {\n  88│    let home = tempfile::TempDir::new().expect(\"temp home\");\n  89│    std::env::set_var(\"SPT_HOME\", home.path());\n  90│\n    │...\n 100│    for _ in 0..200 {\n*101│        if Brain::cold_start(&name, now_ms()).is_ok() {\n 102│            break;\n 103│        }\n 104│        thread::sleep(Duration::from_millis(10));\n    │...\n 106│\n*107│    let adapter = \"mockresume\";\n*108│    register_resume_adapter(adapter);\n 109│    seed_orphaned_online_endpoint(\"survivor\", adapter);\n 110│\n 111│    // Precondition: the fresh broker hosts NO session for the endpoint (RED state).\n    │...\n 117│\n*118│    // The pass under test — the one-shot daemon-restart survival re-run.\n 119│    let owlery = spt_store::perch::owlery_dir();\n 120│    let adapters_dir = spt_store::perch::adapters_dir();\n 121│    let registered = spt_runtime::registry::registered(&adapters_dir);\n*122│    spt_daemon::livehost::resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n 123│\n 124│    // The endpoint's harness was re-run → a live broker session now exists for it.\n 125│    let mut appeared = false;\n## false_promote.rs\n   2│//! REAL broker hosting a REAL PTY child over the real local IPC. Exercises the\n  *3│//! PROMOTION PATH ITSELF (`brainproc::supervise_brain` → `run_trial`), the gate\n   4│//! condition doyle's W2 verdict (@e5ae7a9) made binding for W3.\n   5│//!\n   6│//! ## The bug this gate locks down (the 22:47 false-promote)\n   7│//!\n  *8│//! W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-\n  *9│//! promote window to `BRAIN_WRITE_DEADLINE` (15 s) — it does NOT close it. A new brain\n *10│//! can signal `brain.ready` INSIDE that window while the OLD generation's local\n  11│//! controller conn is still wedged (blocked writes pending), so `update apply`\n  12│//! \"promotes\" onto a still-frozen control plane (the incident-night 22:47 apply). The\n  13│//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\n  14│//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\n *15│//! read from BROKER TRUTH in-process (no brain round-trip).\n  16│//!\n  17│//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\n  18│//!\n    │...\n  22│//! write an independent watchdog that CANCELS + retires a wedged write at the SAME\n *23│//! `brain_write_deadline()` bound — so a real parked writer now self-heals at the\n  24│//! deadline and the rig's persistent-wedge precondition cannot exist (that is the\n  25│//! fix working, not a regression; the 22:47 permanent-frozen-plane class is dead by\n  26│//! construction). Ruling `docs/UPDATE-WEDGE-R4-FALSE-PROMOTE-RULING.md`: this rig\n    │...\n  28│//! `Broker::test_seed_wedged_local_controller` installs a LOCAL controller\n *29│//! (`by = None`, the brain's own resume/host identity) whose `write_blocked_since`\n  30│//! is seeded past the deadline and whose writer never touches the socket (no\n  31│//! watchdog armed → the injected wedge persists, zero timing race). The physical\n *32│//! bound/watchdog itself is `brain_decouple.rs`'s concern. The DRAIN below still\n  33│//! rides the REAL logical path (the take's `stall_evict_controller`), unchanged.\n *34│//! `SPT_BRAIN_WRITE_DEADLINE_MS` still shrinks the 15 s deadline (the knob grants\n  35│//! wall-clock only; it never relaxes an assertion).\n  36│//!\n  37│//! ## What this rig asserts (the DRAINED invariant)\n    │...\n  48│//!\n *49│//! Shares ONE process-global `SPT_HOME` + the `SPT_BRAIN_WRITE_DEADLINE_MS` env and\n  50│//! spins a REAL broker + PTY child. Run isolated with\n  51│//! `cargo nextest run --test false_promote` (process-per-test — CI's path) or\n  52│//! `cargo test --test false_promote -- --test-threads=1`. Never a bare\n    │...\n  60│\n *61│use interprocess::local_socket::Stream;\n *62│use spt_daemon::brainproc::{supervise_brain, BrainRestart, TrialEnv};\n  63│use spt_daemon::codec::{read_frame, write_frame};\n  64│use spt_daemon::frame::{Envelope, Role};\n  65│use spt_daemon::msg::{\n    │...\n 124│\n*125│/// A long-lived trial-candidate child (NOT a real brain — the rig fakes the readiness\n 126│/// stamp + the drained truth; the candidate only has to stay alive through the trial).\n 127│fn candidate_child() -> std::io::Result<Child> {\n 128│    #[cfg(windows)]\n    │...\n 140│\n*141│fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 142│    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 143│}\n 144│\n*145│fn connect(name: &str) -> Stream {\n 146│    for _ in 0..200 {\n 147│        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n*148│            send_hello(&mut c, Role::Brain).expect(\"hello\");\n 149│            return c;\n 150│        }\n 151│        thread::sleep(Duration::from_millis(10));\n    │...\n 157│/// stall-evicts the wedged local incumbent broker-side (the W2 self-heal). Holds the\n*158│/// conn briefly so the take is not immediately detached, then lets it fall.\n 159│fn remote_take(name: &str, sid: u64) {\n 160│    let name = name.to_string();\n 161│    thread::spawn(move || {\n    │...\n 190│/// off the live broker — the whole point of the rig. `record_promoted` captures the\n*191│/// version AND raises `stop` so `supervise_brain` returns.\n 192│struct FalsePromoteEnv {\n 193│    broker: Arc<Broker>,\n 194│    ready: Mutex<Option<u64>>,\n    │...\n 208│    }\n*209│    fn ready_generation(&self) -> Option<u64> {\n 210│        *self.ready.lock().unwrap()\n 211│    }\n 212│    fn clear_ready(&self) {\n    │...\n 228│    }\n*229│    // The gate under test: REAL broker truth, no brain round-trip.\n 230│    fn old_gen_drained(&self) -> bool {\n 231│        !self.broker.any_local_controller_wedged()\n 232│    }\n## handoff.rs\n *1│//! B2 regression — the milestone's load-bearing proof: a brain can be killed and\n *2│//! restarted repeatedly while the broker and its hosted child survive untouched,\n *3│//! and each new brain re-attaches to the output stream **gaplessly** with a fresh\n  4│//! `gen_start` (Spike #1 made real; KNOWN-HAZARDS 2.3 / 2.4).\n  5│//!\n  6│//! Shape: one echo child runs under a broker PTY for the whole test. Across 12\n *7│//! handoffs, each brain (a) re-attaches from its predecessor's resume cursor and\n  8│//! must find — in the *replayed* buffer — the marker its predecessor injected\n  9│//! and then died before reading (proving the broker buffered output produced\n*10│//! while no brain was attached), (b) injects the next marker for its successor,\n 11│//! then (c) \"crashes\" by dropping its connection. Throughout, the child's pid is\n 12│//! unchanged and exactly one session is hosted; `gen_start` advances every\n*13│//! handoff and the generation counter increments.\n 14│//!\n*15│//! A real-process SIGKILL of the brain was proven in Spike #1; here the brain is\n 16│//! a library driven in-thread and a dropped connection *is* the crash boundary\n 17│//! the broker must tolerate (subscriber vanishes; child + log persist).\n 18│// [int->REQ-HAZARD-GEN-START-NOW]\n*19│// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration\n 20│// D7-1): this proves the *in-process* handoff shape (a dropped connection as the\n*21│// crash boundary, brain driven in-thread) — the process-level survival onto a\n 22│// swapped binary is the D7-1 E2E. This file keeps proving the gen-start invariant.\n 23│\n 24│use std::sync::atomic::{AtomicU32, Ordering};\n   │...\n 28│\n*29│use spt_daemon::brain::{Brain, BrainState, BrokerEvent};\n 30│use spt_daemon::msg::SpawnReq;\n 31│use spt_daemon::Broker;\n 32│\n   │...\n 57│\n*58│fn cold_retry(name: &str, now_ms: u64) -> Brain {\n 59│    for _ in 0..300 {\n*60│        if let Ok(b) = Brain::cold_start(name, now_ms) {\n 61│            return b;\n 62│        }\n 63│        thread::sleep(Duration::from_millis(10));\n 64│    }\n*65│    panic!(\"brain cold-start could not connect\");\n 66│}\n 67│\n*68│fn handoff_retry(name: &str, prior: BrainState, now_ms: u64) -> Brain {\n 69│    for _ in 0..300 {\n*70│        if let Ok(b) = Brain::handoff(name, prior.clone(), now_ms) {\n 71│            return b;\n 72│        }\n 73│        thread::sleep(Duration::from_millis(10));\n 74│    }\n*75│    panic!(\"brain handoff could not connect\");\n 76│}\n 77│\n 78│#[test]\n*79│fn brain_restart_survives_gaplessly_with_fresh_gen_start() {\n*80│    const RESTARTS: u64 = 12; // >= 10 (B2 acceptance)\n 81│    let name = unique_name();\n 82│    let broker = Broker::bind(&name).expect(\"bind broker\");\n 83│\n*84│    // The broker accepts connections forever on a detached thread; it outlives\n*85│    // every brain (that is the whole point).\n 86│    let serve_broker = Arc::clone(&broker);\n 87│    thread::spawn(move || {\n 88│        let _ = serve_broker.serve();\n## idempotent.rs\n  *1│//! B5 regression — exactly-once side effects across a brain crash, on the real\n   2│//! PTY-write boundary over the versioned IPC (KNOWN-HAZARDS 7.2,\n  *3│//! [`REQ-HAZARD-RESTART-IDEMPOTENT`]; Spike #6 made real).\n   4│//!\n   5│//! A durable source of ordered input ops (each a unique line) is delivered to a\n  *6│//! single echo child the broker hosts for the whole test. The brain is crashed\n   7│//! (its connection dropped) at each of the three dangerous windows around an\n  *8│//! effect, and a fresh brain re-attaches and re-drives the source from the\n   9│//! start. The broker — the surviving party, holding the exactly-once anchor —\n  10│//! must apply every op **exactly once**: the child echoes each line once and the\n  11│//! broker's applied-set holds every op once, with no dup and no drop, across the\n    │...\n  14│//! Crash windows, per the acceptance criterion:\n *15│//!   - **before-intent** — the brain dies before issuing the op (no commitment;\n  16│//!     the re-drive from the durable source is what prevents a drop).\n *17│//!   - **before-effect** — the brain issues the op (frame on the wire) then dies\n  18│//!     before learning it landed; the broker applies it, the ack is lost.\n *19│//!   - **after-effect** — the brain sees the ack (the effect is durably applied)\n  20│//!     then dies before retiring the op from its source; the re-drive is deduped.\n  21│//!\n  22│//! Because the broker dedups by durable id, *which* life applies a given op is\n  23│//! irrelevant — the end state is exactly-once regardless. A real-process SIGKILL\n *24│//! of the brain was proven in Spike #1/#6; here the brain is a library driven\n  25│//! in-thread and a dropped connection is the crash boundary (the broker + child\n  26│//! survive; the anchor persists).\n *27│// [int->REQ-HAZARD-RESTART-IDEMPOTENT]\n  28│\n  29│use std::sync::atomic::{AtomicU32, Ordering};\n  30│use std::sync::Arc;\n    │...\n  33│\n *34│use spt_daemon::brain::{Brain, BrainState, BrokerEvent};\n  35│use spt_daemon::effect::{EffectKey, Minter, MintedOp};\n  36│use spt_daemon::msg::SpawnReq;\n  37│use spt_daemon::Broker;\n    │...\n  63│\n *64│fn cold_retry(name: &str, now_ms: u64) -> Brain {\n  65│    for _ in 0..300 {\n *66│        if let Ok(b) = Brain::cold_start(name, now_ms) {\n  67│            return b;\n  68│        }\n  69│        thread::sleep(Duration::from_millis(10));\n  70│    }\n *71│    panic!(\"brain cold-start could not connect\");\n  72│}\n  73│\n *74│fn handoff_retry(name: &str, prior: BrainState, now_ms: u64) -> Brain {\n  75│    for _ in 0..300 {\n *76│        if let Ok(b) = Brain::handoff(name, prior.clone(), now_ms) {\n  77│            return b;\n  78│        }\n  79│        thread::sleep(Duration::from_millis(10));\n  80│    }\n *81│    panic!(\"brain handoff could not connect\");\n  82│}\n  83│\n  84│#[derive(Clone, Copy, PartialEq)]\n    │...\n 118│\n*119│/// Drive one brain life: re-send the durable source `0..total` in order, draining\n 120│/// output (and acks) into `buf`, injecting `crash` on one op. Returns the\n 121│/// snapshot to hand to the next life. `buf` is shared across lives, so each\n*122│/// gaplessly-resumed output chunk is accumulated exactly once.\n*123│fn run_life(brain: &mut Brain, buf: &mut Vec<u8>, total: u64, crash: Option<(u64, Window)>) {\n 124│    for op in 0..total {\n 125│        if crash == Some((op, Window::BeforeIntent)) {\n 126│            return;\n 127│        }\n*128│        brain\n 129│            .send_effect(MintedOp::new(Minter::Rc, op), line_for(op).as_bytes())\n 130│            .expect(\"send_effect\");\n 131│        if crash == Some((op, Window::BeforeEffect)) {\n## inject_control_wedge.rs\n  22│//! the writer advances `delivered_through`). The drain hands each chunk off with a\n *23│//! bounded `send_timeout` OFF the log lock; a fully-stalled controller is detached\n  24│//! after `CONTROLLER_WRITE_DEADLINE`. So even while the controller is fully backed\n  25│//! up, the session does NOT wedge — `KIND_SESSIONS` still answers. THIS test\n  26│//! asserts that fixed behavior.\n    │...\n  45│\n *46│use interprocess::local_socket::Stream;\n  47│use spt_daemon::codec::{read_frame, write_frame};\n  48│use spt_daemon::frame::{Envelope, Role};\n  49│use spt_daemon::msg::{\n    │...\n  66│// ── W1b additions (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE): the real-fixture attach\n *67│//    path, the Brain client API, and a net-backed broker for loopback attach. ──\n  68│use std::sync::atomic::{AtomicBool, AtomicU64};\n  69│use std::sync::OnceLock;\n *70│use spt_daemon::attach::{request_attach, serve_attach};\n *71│use spt_daemon::brain::{Brain, BrokerEvent};\n  72│use spt_daemon::effect::{Minter, MintedOp};\n  73│use spt_daemon::msg::decode_bytes;\n  74│use spt_daemon::nethost::{NetConfig, NetHost};\n    │...\n  91│/// scheduler starvation can push the gate's sole elastic bound (`request_attach`'s\n *92│/// journaled `net_open_stream` ack) past 30s with NO product wedge — serviced-but-late\n  93│/// (ack <=3.8s even under pegged CPU; the 30–120s band is pure starvation noise).\n  94│/// Mirrors the `SPT_REGISTRY_BUSY_TIMEOUT_MS` env-knob precedent. The knob grants ack\n  95│/// wall-clock ONLY; it never relaxes the `subscribed` / `got_output` assertions.\n    │...\n 106│/// The pump per-IPC-call READ deadline (`io_timeout`) for every result-gating\n*107│/// `cold_start_pump` brain in this file — both attachers, both drivers, the\n 108│/// controller_by viewer, the keystroke op, AND the W2 result-gating helpers\n 109│/// (`output_contains_within` / `observe_with_xon` / `spawn_xlate_session`). NOT a\n 110│/// connect deadline: `cold_start_pump`'s socket connect is UNBOUNDED — the `Duration`\n    │...\n 112│/// every bounded read (`read_event_until`), never to `LocalSocketTransport::connect`\n*113│/// (brain.rs:311-323, :569). DEFAULT 2s — tight locally, so a REAL wedge (an ack that\n 114│/// never lands) still Errs at the deadline and the `subscribed` assert catches it. CI\n 115│/// exports `SPT_ATTACH_IPC_DEADLINE_MS=30000` ONLY on the shared self-hosted boxes,\n*116│/// where co-tenant scheduler starvation can push the journaled `net_open_stream` ack —\n 117│/// read inside `request_attach` (the gate's SOLE elastic bound) under `call_deadline`\n*118│/// (brain.rs:1066-1068) — past 2s with NO product wedge: that bounded read then Errs →\n 119│/// `request_attach` Err → the attacher sends `(false,false)` via channel at ~2.0s, a\n 120│/// full 2s BEFORE the 30s `attach_gate_watchdog` is ever reached (doyle's SERIALIZED\n 121│/// repro: 2.016s, `source=channel` — NOT a wedge, NOT a connect). DISTINCT bound from\n    │...\n 169│/// `sessions` Mutex — if a wedged per-conn handler is holding that lock the read\n*170│/// blocks and rides teardown to the 240s backstop. Probe it on a detached thread\n 171│/// (the broker is an `Arc`, so the clone keeps it alive) and give up on timeout;\n 172│/// a missed child-kill is harmless, the process-per-test exit reaps it.\n 173│fn session_pid_bounded(broker: &Arc<Broker>, sid: u64, dur: Duration) -> Option<u32> {\n    │...\n 243│\n*244│fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 245│    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 246│}\n 247│\n*248│/// Open a fresh brain connection, send one command, and return whether the\n 249│/// expected reply kind arrives within `deadline`. Runs the whole exchange on a\n 250│/// spawned thread so a BLOCKED `read_frame` (the wedge) surfaces as a timeout\n 251│/// instead of hanging the test. A `false` return = \"did not answer in time\".\n    │...\n 262│        };\n*263│        if send_hello(&mut conn, Role::Brain).is_err() {\n 264│            let _ = tx.send(false);\n 265│            return;\n 266│        }\n    │...\n 306│    let mut a = a.expect(\"connect controller A\");\n*307│    send_hello(&mut a, Role::Brain).expect(\"hello A\");\n 308│    send(\n 309│        &mut a,\n 310│        KIND_SPAWN,\n    │...\n 394│    let mut a = a.expect(\"connect controller A\");\n*395│    send_hello(&mut a, Role::Brain).expect(\"hello A\");\n 396│    send(\n 397│        &mut a,\n 398│        KIND_SPAWN,\n    │...\n 409│\n*410│    // Inject a LARGE payload into the no-binary session via a SECOND brain\n 411│    // connection. dispatch_endpoint_input finds the session but NO translation\n 412│    // binary → it SPOOLS: replies KIND_ENDPOINT_INJECTED{delivered:false} at once\n 413│    // and writes NOTHING to the PTY input (the 4 MiB never reaches write_input, so\n    │...\n 426│        };\n*427│        if send_hello(&mut b, Role::Brain).is_err() {\n 428│            let _ = tx.send(None);\n 429│            return;\n 430│        }\n    │...\n 501│// single-threaded dispatch can no longer service ANY journaled effect, and a\n*502│// concurrent `spt rc` attach dies with \"brain IPC read deadline elapsed\" (facet\n 503│// B). W1's gate only checked KIND_SESSIONS liveness; this gate uses a REAL\n*504│// rc-attach client (request_attach / serve_attach over a real loopback conn + a\n 505│// real PTY child) and asserts the attach is actually SERVICED and RECEIVES output.\n 506│//\n 507│// PLATFORM (mirrors the park-(b) finding above): Windows ConPTY ABSORBS sustained","truncation":{"content":"# crates/spt-daemon/tests/\n## access.rs\n 4|//! reply (stateful firewall), and admits a whitelisted node — with edits to\n*5|//! the store taking effect mid-stream (the gate re-reads per check).\n 6|//!\n 7|//! Two in-process brokers (hermetic loopback endpoints) play the two daemons,\n 8|//! the same harness as `wanmsg.rs`. The payload-origin spoof negative lives\n...\n 18|use spt_daemon::access::note_outbound;\n*19|use spt_daemon::brain::{Brain, BrokerEvent};\n 20|use spt_daemon::effect::{Minter, MintedOp};\n 21|use spt_daemon::nethost::{NetConfig, NetHost};\n 22|use spt_daemon::registryhost::RegistryHost;\n...\n 73|\n*74|fn connect_retry(name: &str) -> Brain {\n 75|    for _ in 0..300 {\n*76|        if let Ok(b) = Brain::cold_start(name, 1) {\n 77|            return b;\n 78|        }\n 79|        thread::sleep(Duration::from_millis(10));\n 80|    }\n*81|    panic!(\"brain could not connect\");\n 82|}\n 83|\n*84|fn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n 85|    for _ in 0..400 {\n*86|        let reply = brain.net_streams().expect(\"net-streams\");\n*87|        if let Some(s) = reply.streams.first() {\n*88|            return (s.stream_id, s.remote_id_hex.clone());\n 89|        }\n 90|        thread::sleep(Duration::from_millis(5));\n 91|    }\n*92|    panic!(\"stream never appeared at the receiver's broker\");\n 93|}\n 94|\n 95|fn offline_perch(id: &str) -> std::path::PathBuf {\n...\n 106|fn funnel_one(\n*107|    brain: &mut Brain,\n*108|    stream_id: u64,\n 109|    decoder: &mut WanDecoder,\n 110|    origin: &str,\n 111|    owlery: &std::path::Path,\n...\n 114|    loop {\n*115|        match brain.read_event().expect(\"read event\") {\n*116|            BrokerEvent::NetStreamData {\n*117|                stream_id: sid,\n 118|                bytes,\n 119|                ..\n*120|            } if sid == stream_id => {\n 121|                if let Some(rec) = decoder.push(&bytes).into_iter().next() {\n 122|                    return receive_wan(&rec, origin, owlery, registry);\n 123|                }\n 124|            }\n*125|            BrokerEvent::NetStreamEof { .. } => panic!(\"eof before the record arrived\"),\n 126|            BrokerEvent::Error { message } => panic!(\"broker error: {message}\"),\n 127|            _ => {}\n 128|        }\n...\n 145|// firewall, the whitelist untouched); (3) whitelisting the node admits its\n*146|// unsolicited traffic again — store edits take effect mid-stream, no daemon\n*147|// restart.\n 148|#[test]\n 149|fn whitelist_refuses_exempts_replies_and_admits_listed() {\n 150|    init_home();\n...\n 168|\n*169|    // Node A dials B and opens the message stream.\n 170|    let mut sender = connect_retry(&name_a);\n 171|    let a_node = sender\n 172|        .net_status()\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 5|//! Two in-process brokers (hermetic loopback endpoints, relays disabled) play\n...\n 9|//! - **remote drive**: keystrokes sent from B arrive at A's PTY through the\n*10|//!   journaled input path, and the session's output streams back to B as\n 11|//!   sequenced records — B sees its own marker echoed by A's child;\n 12|//! - **origin is transport truth** (KNOWN-HAZARDS 7.5): the serve loop gates\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 18|//!   dedups at the PTY-write journal (typed exactly once), re-transmitted\n 19|//!   output dedups at B's render cursor (rendered exactly once) — including\n 20|//!   input B sent INTO the dead window.\n...\n 27|use spt_daemon::attach::{\n*28|    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n 29|    AttachServeOutcome, ControllerIrrecoverablyBehind,\n 30|};\n 31|// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n...\n 33|use spt_daemon::attach::request_attach_endpoint;\n*34|use spt_daemon::brain::{Brain, BrainState, BrokerEvent};\n 35|use spt_daemon::effect::{Minter, MintedOp};\n 36|use spt_daemon::msg::{decode_bytes, SpawnReq};\n 37|use spt_daemon::nethost::{NetConfig, NetHost};\n...\n 45|/// Hold the temp home for the whole test binary so it isn't deleted mid-run.\n*46|/// Every test inits it: serve_attach best-effort-stamps the endpoint perch's\n 47|/// `driven_by` marker, and that resolution must never touch a real home.\n 48|static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n 49|\n...\n 84|\n*85|fn connect_retry(name: &str) -> Brain {\n 86|    for _ in 0..300 {\n*87|        if let Ok(b) = Brain::cold_start(name, 1) {\n 88|            return b;\n 89|        }\n 90|        thread::sleep(Duration::from_millis(10));\n 91|    }\n*92|    panic!(\"brain could not connect\");\n 93|}\n 94|\n 95|fn echo_spawn_req() -> SpawnReq {\n...\n 113|\n*114|/// Poll the brain's `net-streams` until a PEER-initiated stream appears; return\n*115|/// `(stream_id, remote_id_hex)`. Filtering on `!initiated_locally` is what the\n 116|/// production dispatch loop does (dispatch.rs) — and it is REQUIRED for the\n 117|/// single-broker loopback case, where the table holds BOTH the operator's own\n 118|/// row and the cross-wired peer row: the serve target is always the peer row.\n*119|fn wait_for_stream(brain: &mut Brain) -> (u64, String) {\n 120|    for _ in 0..400 {\n*121|        let reply = brain.net_streams().expect(\"net-streams\");\n*122|        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {\n*123|            return (s.stream_id, s.remote_id_hex.clone());\n 124|        }\n 125|        thread::sleep(Duration::from_millis(5));\n 126|    }\n*127|    panic!(\"attach stream never appeared at the target's broker\");\n 128|}\n 129|\n 130|/// The operator+serve drive shared by EVERY attach transport — the M12 W1.5\n## brain_decouple.rs\n*1|//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\n 2|//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\n 3|//!\n 4|//! ## The bug this gate locks down (the update-wedge root)\n 5|//!\n*6|//! A controller's writer thread does a BLOCKING socket write to its brain\n*7|//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\n*8|//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\n 9|//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\n 10|//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\n 11|//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\n...\n 14|//!\n*15|//! ## Faithful in-process analog of a suspended brain\n 16|//!\n 17|//! A test process cannot `NtSuspendProcess` itself, so the rig reproduces the exact\n 18|//! MECHANISM: controller A stops reading its socket. The flood child fills A's recv\n 19|//! buffer, the controller channel fills, and A's writer thread parks INSIDE\n*20|//! `write_frame` — precisely the state a suspended brain leaves its broker conn in\n 21|//! (a peer that stopped draining). This is the same analog the 7.12 wedge gate uses.\n*22|//! `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15 s production deadline so the\n 23|//! structural outcome is provable in ~1 s (the deadline knob grants wall-clock only;\n 24|//! it never relaxes an assertion).\n 25|//!\n...\n 27|//!\n*28|//! With the wedged controller's writer parked past the brain-write deadline:\n 29|//! 1. an attached VIEWER keeps ticking (the fan-out never blocks on the wedged conn);\n 30|//! 2. a second operator's `Control` subscribe COMPLETES — the wedged incumbent is\n 31|//!    stall-evicted broker-side and the newcomer takes the freed slot (pre-W2 this\n...\n 36|//!\n*37|//! What this rig does NOT assert (covered elsewhere / manual): resume + cursor-replay\n 38|//! byte-identity is NOT re-proven here — W2's delta does not touch the replay path, so\n 39|//! output integrity stays covered by the pre-existing ring/cursor units (the effect-\n 40|//! journal replay dedup, the `delivered_through` no-rewind CAS `advanced_cursor`, and\n 41|//! the clean-repaint cold-attach batch). A live `NtSuspendProcess` / `SIGSTOP` run on a\n*42|//! real brain child remains the MANUAL field rig from the incident night — this CI test\n 43|//! is the deterministic in-process ANALOG, not that run.\n 44|//!\n 45|//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n 46|//!\n*47|//! Shares ONE process-global `SPT_HOME` + the `SPT_BRAIN_WRITE_DEADLINE_MS` env, and\n 48|//! spins a REAL broker + PTY child. Run isolated with\n*49|//! `cargo nextest run --test brain_decouple` (process-per-test — CI's path) or\n*50|//! `cargo test --test brain_decouple -- --test-threads=1`. Never a bare\n*51|//! `cargo test --test brain_decouple` on Windows.\n 52|\n 53|use std::sync::atomic::{AtomicU32, Ordering};\n 54|use std::sync::{Arc, OnceLock};\n...\n 57|\n*58|use interprocess::local_socket::Stream;\n 59|use spt_daemon::codec::{read_frame, write_frame};\n 60|use spt_daemon::frame::{Envelope, Role};\n 61|use spt_daemon::msg::{\n...\n 71|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n*72|    format!(\"spt-daemon-braindecouple-{}-{}.sock\", std::process::id(), n)\n 73|}\n 74|\n 75|static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n...\n 121|\n*122|fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 123|    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 124|}\n 125|\n*126|fn connect(name: &str) -> Stream {\n 127|    for _ in 0..200 {\n 128|        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n*129|            send_hello(&mut c, Role::Brain).expect(\"hello\");\n 130|            return c;\n 131|        }\n 132|        thread::sleep(Duration::from_millis(10));\n...\n 151|            Ok(mut c) => {\n*152|                if send_hello(&mut c, Role::Brain).is_err() {\n 153|                    let _ = tx.send(None);\n 154|                    return;\n 155|                }\n## brain_resume_conn_deadlock.rs\n*1|//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\n 2|//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\n 3|//!\n 4|//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n*5|//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n*6|//! onto the brain's OWN request/reply conn. A conn's send half is one\n 7|//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n 8|//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\n 9|//! `write_frame`, and the dispatch reply path (`send_frame`) needs the SAME lock.\n*10|//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\n 11|//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\n*12|//! calls. When an actively-streaming session backs the conn up, the brain's own\n 13|//! subscriber writer blocks holding `send.lock()` → the heartbeat REPLY is starved →\n*14|//! the heartbeat stalls → the brain never drains → SELF-DEADLOCK. That is the field\n 15|//! wedge (both severities: respawn replay-burst + steady-state output backup), with\n 16|//! NO dead peer required.\n 17|//!\n*18|//! RED-first discipline: these tests assert the FIXED (Option A) behavior — the brain\n 19|//! heartbeat stays LIVE while sessions flood. Against the CURRENT tree (where\n*20|//! `resume_sessions` still subscribes) they go RED (the heartbeat starves). Option A\n*21|//! (resume_sessions does NOT subscribe) turns them GREEN. The observable is the number\n 22|//! of heartbeat round-trips that COMPLETE inside a fixed window — robust to the\n 23|//! std `Mutex`'s unfairness (a slow-but-progressing heartbeat still fails the RED bar,\n 24|//! a truly deadlocked one completes ~zero).\n...\n 27|//! Shares one process-global `SPT_HOME`; spins a REAL broker + REAL flood PTY children.\n*28|//! `cargo nextest run --test brain_resume_conn_deadlock` (CI path) or\n*29|//! `cargo test --test brain_resume_conn_deadlock -- --test-threads=1`. Never a bare\n 30|//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\n 31|//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\n 32|\n...\n 37|\n*38|use interprocess::local_socket::Stream;\n*39|use spt_daemon::brain::Brain;\n 40|use spt_daemon::codec::{read_frame, write_frame};\n 41|use spt_daemon::frame::{Envelope, Role};\n 42|use spt_daemon::msg::{SpawnReq, Spawned, KIND_SPAWN, KIND_SPAWNED};\n...\n 48|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n*49|    format!(\"spt-daemon-resumedeadlock-{}-{}.sock\", std::process::id(), n)\n 50|}\n 51|\n 52|static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n...\n 101|\n*102|fn connect(name: &str) -> Stream {\n 103|    for _ in 0..200 {\n 104|        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n*105|            send_hello(&mut c, Role::Brain).expect(\"hello\");\n 106|            return c;\n 107|        }\n 108|        thread::sleep(Duration::from_millis(10));\n...\n 115|/// child floods the PTY regardless, so the broker's ring keeps advancing and the\n*116|/// brain's viewer subscription keeps receiving live output.\n*117|fn spawn_flood(name: &str, endpoint: &str) -> (u64, u32, Stream) {\n 118|    let mut c = connect(name);\n 119|    write_frame(\n 120|        &mut c,\n...\n 132|\n*133|/// How the probe brain resumes before running the heartbeat: the SHIPPING daemon\n*134|/// path — `CursorOnly` → `resume_session_cursors`, no subscribe. (The hazard path,\n## brain_swap.rs\n*1|//! C0 E2E — the M3c keystone: a **brain-only self-update** swaps the brain logic\n 2|//! through the update engine with **zero hosted-endpoint interruption**\n*3|//! (REQ-UPD-3, absolute for the brain-only class — ADR-0004 §A).\n 4|//!\n*5|//! This rides the same broker/brain substrate the B2 handoff test proves, but\n*6|//! drives the swap through the *update engine* (`plan_update` → `apply_brain_only`)\n*7|//! rather than calling `Brain::handoff` directly — the engine *triggers* a\n 8|//! handoff, it does not re-implement one (`M3c-PLAN.md` §C0, clean-room posture).\n 9|//!\n 10|//! Shape: one echo child runs under a broker PTY for the whole test. The outgoing\n*11|//! brain injects a marker it never reads, then the engine applies a brain-only\n*12|//! update: the new brain re-attaches via the supplied relaunch closure (which\n*13|//! stands for \"exec the verified new binary → `Brain::handoff`\") and must find\n 14|//! that marker in the *replayed* buffer — proving the broker buffered output\n*15|//! produced while no brain was attached. Throughout, the child's pid is unchanged\n 16|//! and exactly one session is hosted: the endpoint never noticed the swap.\n 17|//!\n*18|//! **Filename note:** this file is `brain_swap.rs`, NOT `update.rs`, on purpose.\n 19|//! Windows' installer-detection heuristic (UAC) forces elevation for any\n 20|//! executable whose name contains `update`/`setup`/`install`/`patch`; a test\n 21|//! binary `update-<hash>.exe` then fails to launch with `os error 740` (requires\n...\n 24|//! **REQ-UPD-3 `int` re-pointed (restoration D7-1).** This test proves the\n*25|//! *in-process* brain-only swap shape (the engine's `apply_brain_only` over an\n*26|//! in-thread `Brain::handoff` closure) — valuable engine-mechanics coverage, but\n 27|//! NOT process-level survival. The REQ-UPD-3 `int` evidence (no endpoint drops\n*28|//! across a brain-only update) now lives in `crates/spt/tests/brain_survive.rs`,\n*29|//! which swaps a real on-disk binary and respawns the real `spt daemon brain`\n 30|//! child process (ADR-0018 V5). This file keeps exercising the engine path; it is\n 31|//! no longer the REQ-UPD-3 int tag.\n 32|\n...\n 37|\n*38|use spt_daemon::brain::{Brain, BrainState, BrokerEvent};\n 39|use spt_daemon::msg::SpawnReq;\n*40|use spt_daemon::update::{apply_brain_only, plan_update, BrokerAbi, ReleaseSpec, UpdateClass};\n 41|use spt_daemon::Broker;\n 42|\n 43|static SEQ: AtomicU32 = AtomicU32::new(0);\n...\n 67|\n*68|fn cold_retry(name: &str, now_ms: u64) -> Brain {\n 69|    for _ in 0..300 {\n*70|        if let Ok(b) = Brain::cold_start(name, now_ms) {\n 71|            return b;\n 72|        }\n 73|        thread::sleep(Duration::from_millis(10));\n 74|    }\n*75|    panic!(\"brain cold-start could not connect\");\n 76|}\n 77|\n 78|/// The relaunch closure C0's engine drives: it stands for \"the verified new\n*79|/// binary's brain re-attaches over the existing substrate.\" Retries the connect\n## broker.rs\n 4|//! The child is an echo program (`cat` on Unix, `findstr \".\"` on Windows — the\n*5|//! same cross-OS shape spt-term's injection tests use): bytes the brain injects\n 6|//! round-trip through the real PTY and come back as `output` events. We then\n 7|//! kill it and observe the `exit` event. This exercises the whole B1 contract —\n*8|//! spawn under a broker PTY, stream output, accept input, report exit — through\n 9|//! the actual named-pipe / UDS transport.\n 10|//!\n*11|//! The brain is driven **single-threaded**: send a command, then read frames\n 12|//! until the expected reply/event arrives (accumulating any interleaved output).\n*13|//! interprocess `Stream::split()` shares the underlying fd, so a split client\n*14|//! would only signal EOF once *both* halves drop; keeping one `Stream` means\n 15|//! dropping it closes the fd, the broker's command loop hits EOF, and it tears\n 16|//! the session down deterministically — which we then assert.\n 17|// [int->REQ-DAEMON-2]\n...\n 23|\n*24|use interprocess::local_socket::Stream;\n 25|use spt_daemon::codec::{read_frame, write_frame};\n 26|use spt_daemon::effect::{EffectKey, Minter};\n 27|use spt_daemon::frame::{Envelope, Role};\n...\n 62|\n*63|fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 64|    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 65|}\n 66|\n...\n 70|\n*71|    // The broker accepts one brain and serves it on a handler thread.\n 72|    let server_broker = Arc::clone(&broker);\n 73|    let server = thread::spawn(move || {\n*74|        let handle = server_broker.serve_once().expect(\"accept brain\");\n 75|        handle.join().expect(\"handler thread\");\n 76|    });\n 77|\n*78|    // Brain side: connect (retry briefly in case accept lags the bind).\n 79|    let mut conn = None;\n 80|    for _ in 0..200 {\n 81|        match LocalSocketTransport::connect(&name) {\n...\n 93|\n*94|    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n 95|\n 96|    // Spawn the echo child under a broker PTY; read until the `spawned` reply,\n 97|    // accumulating any output that interleaves.\n...\n 159|\n*160|    // Disconnect: dropping the single Stream closes the fd, so the broker's\n 161|    // command loop hits EOF and tears the session down. Joining the handler\n 162|    // (via the server thread) lets us assert the teardown happened.\n 163|    drop(conn);\n...\n 181|#[test]\n*182|fn broker_hosts_pty_child_streams_output_and_accepts_input() {\n 183|    main_test();\n 184|}\n 185|\n...\n 203|    let server = thread::spawn(move || {\n*204|        let handle = server_broker.serve_once().expect(\"accept brain\");\n 205|        handle.join().expect(\"handler thread\");\n 206|    });\n 207|\n...\n 218|    let mut conn = conn.expect(\"connect to broker\");\n*219|    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n 220|\n 221|    // Spawn the echo child LABELLED with an endpoint id (the spawn.endpoint the\n 222|    // broker tracks per session — what KIND_ENDPOINT_INPUT resolves against).\n...\n 327|    let server = thread::spawn(move || {\n*328|        let handle = server_broker.serve_once().expect(\"accept brain\");\n 329|        handle.join().expect(\"handler thread\");\n 330|    });\n 331|    let mut conn = None;\n...\n 341|    let mut conn = conn.expect(\"connect to broker\");\n*342|    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n 343|\n 344|    // A child that prints the injected env var, cross-OS.\n 345|    #[cfg(unix)]\n...\n 398|            Err(_) => {\n*399|                // After EXIT a run of read errors means the stream is fully drained\n 400|                // (no trailing output is coming) — stop. Before EXIT the frame just\n 401|                // isn't here yet, so wait and retry.\n 402|                if exited {\n...\n 450|    let mut conn = conn.expect(\"connect to broker\");\n*451|    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n 452|\n 453|    // A child that exits immediately.\n 454|    #[cfg(unix)]\n...\n 565|        let mut c = conn.expect(\"connect to broker\");\n*566|        send_hello(&mut c, Role::Brain).expect(\"send hello\");\n 567|        c\n 568|    };\n 569|\n## budget.rs\n 42|\n*43|use spt_daemon::brain::Brain;\n 44|use spt_daemon::broker::Broker;\n 45|use spt_daemon::msg::SpawnReq;\n 46|use tempfile::TempDir;\n...\n 79|\n*80|    // One probe brain per seat (kill_session targets the brain's own session).\n*81|    let mut seats: Vec<(Brain, u64, u32)> = Vec::new(); // (brain, session, child pid)\n 82|    for i in 0..n {\n*83|        let mut brain = connect_retry(&name);\n 84|        let req = SpawnReq {\n 85|            program: program.clone(),\n 86|            args: args.clone(),\n...\n 95|        };\n*96|        let (session, pid) = brain.spawn_session_pid(req).expect(\"spawn seat\");\n 97|        let pid = pid.expect(\"seat child pid\");\n*98|        seats.push((brain, session, pid));\n 99|    }\n 100|\n 101|    thread::sleep(settle);\n...\n 155|    // would remain in production). The residual is the daemon baseline.\n*156|    for (brain, _, _) in seats.iter_mut() {\n*157|        let _ = brain.kill_session();\n 158|    }\n 159|    thread::sleep(Duration::from_millis(1_500));\n 160|    let self_after = sample_pid(std::process::id()).unwrap_or_default();\n...\n 204|\n*205|fn connect_retry(name: &str) -> Brain {\n 206|    for _ in 0..300 {\n*207|        if let Ok(b) = Brain::cold_start(name, 1) {\n 208|            return b;\n 209|        }\n 210|        thread::sleep(Duration::from_millis(10));\n 211|    }\n*212|    panic!(\"brain could not connect\");\n 213|}\n 214|\n 215|/// Sample one process: RSS bytes, handle/fd count, cumulative CPU seconds.\n## conn_blackhole_lifecycle.rs\n 42|//!\n*43|//! ## Relationship to `brain_decouple.rs`\n 44|//!\n*45|//! This harness GENERALIZES `brain_decouple.rs`'s r4 gate\n 46|//! (`non_draining_controller_stall_evict_releases_writer_and_connection`, the\n 47|//! Windows-mandatory physical-retirement leg for\n 48|//! REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, which stays the primary\n 49|//! gate for that hazard). It reuses the same SEED/trigger/burst choreography,\n*50|//! the hand-back-the-unread-Stream pattern, the `conn_handler_count` and\n 51|//! `stall_evict_count` seams, the old-client-write-fails probe, and the\n 52|//! teardown-before-assert discipline. It ADDS the two pieces the r4 gate does\n 53|//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n...\n 70|//!\n*71|//! Shares ONE process-global `SPT_HOME` + `SPT_BRAIN_WRITE_DEADLINE_MS`, spins a\n 72|//! REAL broker + TWO PTY children. HEAVY class. Run isolated with\n 73|//! `cargo nextest run -p spt-daemon --test conn_blackhole_lifecycle` (CI's path)\n 74|//! or `cargo test --test conn_blackhole_lifecycle -- --test-threads=1`. NEVER a\n...\n 85|\n*86|use interprocess::local_socket::Stream;\n 87|use spt_daemon::codec::{read_frame, write_frame};\n 88|use spt_daemon::frame::{Envelope, Role};\n 89|use spt_daemon::msg::{\n...\n 119|\n*120|fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n 121|    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n 122|}\n 123|\n*124|fn connect(name: &str) -> Stream {\n 125|    for _ in 0..200 {\n 126|        if let Ok(mut c) = LocalSocketTransport::connect(name) {\n*127|            send_hello(&mut c, Role::Brain).expect(\"hello\");\n 128|            return c;\n 129|        }\n 130|        thread::sleep(Duration::from_millis(10));\n...\n 135|/// A child that FLOODS stdout forever (never reads stdin) — drives the UNRELATED\n*136|/// session so its viewer has a continuous frame stream to keep ticking on\n 137|/// throughout the black-hole incident (invariant 1).\n 138|fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139|    SpawnReq {\n...\n 160|/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n*161|/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162|fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n 163|    SpawnReq {\n 164|        program: \"powershell\".to_string(),\n...\n 183|/// Spawn a PTY child from a fresh conn and return `(spawn_conn, session_id)`.\n*184|fn spawn_session(name: &str, req: SpawnReq) -> (Stream, u64) {\n 185|    let mut c = connect(name);\n 186|    send(&mut c, KIND_SPAWN, serde_json::to_value(req).unwrap());\n 187|    let sid = loop {\n...\n 223|/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n*224|/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225|/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226|fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n 227|    let name = name.to_string();\n...\n 250|    init_home();\n*251|    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 252|    // instead of 15 s — the structural outcome, fast + deterministic (the knob\n 253|    // grants wall-clock only; it never relaxes an assertion).\n*254|    std::env::set_var(\"SPT_BRAIN_WRITE_DEADLINE_MS\", \"400\");\n 255|\n 256|    let name = unique_name();\n 257|    let broker = Broker::bind(&name).expect(\"bind broker\");\n...\n 292|    // ── BLACK-HOLED session B: the real SEED+burst PTY child from conn A, then\n*293|    //    DROP A so the ONLY persistent brain conn is the black-holed controller R. ──\n 294|    let (a, sid_b) = spawn_session(&name, seed_burst_spawn_req(\"blackhole-victim-ep\"));\n 295|    drop(a);\n 296|\n...\n 300|    //    drains a few burst frames (a real seq>0 frame delivered), and STOPS\n*301|    //    reading — handing the Stream back to main UNREAD. Main holds it, never\n 302|    //    draining, so the broker's R-writer parks in `write_frame` exactly as\n*303|    //    against a suspended brain. R's Stream is NEVER dropped until the very end. ──\n 304|    let r_name = name.clone();\n 305|    let (seed_tx, seed_rx) = std::sync::mpsc::channel::<(bool, u64)>();\n 306|    let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();\n*307|    let (hand_tx, hand_rx) = std::sync::mpsc::channel::<(Stream, u64)>();\n 308|    thread::spawn(move || {\n 309|        let mut r = connect(&r_name);\n 310|        send(\n...\n 386|        }\n*387|        // Hand the Stream back to main UNREAD (main holds it = the black-holed\n 388|        // client). The broker's R-writer stays wedged on the remaining burst.\n 389|        let _ = hand_tx.send((r, max_seq));\n 390|    });\n...\n 398|    let _ = go_tx.send(());\n*399|    // Recv the handed-back black-holed Stream (held UNREAD for the rest of the test).\n 400|    let handed = hand_rx.recv_timeout(Duration::from_secs(25)).ok();\n## control_stamp_lifetime.rs\n 11|//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n*12|//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n 13|//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n 14|//! path dropped the OutputLog with the controller slot still occupied, so the\n 15|//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\n...\n 48|\n*49|use spt_daemon::brain::Brain;\n 50|use spt_daemon::msg::SpawnReq;\n 51|use spt_daemon::Broker;\n 52|\n...\n 89|\n*90|fn connect_brain(name: &str) -> Brain {\n 91|    for _ in 0..300 {\n*92|        if let Ok(b) = Brain::cold_start(name, 1) {\n 93|            return b;\n 94|        }\n 95|        thread::sleep(Duration::from_millis(10));\n 96|    }\n*97|    panic!(\"brain could not connect to {name}\");\n 98|}\n 99|\n 100|/// A long-lived stdin-reading PTY child so the spawned session stays LIVE until we\n...\n 145|    let broker = start_broker(&name);\n*146|    let mut spawner = connect_brain(&name);\n 147|    let sid = spawner\n 148|        .spawn_session(idle_spawn_req(&id))\n 149|        .expect(\"spawn live session for the endpoint\");\n...\n 183|    // ── Kill the PTY CHILD → the exit-waiter reaps the session (NOT a conn drop, so\n*184|    //    the controller-detach path never runs; only the reap path can unlatch). ──\n 185|    let pid = broker\n 186|        .session_pid(sid)\n 187|        .expect(\"live session must have a pid before kill\");\n...\n 247|    let broker = start_broker(&name);\n*248|    let mut spawner = connect_brain(&name);\n 249|    let sid = spawner\n 250|        .spawn_session(idle_spawn_req(&id))\n 251|        .expect(\"spawn live session before its perch exists\");\n...\n 266|    for _ in 0..50 {\n*267|        let mut brain = connect_brain(&name);\n*268|        let _ = brain.sessions().expect(\"KIND_SESSIONS reply\");\n 269|        thread::sleep(Duration::from_millis(50));\n 270|        if spt_store::info::read_info(&self_perch)\n 271|            .map(|i| i.controlled)\n...\n 304|// + `driven_by` on a spt-hosted online live_agent perch AFTER the harness exited,\n*305|// persisting >20min AND ACROSS A DAEMON RESTART. This is the DIFFERENT reap leg\n 306|// from `reap_clears_control_and_viewer_stamps` above (that drives the broker\n 307|// exit-waiter's `stamp_reaped`); B3 is the `reconcile_hosted_liveness` sweep — the\n 308|// tick that clears a sticky stamp on a perch whose broker session is simply GONE,\n*309|// regardless of status, so an already-offline perch left sticky across a restart is\n 310|// swept (path iv, the boot sweep), not only on the online→offline transition.\n 311|//\n 312|// This int drives the REAL reconcile entry (`spt_daemon::livehost::reconcile_hosted_liveness`)\n 313|// against the REAL on-disk owlery, with the live-session set DERIVED FROM A REAL\n*314|// BROKER (query_live_session_endpoints's exact shape: `brain.sessions()` →\n 315|// endpoint ids). A decoy live session keeps the broker set genuinely NON-empty, so\n 316|// the reap fires precisely because the broker reports NO session for the sticky\n 317|// perch — not because we hand it an empty set. Two arms:\n...\n 319|//   (B) already-OFFLINE + sticky → cleared REGARDLESS of status (the boot sweep; a\n*320|//       restarted daemon must not resurrect the stamps).\n 321|// [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n 322|\n 323|/// Seed a spt-hosted (`controllable=Some(true)`) live_agent self-perch at `status`,\n...\n 351|    let online_id = format!(\"b3-online-{pid}\");\n*352|    // Arm B: already-OFFLINE + sticky (survived a daemon restart — the boot sweep).\n 353|    let offline_id = format!(\"b3-boot-{pid}\");\n 354|    let online_perch = seed_sticky_hosted_perch(&online_id, spt_store::liveness::STATUS_ONLINE, node);\n 355|    let offline_perch =\n...\n 364|    let broker = start_broker(&name);\n*365|    let mut spawner = connect_brain(&name);\n 366|    let decoy_sid = spawner\n 367|        .spawn_session(idle_spawn_req(&decoy_id))\n 368|        .expect(\"spawn the decoy live session\");\n...\n 372|    // `query_live_session_endpoints`: KIND_SESSIONS reply → endpoint ids.\n*373|    let mut probe = connect_brain(&name);\n 374|    let reply = probe.sessions().expect(\"KIND_SESSIONS reply\");\n 375|    let live: BTreeSet<String> = reply\n 376|        .sessions\n...\n 428|    // of status. A naive reap gated on the online→offline transition would skip this\n*429|    // (the perri F-b bug: >20min + across a daemon restart). Presence stays OFFLINE.\n 430|    // [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n 431|    assert!(\n 432|        !after_offline.controlled,\n*433|        \"B3 arm B (boot sweep): a restarted daemon must clear a sticky controlled on an \\\n 434|         ALREADY-OFFLINE perch — not resurrect it\"\n 435|    );\n 436|    assert_eq!(\n## daemon_e2e.rs\n 2|//! architecture through the real IPC in one flow: a daemon hosts a LiveAgent's\n*3|//! lifecycle as in-process loops **and** survives a brain restart with its hosted\n*4|//! PTY child + output stream intact and gapless.\n 5|//!\n 6|//! Flow (the M3b done-definition, ADR-0004 / `M3b-PLAN.md` §B9):\n 7|//!\n*8|//! 1. **spawn** — a [`Broker`] hosts a real PTY child; a [`Brain`] cold-starts,\n 9|//!    spawns the session, and round-trips a marker through it (the session is live).\n*10|//! 2. **Psyche-loop** — a [`BrainLifecycle`] (the consolidated re-host of the\n 11|//!    interim `api listen` live half, [`REQ-DAEMON-1`]) spawns the Psyche companion;\n 12|//!    the daemon marks the hosted perch `online`, so its liveness is the daemon\n 13|//!    `status`, never the summarizer pid ([`REQ-HAZARD-DAEMON-HOSTED-LIVENESS`]).\n...\n 15|//!    tick folds it in (the daemon-hosted lifecycle loop, [`REQ-DAEMON-1`]).\n*16|//! 4. **brain-restart-survives** — the brain is dropped (a crash) and handed off\n 17|//!    repeatedly; across each handoff the child's pid is unchanged, exactly one\n*18|//!    session is hosted, the output stream replays gaplessly, and `gen_start`\n 19|//!    advances ([`REQ-DAEMON-2`] / [`REQ-HAZARD-GEN-START-NOW`]). The lifecycle is\n*20|//!    stateless on disk, so it keeps ingesting communes straight across the restart.\n 21|//! 5. **graceful-signoff** — the lifecycle's graceful teardown runs the load-bearing\n 22|//!    ordering — echo-commune BEFORE teardown (3.3) — and removes the Self's ready\n 23|//!    marker ([`REQ-DAEMON-4`]).\n...\n 27|//! echoes them, exactly as B8 documents (bare `\\n` echoes a lone `\\r` and collapses\n*28|//! boundaries — the Spike #4/#5 PTY stream-contract gotcha).\n 29|//!\n 30|//! Single `#[test]` on purpose: the flow owns the process-global `SPT_HOME`, so it\n 31|//! must not race a sibling test in this binary.\n*32|// REQ-DAEMON-1 `int` re-pointed to crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs\n*33|// (M11-W0.4 (i)): the REAL brain PROCESS (run_brain) hosts the Psyche, retiring\n*34|// this file's type-level BrainLifecycle::with_config-direct over-claim (it kept\n 35|// green coverage over an UNWIRED production path — the M3-era false-green). This\n 36|// file keeps its REQ-DAEMON-4 + hazard int evidence (the lifecycle machinery +\n 37|// daemon-hosted-liveness), which it still exercises directly.\n*38|// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration\n 39|// D7-1): the M3b-B9 flow proves the daemon-hosted lifecycle + an *in-process*\n*40|// brain-restart survival, not the process-level swap. Process-level endpoint\n*41|// survival across a brain-PROCESS restart onto a swapped binary is the D7-1 E2E.\n 42|// [int->REQ-DAEMON-4]\n 43|// [int->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n 44|\n...\n 49|\n*50|use spt_daemon::brain::BrainState;\n 51|use spt_daemon::msg::SpawnReq;\n*52|use spt_daemon::{Brain, BrainLifecycle, Broker, DaemonConfig};\n 53|use spt_runtime::Manifest;\n 54|use spt_store::perch::{self, ParentHint};\n 55|\n...\n 108|\n*109|fn cold_retry(name: &str, now_ms: u64) -> Brain {\n 110|    for _ in 0..300 {\n*111|        if let Ok(b) = Brain::cold_start(name, now_ms) {\n 112|            return b;\n 113|        }\n 114|        thread::sleep(Duration::from_millis(10));\n 115|    }\n*116|    panic!(\"brain cold-start could not connect\");\n 117|}\n 118|\n*119|fn handoff_retry(name: &str, prior: BrainState, now_ms: u64) -> Brain {\n 120|    for _ in 0..300 {\n## daemon_lifecycle_real_brain.rs\n 2|//! `[int->REQ-DAEMON-1]` evidence OFF the type-level `daemon_e2e` (which\n*3|//! constructs a [`BrainLifecycle`] directly — green coverage over an UNWIRED\n*4|//! production path, the M3-era false-green) ONTO the actual brain entry point:\n 5|//!\n*6|//! a [`Broker`] + `brainproc::run_brain` (the real brain's `main`-equivalent,\n 7|//! which since M11-W0.1 runs `livehost::spawn_live_host`) HOSTS a real ONLINE live\n 8|//! endpoint and DRIVES its pulse loop — proven by the loop ingesting a commune into\n*9|//! the live-context tier. The reconcile that hosts is the REAL brain's, not an in-\n*10|//! test `reconcile_once` call. A cold `run_brain` over an already-online perch IS\n*11|//! the post-brain-restart rehost: the perch's `status=online` survives the brain\n*12|//! process, and the fresh brain re-hosts the lifecycle at boot (daemon-authoritative\n 13|//! liveness, CONTEXT:194).\n 14|//!\n 15|//! **F-030 W1:** the Psyche is now a PER-EVENT TURN, not a resident process, so this\n 16|//! no longer waits for a resident `<id>-psyche` perch to bind (host_one spawns none)\n*17|//! and reaps no detached child. The DAEMON-1 evidence is the hosted pulse loop\n 18|//! ingesting the commune — unchanged and independent of the retired resident spawn.\n 19|//!\n 20|//! Single `#[test]`: it owns the process-global `SPT_HOME`.\n...\n 26|\n*27|use spt_daemon::brainproc::{run_brain, StartReason};\n 28|use spt_daemon::endpoint::broker_socket_name;\n 29|use spt_daemon::Broker;\n 30|use spt_store::liveness::STATUS_ONLINE;\n...\n 33|// A no-op `psyche_init` command. F-030 W1 host_one spawns NO resident psyche, but\n*34|// `BrainLifecycle::with_config_in` still REQUIRES the manifest to declare the role,\n 35|// so it stays present (never spawned). The pulse loop drives regardless — commune\n 36|// ingest is independent of any psyche process.\n 37|#[cfg(windows)]\n...\n 42|#[test]\n*43|fn real_brain_process_hosts_the_psyche_for_an_online_live_endpoint() {\n 44|    let home = tempfile::tempdir().unwrap();\n 45|    std::env::set_var(\"SPT_HOME\", home.path());\n 46|\n*47|    // A live-capable adapter, registered so the brain resolves the manifest from\n 48|    // disk by the perch's adapter (the production resolve path).\n 49|    let drops = home.path().join(\"drops\");\n 50|    std::fs::create_dir_all(&drops).unwrap();\n...\n 64|    // establish→online path is proven by live_firsthost/live_bind ints; here the\n*65|    // focus is the REAL brain hosting it). status=online survives the brain.\n 66|    let id = \"agent9\";\n 67|    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 68|    std::fs::create_dir_all(&perch_path).unwrap();\n...\n 73|\n*74|    // ── the REAL brain: a Broker + brainproc::run_brain on a thread (NOT a direct\n*75|    // BrainLifecycle::with_config). run_brain runs spawn_live_host, so the reconcile\n*76|    // hosting the Psyche is the real brain's, at boot.\n 77|    let broker = Broker::bind(&broker_socket_name()).expect(\"bind broker\");\n 78|    {\n 79|        let serve = Arc::clone(&broker);\n...\n 84|    thread::spawn(|| {\n*85|        let _ = run_brain(0, StartReason::Cold);\n 86|    });\n 87|\n*88|    // The real brain's pulse loop runs (F-030 W1: NO resident psyche is spawned —\n 89|    // the DAEMON-1 evidence is the hosted loop ticking, not a bound `<id>-psyche`\n 90|    // perch): a commune drop is ingested into the live context tier (proves the loop\n*91|    // ticks in the brain, not a test-driven tick).\n 92|    std::fs::write(\n 93|        drops.join(format!(\"{id}-commune.md\")),\n*94|        \"Source: self-commune\\nreal-brain note\",\n 95|    )\n 96|    .unwrap();\n 97|    let ctx = spt_store::contextstore::live_context_file(&home.path().join(\"tracked\"), id);\n## digest.rs\n 13|//!   (impossible under the superseded byte-parse shape);\n*14|//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows\n 15|//!   delivers the structured delta to a subscriber (the push-driven freshness\n 16|//!   the `api digest-entry` path also drives — autonomous file-watch is deferred\n 17|//!   to the consuming frontend milestone).\n...\n 37|/// A stdin pass-through \"extractor\": the source bytes (piped in) round-trip to\n*38|/// stdout unchanged, so the fixture JSONL *is* the contract-record stream.\n 39|#[cfg(unix)]\n 40|const PASSTHROUGH: &str = \"cat\";\n 41|#[cfg(windows)]\n...\n 130|\n*131|    // ── Delta-stream: a reproject after the log grows delivers a delta ───────\n 132|    let fixture2 = home.path().join(\"cc2-session.jsonl\");\n 133|    std::fs::write(&fixture2, \"{\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"first\\\"}\\n\").unwrap();\n 134|    establish_cc_endpoint(\"cc2\", &fixture2);\n## dispatch.rs\n 1|//! Inbound dispatch E2E (D9-1-1): the serve half of every wire protocol runs\n*2|//! **undriven** — no probe brain hand-wires `serve_*`; the daemon-side\n*3|//! dispatcher claims each peer-initiated stream, classifies it by first\n 4|//! record, and serves it. This is the production behavior the D5–D8 E2E\n 5|//! suites deferred (\"the daemon dispatch\").\n 6|//!\n...\n 17|\n*18|use spt_daemon::brain::Brain;\n 19|use spt_daemon::broker::Broker;\n 20|use spt_daemon::effect::{Minter, MintedOp};\n 21|use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};\n...\n 84|\n*85|fn connect_retry(name: &str) -> Brain {\n 86|    for _ in 0..300 {\n*87|        if let Ok(b) = Brain::cold_start(name, 1) {\n 88|            return b;\n 89|        }\n 90|        thread::sleep(Duration::from_millis(10));\n 91|    }\n*92|    panic!(\"brain could not connect\");\n 93|}\n 94|\n 95|/// Stand B's dispatcher up exactly as the daemon does, returning its stop\n...\n 132|        last_seen: String::new(),\n*133|        lease_epoch: 1,\n 134|    });\n 135|    roster.save().expect(\"save roster\");\n 136|    let mut vis = VisibilityStore::load();\n...\n 142|\n*143|fn node_hex(brain: &mut Brain) -> String {\n*144|    brain\n 145|        .net_status()\n 146|        .expect(\"status\")\n 147|        .node_id_hex\n...\n 230|        artifact_sha256: sha256_hex(&artifact),\n*231|        brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,\n 232|        broker_resource_abi: BROKER_RESOURCE_ABI,\n 233|        product_version: String::new(),\n 234|    };\n...\n 316|    let conn = a.net_dial(addr, Some(MintedOp::new(Minter::Cli, op()))).expect(\"dial\");\n*317|    let opened = a.net_open_stream(conn.conn_id, Some(MintedOp::new(Minter::Cli, op()))).expect(\"open\");\n*318|    a.net_stream_send(opened.stream_id, &line, None, true)\n 319|        .expect(\"send feed\");\n 320|\n 321|    // The dispatcher applies asynchronously — poll B's canonical store.\n...\n 343|// and the direction fact is recorded right: A's table shows its opened\n*344|// stream as locally-initiated; B's shows the same stream as peer-initiated\n 345|// (what the dispatcher claimed it by).\n 346|#[test]\n 347|fn dispatcher_funnels_wan_messages_undriven() {\n...\n 367|    let conn = a.net_dial(addr, Some(MintedOp::new(Minter::Cli, op()))).expect(\"dial\");\n*368|    let opened = a.net_open_stream(conn.conn_id, Some(MintedOp::new(Minter::Cli, op()))).expect(\"open\");\n 369|    let msg = WanMessage {\n 370|        target: target.to_string(),\n 371|        from: \"doyle\".to_string(),\n...\n 374|    };\n*375|    a.net_stream_send(\n*376|        opened.stream_id,\n 377|        &spt_net::net::ndjson::encode_line(&msg),\n 378|        None,\n 379|        true,\n...\n 382|\n*383|    // Direction fact, both sides of the same stream.\n*384|    let a_rows = a.net_streams().expect(\"A streams\").streams;\n 385|    assert!(\n 386|        a_rows\n 387|            .iter()\n*388|            .any(|s| s.stream_id == opened.stream_id && s.initiated_locally),\n 389|        \"A's own open is locally-initiated in A's table\"\n 390|    );\n*391|    let b_rows = b_probe.net_streams().expect(\"B streams\").streams;\n 392|    assert!(\n 393|        b_rows.iter().all(|s| !s.initiated_locally),\n 394|        \"every row at B is peer-initiated (B opened nothing)\"\n## docs_server_e2e.rs\n 8|//!\n*9|//! Hermetic: port 0 (ephemeral), raw std TcpStream client — no HTTP client\n 10|//! dev-dep. The server thread is the production `docshost::start`.\n 11|//\n 12|// [int->REQ-DOCS-LOCAL-SERVER]\n...\n 14|use std::io::{Read, Write};\n*15|use std::net::TcpStream;\n 16|use std::path::Path;\n 17|\n 18|/// Minimal HTTP/1.1 request over loopback; returns (status-line, headers,\n...\n 20|fn http(port: u16, request_line: &str) -> (String, String, Vec<u8>) {\n*21|    let mut stream = TcpStream::connect((\"127.0.0.1\", port)).expect(\"connect docs server\");\n 22|    write!(\n*23|        stream,\n 24|        \"{request_line}\\r\\nHost: localhost:{port}\\r\\nConnection: close\\r\\n\\r\\n\"\n 25|    )\n 26|    .unwrap();\n 27|    let mut raw = Vec::new();\n*28|    stream.read_to_end(&mut raw).expect(\"read response\");\n 29|    let split = raw\n 30|        .windows(4)\n 31|        .position(|w| w == b\"\\r\\n\\r\\n\")\n## driven_by_selfheal.rs\n 6|//! An spt-hosted endpoint can latch `ONLINE+CONTROLLED` — `info.json`\n*7|//! `driven_by = Some(node)` — and never clear it when a controller's detach is\n 8|//! LOST (no clean disconnect, and no output to trigger the W1 drain-evict\n 9|//! `mark_controller_gone`). The picker then renders a phantom \"controlled by X\"\n 10|//! forever. `reconcile_hosted_liveness` (`livehost.rs`) is the pull-primary\n...\n 35|//!   an idle session whose ABANDONED REMOTE controller wedged without a clean\n*36|//!   detach — keeps `controller_by==Some(origin)`, which a reconcile read can't\n 37|//!   detect at all. That case is its own deferred requirement,\n 38|//!   `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT` (broker-side controller eviction —\n 39|//!   NOT a reconcile `controller_by` clear). See `inject_control_wedge.rs` `w5_a2`\n...\n 52|//! It also captures the broker's OWN self-heal on an OBSERVED controller\n*53|//! disconnect (per-conn `detach_if`→`clear_controller`→`stamp_driven_by`).\n 54|//!\n 55|//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n 56|//!\n...\n 73|\n*74|use spt_daemon::brain::Brain;\n 75|use spt_daemon::msg::SpawnReq;\n 76|use spt_daemon::Broker;\n 77|use spt_daemon::livehost::reconcile_hosted_liveness;\n...\n 120|\n*121|/// Connect a Brain to the broker with bounded retry (cold_start — the request-reply\n*122|/// carrier `Brain::sessions()` rides; KIND_SESSIONS is a single reply, no streaming,\n 123|/// so a plain Whole conn is fine here).\n*124|fn connect_brain(name: &str) -> Brain {\n 125|    for _ in 0..300 {\n*126|        if let Ok(b) = Brain::cold_start(name, 1) {\n 127|            return b;\n 128|        }\n 129|        thread::sleep(Duration::from_millis(10));\n 130|    }\n*131|    panic!(\"brain could not connect to {name}\");\n 132|}\n 133|\n 134|/// A trivial long-lived PTY child so a spawned session stays LIVE (Gap A needs a\n...\n 156|/// spt-hosted (`controllable=Some(true)`) `live_agent` self-perch, `status=online`,\n*157|/// pre-stamped `driven_by=Some(node)` (the lost-detach latch). Mirrors the\n 158|/// livehost-bootgate-e2e stamping exactly, plus the `driven_by` marker.\n 159|fn stamp_latched_perch(id: &str, node: &str) -> std::path::PathBuf {\n 160|    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);\n...\n 166|    spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_ONLINE).unwrap();\n*167|    // The lost-detach latch the self-heal must clear.\n 168|    spt_store::info::set_driven_by(&self_perch, Some(node)).unwrap();\n 169|    self_perch\n 170|}\n...\n 191|    let _broker = start_broker(&name);\n*192|    let mut brain = connect_brain(&name);\n*193|    let reply = brain.sessions().expect(\"KIND_SESSIONS reply\");\n 194|    let live_sessions: BTreeSet<String> =\n 195|        reply.sessions.iter().map(|s| s.endpoint.clone()).collect();\n 196|\n...\n 270|    // ── A REAL broker hosting a LIVE session for THIS endpoint. The spawning\n*271|    //    brain is pre-attached as the LOCAL controller (dispatch_spawn →\n 272|    //    become_controller(by=None)). The PTY child keeps the session live in the\n 273|    //    broker table. ──\n 274|    let name = unique_name();\n 275|    let broker = start_broker(&name);\n*276|    let mut spawner = connect_brain(&name);\n 277|    let sid = spawner\n 278|        .spawn_session(idle_spawn_req(&id))\n 279|        .expect(\"spawn live session for the endpoint\");\n...\n 281|    // FINDING (the broker's OWN self-heal on a controller disconnect): dropping the\n*282|    // spawner socket makes the broker's per-conn handler run `detach_if` →\n 283|    // `clear_controller` → `stamp_driven_by`, which (since controller_by() is now\n 284|    // None) RE-STAMPS the perch's driven_by to None. So an OBSERVED controller\n 285|    // disconnect already unlatches via the broker single-writer — even without a\n*286|    // clean detach. The Gap-A the W5 RECONCILE must heal is the path where that\n*287|    // re-stamp NEVER fires: a brain restart re-adopts the live session but the\n 288|    // in-memory controller (and thus the clear_controller re-stamp) never existed,\n 289|    // so the STALE on-disk driven_by marker persists against a controller_by==None\n 290|    // session. We reproduce exactly THAT state below.\n...\n 300|         driven_by_after_spawner_drop={driven_by_after_disconnect:?} \\\n*301|         (None = the broker's detach_if→clear_controller→stamp_driven_by already \\\n 302|         unlatched the OBSERVED disconnect — so the RECONCILE Gap-A is only the \\\n*303|         NEVER-RE-STAMPED brain-restart leftover, reproduced next) ===\"\n 304|    );\n 305|\n## endpoint_survival.rs\n*1|//! LIFECYCLE-TRUTH W3 (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) — daemon-restart endpoint\n 2|//! survival, against a REAL broker over the real local IPC.\n 3|//!\n 4|//! ## The bug this gate locks down (seed #6, the field-accept blocker)\n...\n 8|//! though their records survived (info.json status + adapter + cwd + a ledger row). The\n*9|//! fix: `livehost::resume_restart_orphaned_endpoints` runs ONCE at the fresh broker's\n 10|//! live-host start and RE-RUNS the harness of every previously-ONLINE spt-hosted\n*11|//! endpoint the restart orphaned, resuming from its last ledger session.\n 12|//!\n 13|//! ## What this rig proves\n 14|//!\n...\n 18|//! session. RED-first: WITHOUT the pass the broker has no session for it (it stays\n*19|//! offline). The two belts are unit-proven in `livehost::restart_resume_gate_*`; this\n 20|//! rig proves the end-to-end re-run against the real broker + a real spawned harness.\n 21|//!\n 22|//! ## RUNNER CONTRACT — process-per-test (nextest) or single-threaded\n...\n 30|\n*31|use spt_daemon::brain::now_ms;\n*32|use spt_daemon::{Brain, Broker};\n 33|\n*34|/// A long-lived, quiet harness the `[session.self]` resume spawns — it stays running\n 35|/// (so the broker session persists for the assertion) and produces no output.\n 36|#[cfg(windows)]\n 37|const HARNESS_CMD: &str = \"cmd /c ping -n 600 127.0.0.1 >nul\";\n...\n 40|\n*41|/// Register a live-capable adapter whose `[session.self]` (the is_resume fallback when\n*42|/// no `[session.resume]`) spawns the long-lived quiet harness, so\n 43|/// `launch_harness_brokered_in` has a real program to run.\n*44|fn register_resume_adapter(name: &str) {\n 45|    let toml = format!(\n 46|        \"[adapter]\\nname=\\\"{name}\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\n 47|         min_spt_core_version=\\\"0\\\"\\n\\n\\\n...\n 53|    spt_runtime::registry::register(&spt_store::perch::adapters_dir(), &src, 1000)\n*54|        .expect(\"register the resume adapter\");\n 55|}\n 56|\n 57|/// Seed a previously-ONLINE, spt-hosted (controllable), relay-less endpoint with a\n*58|/// ledger row to resume from — the daemon-restart-orphaned shape.\n 59|fn seed_orphaned_online_endpoint(id: &str, adapter: &str) {","truncated":true,"truncatedBy":"bytes","totalLines":1452,"totalBytes":69522,"outputLines":1080,"outputBytes":51136,"lastLinePartial":false,"firstLineExceedsLimit":false},"meta":{"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":1452,"totalBytes":69522,"outputLines":1080,"outputBytes":51136,"shownRange":{"start":1,"end":1080},"nextOffset":1081}}},"isError":false,"timestamp":1784195875651}}
{"type":"message","id":"56771489","parentId":"58e21912","timestamp":"2026-07-16T09:58:06.075Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Assessing connection ID lifecycle**"},{"type":"toolCall","id":"call_Ceqrqvq4xNoYcxd0cGiQ4hD2|fc_047bb49263dfa303016a58ab2d7bfc8198ac5d2db9767dbd09","name":"read","arguments":{"i":"Reading dispatch ownership model","path":"crates/spt-daemon/src/dispatch.rs","selector":"1-70,188-235,255-405"}},{"type":"toolCall","id":"call_sGzDMzGFX2YKjvFGGQivgIPh|fc_047bb49263dfa303016a58ab2d7c0c8198ad531e6d67aa5c79","name":"read","arguments":{"i":"Reading stream subscriber ownership","path":"crates/spt-daemon/src/nethost.rs","selector":"151-295,448-705,1380-1533"}},{"type":"toolCall","id":"call_Z1VEJrIMqSajGDa5rhpK3rz7|fc_047bb49263dfa303016a58ab2d7c188198b59c8118a531cd6f","name":"read","arguments":{"i":"Reading broker connection identifiers","path":"crates/spt-daemon/src/conn.rs","selector":"1-285"}},{"type":"toolCall","id":"call_TKv0kXv0UGEcuCVFtvv0ABF4|fc_047bb49263dfa303016a58ab2d7c248198a7bb87feed9d5368","name":"grep","arguments":{"i":"Locating conn IDs lifecycle","pattern":"NEXT_CONN|conn_id|alloc.*conn|BrokerConn::|ConnId|register.*conn|detach_stream_sub|handle_conn\\(","path":"crates/spt-daemon/src","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_nfM7pZBFKYAnSxeGPUJpLxN1|fc_047bb49263dfa303016a58ab2d7c308198aca7f642dabe556b","name":"grep","arguments":{"i":"Finding dispatch restart test","pattern":"run_dispatch_loop\\(|claimed|restart.*dispatch|dispatch.*restart|re-serve.*stream|SAME stream","path":"crates/spt-daemon/tests;crates/spt/tests;crates/spt-daemon/src","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":31519,"output":420,"cacheRead":82432,"cacheWrite":0,"totalTokens":114371,"reasoningTokens":113,"cost":{"input":0.157595,"output":0.0126,"cacheRead":0.041215999999999996,"cacheWrite":0,"total":0.21141100000000002}},"stopReason":"toolUse","timestamp":1784195875697,"responseId":"resp_047bb49263dfa303016a58ab240ec48198acc6ccece610d1af","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58ab26ea788198a1a00247cb630434","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKstG-sR0xWuoMn-DnkSpfALOPFp_J-H4iB4W3WJcT5FXyIMVlPRj3wZEHz8a4sBlhhcgS0uZj-GKN-smRfTnyQ8CwOvC4YYdeTp2CDwLmtmMVSt2lJFAqR_esLKaCv2jwhQU9ZkKaoL_8JJHoyXB8s7eQ7XEWoZNo11OEKAG0X4_0dw4uyMOjiqBXkcVCiws4dt-vi188m8Rj7j5YXWxP8Lii6pwv-oMOr3lN5mEbrIxNX6LZRyabv18M8pndBzKONHsSvyBDSYeJ8fQYln54jQR54VxC1pKoyTdwMLmPRnyP4Q4UrPqitxe0oAupg7hmbWrevaOPWyfu9dmrUMsuvD27aslhDe-ZT1seV7-Z82ij1GXwUeaGMOuYhk9vR0buD12Y-9qwvpxYDBOxk8k0-cP_H9MevI6-b6VzdjmQQ6971M2VuX_U8HFMDGBUNBO1EfYRD-owH6-B1xxBZBpWXhl456UDwAo3gtRA3KcVjgxQRJ1Z1VGbBQtpETdCxnu0_PTq_PWbZwQh1Lz2nzkfCVQiCL4-C16d0DZVdaRniUhkpQYfxh5YUP1YhnygSxtvT457tH0KPFrbl63xFpNQBoZqhlqIavd9SUrmhqEa_VWtiAUbPBu6tSUBBkjLL0lTKVROdNm1MCo3SABNHShf2N1I2QceDFFrbCIIZR_73VdGS22N7Ad4Eg8GfKvxwHDUHWmN94Vtn5-7jwHiPkTiAyk-8R6NQtr1M1LIFl2Ag8NTmHuxmJAe4DkaNFxg-bEt8WBGFzpOXca8TeQRvZb_cdFu3HBrJy7SrYYifSs1Roe8vus1snBNFs6XP28jbX4r-RaerlvKspqA9RbQMb6aLrCs0JSfpNSldp6qduFKmTgy_3u8vrrkGiG3gULAdaCgFhq7RCUsCnWT_iA16ApfPECveSw5dp4vO0pwVZHIeRXHYCuqNWUd-wakbJUmo4VlPODg8fQBGe_a7XXi7jOHhZOs1j7P8jk4KrT9UDTLqhwLlltm1E2OcigNoNh7u_-pwJR38DQrNzMgivMt11lHMk0OwpgNHETPGvA7YF-namZdCWuvAQe4B7mICk6dl0EfOAmDFDZPT6mCu0qwtLla8f8I8sXf3QPcoB-J5jmlsNhcrN2ycUxJiQwlvC4OqezoSdj07nV1dYoQmhPFhMF7fPUH_n9pCVn_LV_3rI_GYxfQl1n8JP7fvJpBepWK6MF8v8buwr4XiXg1i5c4ega-nflgUGqetE-ZwbIz6RHJ0Ckv1IsPvSeugJDQ9CGgEuAYISzdzleNGhWJ4gs8F93jcM7Rips3kNepWGUbuKzZUvltJg7vIE0nJK46hE8rHv2iYH6HFJEfUJfRMatTUHLH3oAtJFsCJJHKgaJIaF3X3mQWUMBwu2hg3DHgnInwDEiL6mcSdztYYN3Ph_aD8-FaLbrR9Bc2pvRqL-cMldnunzDEBzdD2EW_oYc9D0zDPWYynoZr_b3JSnfEQHg3XABGlhG4ghy8osBe-KjQlpRKLe14n1W1lQaZwtXhulVN_dCU9WZTB_CuBB_bRsfiGwbPoCSHG_57T7BDKV-vzLlW0wwTSw1C0BoxnMTdrpFIfUB7RnAaZqqYpGVft7RomriUK5kIzPt5_wY4SqE8RrBdezvOX6zyVft8SRnId-7jooecBBlBHqzgpMBlYMvGNxO2Gx0w==","summary":[{"type":"summary_text","text":"**Assessing connection ID lifecycle**"}]},{"id":"fc_047bb49263dfa303016a58ab2d7bfc8198ac5d2db9767dbd09","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatch ownership model\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"1-70,188-235,255-405\"}","call_id":"call_Ceqrqvq4xNoYcxd0cGiQ4hD2","name":"read"},{"id":"fc_047bb49263dfa303016a58ab2d7c0c8198ad531e6d67aa5c79","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading stream subscriber ownership\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":\"151-295,448-705,1380-1533\"}","call_id":"call_sGzDMzGFX2YKjvFGGQivgIPh","name":"read"},{"id":"fc_047bb49263dfa303016a58ab2d7c188198b59c8118a531cd6f","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker connection identifiers\",\"path\":\"crates/spt-daemon/src/conn.rs\",\"selector\":\"1-285\"}","call_id":"call_Z1VEJrIMqSajGDa5rhpK3rz7","name":"read"},{"id":"fc_047bb49263dfa303016a58ab2d7c248198a7bb87feed9d5368","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating conn IDs lifecycle\",\"pattern\":\"NEXT_CONN|conn_id|alloc.*conn|BrokerConn::|ConnId|register.*conn|detach_stream_sub|handle_conn\\\\(\",\"path\":\"crates/spt-daemon/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_TKv0kXv0UGEcuCVFtvv0ABF4","name":"grep"},{"id":"fc_047bb49263dfa303016a58ab2d7c308198aca7f642dabe556b","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding dispatch restart test\",\"pattern\":\"run_dispatch_loop\\\\(|claimed|restart.*dispatch|dispatch.*restart|re-serve.*stream|SAME stream\",\"path\":\"crates/spt-daemon/tests;crates/spt/tests;crates/spt-daemon/src\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_nfM7pZBFKYAnSxeGPUJpLxN1","name":"grep"}]},"duration":10376.95090000052,"ttft":3212.418700000271,"contextSnapshot":{"promptTokens":113951,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Ceqrqvq4xNoYcxd0cGiQ4hD2|fc_047bb49263dfa303016a58ab2d7bfc8198ac5d2db9767dbd09","toolName":"read","startedAt":"2026-07-16T09:58:06.076Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading dispatch ownership model"},"id":"a238a92a","parentId":"56771489","timestamp":"2026-07-16T09:58:06.076Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_sGzDMzGFX2YKjvFGGQivgIPh|fc_047bb49263dfa303016a58ab2d7c0c8198ad531e6d67aa5c79","toolName":"read","startedAt":"2026-07-16T09:58:06.076Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Reading stream subscriber ownership"},"id":"f442e990","parentId":"a238a92a","timestamp":"2026-07-16T09:58:06.076Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Z1VEJrIMqSajGDa5rhpK3rz7|fc_047bb49263dfa303016a58ab2d7c188198b59c8118a531cd6f","toolName":"read","startedAt":"2026-07-16T09:58:06.076Z","args":{"path":"crates/spt-daemon/src/conn.rs"},"intent":"Reading broker connection identifiers"},"id":"ad2d2d6e","parentId":"f442e990","timestamp":"2026-07-16T09:58:06.076Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_TKv0kXv0UGEcuCVFtvv0ABF4|fc_047bb49263dfa303016a58ab2d7c248198a7bb87feed9d5368","toolName":"grep","startedAt":"2026-07-16T09:58:06.076Z","args":{"path":"crates/spt-daemon/src"},"intent":"Locating conn IDs lifecycle"},"id":"c4b9989f","parentId":"ad2d2d6e","timestamp":"2026-07-16T09:58:06.076Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_nfM7pZBFKYAnSxeGPUJpLxN1|fc_047bb49263dfa303016a58ab2d7c308198aca7f642dabe556b","toolName":"grep","startedAt":"2026-07-16T09:58:06.076Z","args":{"path":"crates/spt-daemon/tests;crates/spt/tests;crates/spt-daemon/src"},"intent":"Finding dispatch restart test"},"id":"b7acac49","parentId":"c4b9989f","timestamp":"2026-07-16T09:58:06.076Z"}
{"type":"message","id":"ab360cba","parentId":"b7acac49","timestamp":"2026-07-16T09:58:06.085Z","message":{"role":"toolResult","toolCallId":"call_Ceqrqvq4xNoYcxd0cGiQ4hD2|fc_047bb49263dfa303016a58ab2d7bfc8198ac5d2db9767dbd09","toolName":"read","content":[{"type":"text","text":"//! Inbound net-stream dispatch (D9-1) — the daemon-side acceptor that turns\n//! the hand-wired serve loops of D5–D8 into production behavior.\n//!\n//! Every E2E test so far drove the serve half manually (a probe brain calling\n//! `serve_sync` / `serve_update` / `apply_notif_feed` / `receive_wan` against\n//! a known stream). In production nothing did — a peer could dial and write\n//! and the bytes sat in the broker's stream log unread. This module is the\n//! missing consumer: a daemon-hosted loop that watches the broker's stream\n//! table for **peer-initiated** streams, classifies each by its first NDJSON\n//! record, and hands it to the right serve loop on its own worker thread.\n//!\n//! ## Direction (the gotcha-#9 fact, now first-class)\n//!\n//! The stream table holds locally-opened outbound streams too. The dispatcher\n//! claims only rows with `initiated_locally == false` — serving our own\n//! requester stream would deadlock the requester against ourselves. The flag\n//! is broker-recorded at registration (`open_stream` vs the accept loop),\n//! never inferred.\n//!\n//! ## Classification (first-record demux)\n//!\n//! Streams are single-purpose; the opener's first record names the protocol.\n//! The kind-tagged families collide on variant names (`request` is both\n//! sync's and attach's opener; `chunk`/`done`/`err` recur everywhere), so the\n//! demux keys on the first line's **shape**, not its kind alone:\n//!\n//! | first line | family |\n//! |---|---|\n//! | `kind=request` + `sync_id` | sync pull ([`crate::sync::serve_sync`]) |\n//! | `kind=request` + `session_id` | remote-drive attach (D9-1-3) |\n//! | `kind=request` + `endpoint`+`event` | remote rest op ([`crate::resthost::serve_rest`]) |\n//! | `kind=request` + `owner`+`shell_ref` | cross-node shell link ([`crate::linkhost::serve_shell_link`]) |\n//! | `kind=query` | update pull ([`crate::propagate::serve_update`]) |\n//! | `kind=row` | notif feed ([`crate::notifsync::apply_notif_feed`]) |\n//! | `kind=fetch` / `kind=push` | file transfer (D9-1-3) |\n//! | untagged + `target`+`op_id` | WAN message feed ([`crate::wan::receive_wan`]) |\n//! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |\n//! | anything else | unknown — dropped, loud |\n//!\n//! A family whose production wiring lands in a later slice is still\n//! classified here (the table is complete) but routed to a typed\n//! [`Unwired`](DispatchOutcome::Unwired) drop — loud, never silent.\n//!\n//! ## Worker model\n//!\n//! Each claimed stream gets its own worker thread with its **own** brain IPC\n//! connection (the broker is multi-client by design; the probe-brain pattern\n//! every E2E test uses). The peek and the serve use **separate** connections:\n//! the peek brain subscribes from 0 to read the first line and is dropped;\n//! the serve loop then subscribes from 0 on a fresh connection so it owns its\n//! stream's event feed alone (no double-subscription interleave). Origin is\n//! the table's `remote_id_hex` — handshake-proven, never payload\n//! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.\n// [impl->REQ-NET-1]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n\nuse std::collections::HashSet;\nuse std::io;\nuse std::path::PathBuf;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::Duration;\n\nuse spt_net::net::notif::NotifDecoder;\nuse spt_net::net::replicate::{\n    FeedDecoder as RegistryDecoder, NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate,\n};\nuse spt_net::net::wanmsg::{WanDecoder, WanReply};\nuse spt_store::contextstore::ContextStore;\n…\nimpl DispatchPaths {\n…\n    pub fn production() -> DispatchPaths {\n…\n        DispatchPaths {\n…\n            releases: home.join(\"releases\"),\n            scratch: home.join(\"scratch\").join(\"net\"),\n        }\n    }\n}\n\n/// Watch the broker's stream table and dispatch every **peer-initiated**\n/// stream to a family worker, until `stop` is set. Blocks the calling thread\n/// (the daemon spawns it on its own); workers detach (each ends with its\n/// stream).\npub fn run_dispatch_loop(\n    broker_name: &str,\n    paths: &DispatchPaths,\n    registry: Arc<RegistryHost>,\n    stop: &AtomicBool,\n    poll: Duration,\n) -> io::Result<()> {\n    let mut brain = connect(broker_name)?;\n    let mut claimed: HashSet<u64> = HashSet::new();\n\n    while !stop.load(Ordering::Relaxed) {\n        let reply = brain.net_streams()?;\n        for s in reply.streams {\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\n            // invisible until its first write anyway — gotcha #6 — but the\n            // next_seq guard also skips a row whose pump hasn't appended yet).\n            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {\n                continue;\n            }\n            let name = broker_name.to_string();\n            let origin = s.remote_id_hex.clone();\n            let stream_id = s.stream_id;\n            let p = paths.clone();\n            let reg = Arc::clone(&registry);\n            thread::spawn(move || {\n                let outcome = worker(&name, stream_id, &origin, &p, &reg);\n                match outcome {\n                    DispatchOutcome::Served(_) => {}\n                    other => eprintln!(\"DISPATCH:{stream_id}:{other:?}\"),\n                }\n            });\n        }\n        thread::sleep(poll);\n    }\n    Ok(())\n}\n\n/// Connect a fresh brain IPC client (retrying through broker startup).\n…\n/// Peek the stream's first NDJSON line on a throwaway brain connection.\nfn peek_first_line(broker_name: &str, stream_id: u64) -> io::Result<Option<serde_json::Value>> {\n    let mut brain = connect(broker_name)?;\n    brain.net_stream_subscribe(stream_id, 0)?;\n    let mut buf: Vec<u8> = Vec::new();\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                buf.extend_from_slice(&bytes);\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                // Finished without a complete line: classify what we have\n                // (a line without a trailing newline before EOF still counts).\n                return Ok(serde_json::from_slice(&buf).ok());\n            }\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n            _ => {}\n        }\n    }\n}\n\n/// One claimed stream's worker: peek → classify → run the family's serve\n/// loop on a fresh brain connection.\nfn worker(\n    broker_name: &str,\n    stream_id: u64,\n    origin: &str,\n    paths: &DispatchPaths,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    let line = match peek_first_line(broker_name, stream_id) {\n        Ok(Some(v)) => v,\n        Ok(None) => return DispatchOutcome::Unknown,\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    };\n    let family = classify_first_line(&line);\n\n    let mut brain = match connect(broker_name) {\n        Ok(b) => b,\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    };\n\n    match family {\n        StreamFamily::Sync => {\n            let cs = match ContextStore::open_or_init() {\n                Ok(cs) => cs,\n                Err(e) => return DispatchOutcome::Failed(e.to_string()),\n            };\n            let policy = SyncPolicy::load();\n            match serve_sync(\n                &mut brain,\n                stream_id,\n                origin,\n                0,\n                &cs,\n                &policy,\n                &paths.scratch,\n            ) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Update => {\n            let cache = ReleaseCache::open(&paths.releases);\n            let roster = RosterStore::load();\n            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n        StreamFamily::Attach => {\n            // The gate subject is the session's OWNING endpoint, resolved\n            // from the broker's table (the spawn's label) — never from wire\n            // bytes. An unlabeled or unknown session refuses, fail-closed.\n            //\n            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n            // cannot know our local session id, so it names the ENDPOINT. When the\n            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n            // we actually host a live session for it (D5 session-presence gate;\n            // D6: a stale registry row that named a gone endpoint resolves to no\n            // session → refuse cleanly, never a false attach). The endpoint id is\n            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\n            // `endpoint`); the origin subject stays the handshake identity. The\n            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\n            // is unchanged.\n            // `endpoint` is the gate subject; `remote_session` carries the\n            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\n            // before serve_attach subscribes — never mid-serve, which would swallow\n            // interleaved attach records) and `None` on the local path.\n            let (endpoint, remote_session) = match line\n                .get(\"endpoint_id\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n            {\n                Some(ep) => match crate::attach::resolve_local_session(&mut brain, ep) {\n                    Some(sid) => (Some(ep.to_string()), Some(sid)),\n                    None => (None, None),\n                },\n                None => {\n                    let session_id = line.get(\"session_id\").and_then(|v| v.as_u64());\n                    let endpoint = session_id.and_then(|sid| {\n                        brain.sessions().ok().and_then(|reply| {\n                            reply\n                                .sessions\n                                .iter()\n                                .find(|s| s.session_id == sid && !s.endpoint.is_empty())\n                                .map(|s| s.endpoint.clone())\n                        })\n                    });\n                    (endpoint, None)\n                }\n            };\n            match endpoint {\n                Some(endpoint) => match serve_attach(\n                    &mut brain,\n                    broker_name,\n                    stream_id,\n                    &endpoint,\n                    origin,\n                    0,\n                    remote_session,\n                ) {\n                    Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                    Err(e) => DispatchOutcome::Failed(e.to_string()),\n                },\n                None => {\n                    // Refuse politely: finish our send side, claim stays.\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n                }\n            }\n        }\n        StreamFamily::Xfer => {\n            // The wire names whose whitelist gates (the rule set); the origin\n            // it is checked against stays handshake-proven. Blank refuses.\n            let endpoint = line\n                .get(\"endpoint\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n                .map(str::to_string);\n            match endpoint {\n…\n            }\n        }\n…\n    }\n}"}],"details":{"displayContent":{"text":"//! Inbound net-stream dispatch (D9-1) — the daemon-side acceptor that turns\n//! the hand-wired serve loops of D5–D8 into production behavior.\n//!\n//! Every E2E test so far drove the serve half manually (a probe brain calling\n//! `serve_sync` / `serve_update` / `apply_notif_feed` / `receive_wan` against\n//! a known stream). In production nothing did — a peer could dial and write\n//! and the bytes sat in the broker's stream log unread. This module is the\n//! missing consumer: a daemon-hosted loop that watches the broker's stream\n//! table for **peer-initiated** streams, classifies each by its first NDJSON\n//! record, and hands it to the right serve loop on its own worker thread.\n//!\n//! ## Direction (the gotcha-#9 fact, now first-class)\n//!\n//! The stream table holds locally-opened outbound streams too. The dispatcher\n//! claims only rows with `initiated_locally == false` — serving our own\n//! requester stream would deadlock the requester against ourselves. The flag\n//! is broker-recorded at registration (`open_stream` vs the accept loop),\n//! never inferred.\n//!\n//! ## Classification (first-record demux)\n//!\n//! Streams are single-purpose; the opener's first record names the protocol.\n//! The kind-tagged families collide on variant names (`request` is both\n//! sync's and attach's opener; `chunk`/`done`/`err` recur everywhere), so the\n//! demux keys on the first line's **shape**, not its kind alone:\n//!\n//! | first line | family |\n//! |---|---|\n//! | `kind=request` + `sync_id` | sync pull ([`crate::sync::serve_sync`]) |\n//! | `kind=request` + `session_id` | remote-drive attach (D9-1-3) |\n//! | `kind=request` + `endpoint`+`event` | remote rest op ([`crate::resthost::serve_rest`]) |\n//! | `kind=request` + `owner`+`shell_ref` | cross-node shell link ([`crate::linkhost::serve_shell_link`]) |\n//! | `kind=query` | update pull ([`crate::propagate::serve_update`]) |\n//! | `kind=row` | notif feed ([`crate::notifsync::apply_notif_feed`]) |\n//! | `kind=fetch` / `kind=push` | file transfer (D9-1-3) |\n//! | untagged + `target`+`op_id` | WAN message feed ([`crate::wan::receive_wan`]) |\n//! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |\n//! | anything else | unknown — dropped, loud |\n//!\n//! A family whose production wiring lands in a later slice is still\n//! classified here (the table is complete) but routed to a typed\n//! [`Unwired`](DispatchOutcome::Unwired) drop — loud, never silent.\n//!\n//! ## Worker model\n//!\n//! Each claimed stream gets its own worker thread with its **own** brain IPC\n//! connection (the broker is multi-client by design; the probe-brain pattern\n//! every E2E test uses). The peek and the serve use **separate** connections:\n//! the peek brain subscribes from 0 to read the first line and is dropped;\n//! the serve loop then subscribes from 0 on a fresh connection so it owns its\n//! stream's event feed alone (no double-subscription interleave). Origin is\n//! the table's `remote_id_hex` — handshake-proven, never payload\n//! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.\n// [impl->REQ-NET-1]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n\nuse std::collections::HashSet;\nuse std::io;\nuse std::path::PathBuf;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::Duration;\n\nuse spt_net::net::notif::NotifDecoder;\nuse spt_net::net::replicate::{\n    FeedDecoder as RegistryDecoder, NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate,\n};\nuse spt_net::net::wanmsg::{WanDecoder, WanReply};\nuse spt_store::contextstore::ContextStore;\n…\nimpl DispatchPaths {\n…\n    pub fn production() -> DispatchPaths {\n…\n        DispatchPaths {\n…\n            releases: home.join(\"releases\"),\n            scratch: home.join(\"scratch\").join(\"net\"),\n        }\n    }\n}\n\n/// Watch the broker's stream table and dispatch every **peer-initiated**\n/// stream to a family worker, until `stop` is set. Blocks the calling thread\n/// (the daemon spawns it on its own); workers detach (each ends with its\n/// stream).\npub fn run_dispatch_loop(\n    broker_name: &str,\n    paths: &DispatchPaths,\n    registry: Arc<RegistryHost>,\n    stop: &AtomicBool,\n    poll: Duration,\n) -> io::Result<()> {\n    let mut brain = connect(broker_name)?;\n    let mut claimed: HashSet<u64> = HashSet::new();\n\n    while !stop.load(Ordering::Relaxed) {\n        let reply = brain.net_streams()?;\n        for s in reply.streams {\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\n            // invisible until its first write anyway — gotcha #6 — but the\n            // next_seq guard also skips a row whose pump hasn't appended yet).\n            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {\n                continue;\n            }\n            let name = broker_name.to_string();\n            let origin = s.remote_id_hex.clone();\n            let stream_id = s.stream_id;\n            let p = paths.clone();\n            let reg = Arc::clone(&registry);\n            thread::spawn(move || {\n                let outcome = worker(&name, stream_id, &origin, &p, &reg);\n                match outcome {\n                    DispatchOutcome::Served(_) => {}\n                    other => eprintln!(\"DISPATCH:{stream_id}:{other:?}\"),\n                }\n            });\n        }\n        thread::sleep(poll);\n    }\n    Ok(())\n}\n\n/// Connect a fresh brain IPC client (retrying through broker startup).\n…\n/// Peek the stream's first NDJSON line on a throwaway brain connection.\nfn peek_first_line(broker_name: &str, stream_id: u64) -> io::Result<Option<serde_json::Value>> {\n    let mut brain = connect(broker_name)?;\n    brain.net_stream_subscribe(stream_id, 0)?;\n    let mut buf: Vec<u8> = Vec::new();\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                buf.extend_from_slice(&bytes);\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                // Finished without a complete line: classify what we have\n                // (a line without a trailing newline before EOF still counts).\n                return Ok(serde_json::from_slice(&buf).ok());\n            }\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n            _ => {}\n        }\n    }\n}\n\n/// One claimed stream's worker: peek → classify → run the family's serve\n/// loop on a fresh brain connection.\nfn worker(\n    broker_name: &str,\n    stream_id: u64,\n    origin: &str,\n    paths: &DispatchPaths,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    let line = match peek_first_line(broker_name, stream_id) {\n        Ok(Some(v)) => v,\n        Ok(None) => return DispatchOutcome::Unknown,\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    };\n    let family = classify_first_line(&line);\n\n    let mut brain = match connect(broker_name) {\n        Ok(b) => b,\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    };\n\n    match family {\n        StreamFamily::Sync => {\n            let cs = match ContextStore::open_or_init() {\n                Ok(cs) => cs,\n                Err(e) => return DispatchOutcome::Failed(e.to_string()),\n            };\n            let policy = SyncPolicy::load();\n            match serve_sync(\n                &mut brain,\n                stream_id,\n                origin,\n                0,\n                &cs,\n                &policy,\n                &paths.scratch,\n            ) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Update => {\n            let cache = ReleaseCache::open(&paths.releases);\n            let roster = RosterStore::load();\n            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n        StreamFamily::Attach => {\n            // The gate subject is the session's OWNING endpoint, resolved\n            // from the broker's table (the spawn's label) — never from wire\n            // bytes. An unlabeled or unknown session refuses, fail-closed.\n            //\n            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n            // cannot know our local session id, so it names the ENDPOINT. When the\n            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n            // we actually host a live session for it (D5 session-presence gate;\n            // D6: a stale registry row that named a gone endpoint resolves to no\n            // session → refuse cleanly, never a false attach). The endpoint id is\n            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\n            // `endpoint`); the origin subject stays the handshake identity. The\n            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\n            // is unchanged.\n            // `endpoint` is the gate subject; `remote_session` carries the\n            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\n            // before serve_attach subscribes — never mid-serve, which would swallow\n            // interleaved attach records) and `None` on the local path.\n            let (endpoint, remote_session) = match line\n                .get(\"endpoint_id\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n            {\n                Some(ep) => match crate::attach::resolve_local_session(&mut brain, ep) {\n                    Some(sid) => (Some(ep.to_string()), Some(sid)),\n                    None => (None, None),\n                },\n                None => {\n                    let session_id = line.get(\"session_id\").and_then(|v| v.as_u64());\n                    let endpoint = session_id.and_then(|sid| {\n                        brain.sessions().ok().and_then(|reply| {\n                            reply\n                                .sessions\n                                .iter()\n                                .find(|s| s.session_id == sid && !s.endpoint.is_empty())\n                                .map(|s| s.endpoint.clone())\n                        })\n                    });\n                    (endpoint, None)\n                }\n            };\n            match endpoint {\n                Some(endpoint) => match serve_attach(\n                    &mut brain,\n                    broker_name,\n                    stream_id,\n                    &endpoint,\n                    origin,\n                    0,\n                    remote_session,\n                ) {\n                    Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                    Err(e) => DispatchOutcome::Failed(e.to_string()),\n                },\n                None => {\n                    // Refuse politely: finish our send side, claim stays.\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n                }\n            }\n        }\n        StreamFamily::Xfer => {\n            // The wire names whose whitelist gates (the rule set); the origin\n            // it is checked against stays handshake-proven. Blank refuses.\n            let endpoint = line\n                .get(\"endpoint\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n                .map(str::to_string);\n            match endpoint {\n…\n            }\n        }\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,null,182,null,184,null,186,null,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,null,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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,null,420,421,null,456,457]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"}}},"isError":false,"timestamp":1784195886084}}
{"type":"message","id":"cec88258","parentId":"ab360cba","timestamp":"2026-07-16T09:58:06.101Z","message":{"role":"toolResult","toolCallId":"call_sGzDMzGFX2YKjvFGGQivgIPh|fc_047bb49263dfa303016a58ab2d7c0c8198ad531e6d67aa5c79","toolName":"read","content":[{"type":"text","text":"/// discipline applied to the network (D4b): the stream's single read pump is\n/// the only producer; a brain connection attaches as the subscriber; every\n/// subscriber send happens under the enclosing `Mutex`, so live chunks and a\n/// replay can never interleave out of `seq` order. Chunks produced while no\n/// brain is attached sit in the ring and replay on the next subscribe — a\n/// brain restart loses nothing (Spike #3 invariant B/D).\n// [impl->REQ-DAEMON-2]\nstruct StreamLog {\n    stream_id: u64,\n    /// TRANSPORT read counter: bumped once per [`append`](Self::append) (one\n    /// read-pump `read_chunk`), INDEPENDENT of byte size. NOT a logical-chunk\n    /// count — QUIC may coalesce several sender writes into one receiver\n    /// `read_chunk` (or split one), so `next_seq` can be < the number of logical\n    /// chunks the sender issued for the SAME bytes. Correctness (byte-exact +\n    /// exactly-once) holds via these transport seqs + the recv cursor regardless;\n    /// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows\n    /// coalescing flake) and must gate on BYTES instead.\n    next_seq: u64,\n    ring: VecDeque<(u64, Vec<u8>)>,\n    cap_chunks: usize,\n    subscriber: Option<SharedSend>,\n    /// Read side finished cleanly (peer called finish / conn ended).\n    finished: bool,\n    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n    /// congestion surfaces as lag, never loss.\" So a retentive log never cap-evicts\n    /// on append (no loss), and the read pump REFUSES to pull the next chunk once\n    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the\n    /// transport's native flow-control then lags the *sender*. A chunk leaves the\n    /// ring ONLY when [`drain`](Self::drain) (the recv cursor) pops it; the pump is\n    /// woken to resume via the entry's `room` notifier. Ordinary streams keep\n    /// `retentive_cap = 0` (a slow/absent brain must not OOM the broker via an\n    /// unbounded ring) and are drained by subscribe, never the sync cursor.\n    retentive_cap: usize,\n}\n\nimpl StreamLog {\n    fn new(stream_id: u64, cap_chunks: usize) -> Self {\n        StreamLog {\n            stream_id,\n            next_seq: 0,\n            ring: VecDeque::new(),\n            cap_chunks,\n            subscriber: None,\n            finished: false,\n            retentive_cap: 0,\n        }\n    }\n\n    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\n    fn set_retentive(&mut self, cap: usize) {\n        self.retentive_cap = cap;\n    }\n\n    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\n    /// live-send to the current subscriber. Read-pump only. A non-retentive log\n    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\n    /// never evicts — bytes leave only via [`drain`](Self::drain).\n    fn append(&mut self, bytes: &[u8]) {\n        let seq = self.next_seq;\n        self.next_seq += 1;\n        self.ring.push_back((seq, bytes.to_vec()));\n        if self.retentive_cap == 0 {\n            while self.ring.len() > self.cap_chunks {\n                self.ring.pop_front();\n            }\n        }\n        if let Some(sub) = &self.subscriber {\n            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Drain every undrained chunk in order (the shell-tunnel recv cursor, W3).\n    /// Pops the whole ring front-to-back (FIFO by `seq`) and concatenates the\n    /// bytes — chunk boundaries are arbitrary read-pump splits, irrelevant to an\n    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,\n    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so\n    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing\n    /// pending yet.\n    fn drain(&mut self) -> (Vec<u8>, bool) {\n        let mut out = Vec::new();\n        while let Some((_, chunk)) = self.ring.pop_front() {\n            out.extend_from_slice(&chunk);\n        }\n        (out, self.finished)\n    }\n\n    /// Mark the read side finished and tell the subscriber (with the final\n    /// cursor, so it can verify completeness).\n    fn finish(&mut self) {\n        self.finished = true;\n        if let Some(sub) = &self.subscriber {\n            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Attach `sub` as the subscriber, replaying every retained chunk with\n    /// `seq >= from_seq` (then eof, if already finished) before live streaming.\n    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\n        // record — this is one of the two RCA-pinned shared-carrier classes.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        sub.describe(&format!(\"stream-subscriber stream={}\", self.stream_id));\n        sub.lifecycle_event(\n            \"stream-sub-attach\",\n            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\n        );\n        self.subscriber = Some(Arc::clone(&sub));\n        for (seq, bytes) in self.ring.iter().filter(|(s, _)| *s >= from_seq) {\n            let frame = net_stream_data_envelope(self.stream_id, *seq, bytes);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n        if self.finished {\n            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n    /// not unsubscribe a newer brain).\n    fn detach_if(&mut self, sub: &SharedSend) {\n        if let Some(cur) = &self.subscriber {\n            if Arc::ptr_eq(cur, sub) {\n                self.subscriber = None;\n            }\n        }\n    }\n}\n\n/// A bounded, sequenced log of connection liveness events — the broker-owned\n…\n/// QUIC reads via quinn's chunk API; loopback via the read end of the duplex.\n/// Both append to the same [`StreamLog`], so the subscriber (and everything\n/// above it) cannot tell the transports apart.\nenum RecvHalf {\n    Quic(RecvStream),\n    Loopback(ReadHalf<DuplexStream>),\n}\n\n/// One live stream the broker holds (QUIC or loopback): the send half (brain\n/// writes ride it over IPC) + the sequenced read log.\nstruct StreamEntry {\n    conn_id: u64,\n    remote_id_hex: String,\n    /// Whether THIS node opened the stream (`open_stream`) vs the peer\n    /// (accept loop). The inbound dispatcher (D9-1) claims only\n    /// peer-initiated streams; without this fact the table is ambiguous —\n    /// it holds locally-opened outbound streams too (gotcha: a dispatcher\n    /// that served its own requester stream would deadlock on itself).\n    initiated_locally: bool,\n    /// The send half, serialized behind an async mutex (writes happen on the\n    /// host's runtime via `block_on`).\n    send: tokio::sync::Mutex<SendHalf>,\n    log: Arc<Mutex<StreamLog>>,\n    /// Wakes the read pump when a retentive (tunnel) stream's ring drains below\n    /// its backpressure cap (M11-W3, REQ-SHELL-4) — see [`StreamLog::retentive_cap`].\n    /// Unused by ordinary (non-retentive) streams.\n    room: Arc<tokio::sync::Notify>,\n}\n\n/// Tables shared between the host's sync surface and its runtime tasks\n/// (accept loop, per-conn stream acceptors, per-stream read pumps).\nstruct NetShared {\n    next_conn_id: AtomicU64,\n    conns: Mutex<HashMap<u64, ConnEntry>>,\n    next_stream_id: AtomicU64,\n    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,\n    ring_cap: usize,\n    /// The conn-liveness event log (D4c) — one per host, all conns interleaved.\n    presence: Mutex<PresenceLog>,\n}\n\nimpl NetShared {\n    fn new() -> Self {\n        NetShared {\n            next_conn_id: AtomicU64::new(1),\n            conns: Mutex::new(HashMap::new()),\n            next_stream_id: AtomicU64::new(1),\n            streams: Mutex::new(HashMap::new()),\n            ring_cap: DEFAULT_STREAM_RING_CHUNKS,\n            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),\n        }\n    }\n}\n\n/// Register a live connection: mint its broker id and spawn its inbound-stream\n/// acceptor. Must run inside the host's runtime context (the accept loop, or a\n/// `block_on` future) — it spawns tasks.\n/// The connection's OBSERVED remote address as opaque seed JSON (REQ-CONV-1):\n/// the path address string, or `null` when id-only / path-less (nothing worth\n/// seeding over discovery). Shared by [`register_conn`]'s presence write-back\n/// and [`NetHost::conn_remote_addr_json`] so both seed identically.\n// [impl->REQ-CONV-1]\nfn observed_addr_json(conn: &Connection) -> serde_json::Value {\n    let addr = spt_net::net::endpoint::conn_remote_addr(conn);\n    if addr.is_empty() {\n        return serde_json::Value::Null; // id-only adds nothing over discovery\n    }\n    serde_json::to_value(addr).unwrap_or(serde_json::Value::Null)\n}\n\n/// A `Send + 'static` snapshot of one dial's inputs — the SHARED connect path\n/// for both the blocking [`NetHost::dial`] and the spawned\n/// [`NetHost::submit_dial`], so W1's per-dial bound and the Mesh-D2 seed-proof\n/// stay single-sourced (never a second, drifting dial impl).\nstruct DialPlan {\n    endpoint: Arc<NetEndpoint>,\n    shared: Arc<NetShared>,\n    local_id: PublicKey,\n    membership: Option<MembershipSource>,\n    roster_rx: Option<RosterExchange>,\n    self_addr: serde_json::Value,\n}\n\nimpl DialPlan {\n    /// Connect, prove membership in-band (Mesh-D2 — a failed/empty proof drops\n    /// the dial before it registers; Mesh-D4 roster leg rides the same stream on\n    /// success), then register the conn (which emits `PRESENCE_CONNECTED` with\n    /// the observed addr). The caller bounds this await (the blocking dial via\n    /// `bounded_block_on`; the submit via its own `timeout`).\n    async fn connect(self, addr: EndpointAddr) -> io::Result<(u64, String)> {\n        let conn = self\n            .endpoint\n            .connect(addr)\n            .await\n            .map_err(|e| io::Error::other(e.to_string()))?;\n        let proven = match self.membership {\n            Some(src) => {\n                let creds = src();\n                prove_membership(\n                    &conn,\n                    self.local_id,\n                    ProofRole::Dialer,\n                    &creds,\n                    self.roster_rx.as_ref(),\n                    &self.self_addr,\n                )\n                .await\n                .ok_or_else(|| {\n                    io::Error::other(\"seed-proof failed: peer is not a subnet member\")\n                })?\n            }\n            None => HashSet::new(),\n        };\n        Ok(register_conn(&self.shared, conn, proven))\n    }\n}\n\nfn register_conn(\n    shared: &Arc<NetShared>,\n    conn: Connection,\n    proven_subnets: HashSet<String>,\n) -> (u64, String) {\n    let remote_id_hex = PublicKey::from_bytes(conn.remote_id().as_bytes())\n        .expect(\"an iroh EndpointId is always a valid ed25519 public key\")\n        .to_hex();\n    let id = shared.next_conn_id.fetch_add(1, Ordering::Relaxed);\n    shared.conns.lock().unwrap().insert(\n        id,\n        ConnEntry {\n            kind: ConnKind::Quic(conn.clone()),\n            remote_id_hex: remote_id_hex.clone(),\n            proven_subnets,\n        },\n    );\n\n    // Presence: announce the row BEFORE spawning the closed-watcher, so the\n    // `connected` event can never race behind its own `disconnected` (D4c). The\n    // event carries the OBSERVED remote addr (REQ-CONV-1 seed write-back) so a\n    // non-blocking submit-dial seeds `peer-addrs.json` like the blocking reply\n    // did (id-only / path-less → `null`, nothing worth seeding).\n    let observed = observed_addr_json(&conn);\n    shared\n        .presence\n        .lock()\n        .unwrap()\n        .append_connected(id, &remote_id_hex, observed);\n\n    // Closed-watcher: the row's exit path. Awaits the connection's end (clean\n    // close, conn loss, or our own endpoint close), removes the row — conns\n    // finally LEAVE the table — then appends the `disconnected` event (row\n    // gone before the event is observable).\n    // [impl->REQ-EP-4]\n    let sh_close = Arc::clone(shared);\n    let conn_close = conn.clone();\n    let hex_close = remote_id_hex.clone();\n    tokio::spawn(async move {\n        conn_close.closed().await;\n        sh_close.conns.lock().unwrap().remove(&id);\n        sh_close\n            .presence\n            .lock()\n            .unwrap()\n            .append(PRESENCE_DISCONNECTED, id, &hex_close);\n    });\n\n    // Inbound-stream acceptor: every peer-opened bidi stream joins the table\n    // and gets a read pump. (QUIC streams are lazy — the peer's open becomes\n    // visible here on its first write.) Ends when the connection does.\n    let sh = Arc::clone(shared);\n    let hex = remote_id_hex.clone();\n    tokio::spawn(async move {\n        while let Ok((send, recv)) = conn.accept_bi().await {\n            register_stream(\n                &sh,\n                id,\n                &hex,\n                SendHalf::Quic(send),\n                RecvHalf::Quic(recv),\n                false,\n            );\n        }\n    });\n\n    (id, remote_id_hex)\n}\n\n/// Register a live stream (opened or accepted): mint its broker id and spawn\n/// its read pump. Must run inside the host's runtime context.\n/// `initiated_locally` records the direction fact for the table (true only\n/// from [`NetHost::open_stream`]).\nfn register_stream(\n    shared: &Arc<NetShared>,\n    conn_id: u64,\n    remote_id_hex: &str,\n    send: SendHalf,\n    recv: RecvHalf,\n    initiated_locally: bool,\n) -> u64 {\n    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);\n    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));\n    let room = Arc::new(tokio::sync::Notify::new());\n    shared.streams.lock().unwrap().insert(\n        id,\n        Arc::new(StreamEntry {\n            conn_id,\n            remote_id_hex: remote_id_hex.to_string(),\n            initiated_locally,\n            send: tokio::sync::Mutex::new(send),\n            log: Arc::clone(&log),\n            room: Arc::clone(&room),\n        }),\n    );\n\n    // The read pump — the stream's single producer. A clean end (peer finish)\n    // and a torn end (conn lost) both finish the log; the buffered chunks stay\n    // replayable either way. The transport differs only at this read leaf\n    // (QUIC chunk API vs the loopback duplex's `AsyncRead`); both append to the\n    // same `StreamLog`, so nothing above the pump can tell them apart.\n    //\n    // Backpressure (M11-W3, REQ-SHELL-4): a retentive (tunnel) log never evicts,\n    // so before pulling the next chunk the pump WAITS while the undrained ring is\n    // at its `retentive_cap`. The unread bytes stay in the transport's buffer, so\n    // QUIC stream flow-control (or the loopback duplex's bounded buffer) lags the\n    // SENDER — \"lag, never loss\" (CONTEXT:262). A `drain` wakes `room`. Ordinary\n    // streams have `retentive_cap == 0`, so this gate is a no-op for them.\n    tokio::spawn(async move {\n        match recv {\n            RecvHalf::Quic(mut recv) => loop {\n                await_ring_room(&log, &room).await;\n                match recv.read_chunk(STREAM_READ_CHUNK).await {\n                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),\n                    Ok(None) | Err(_) => {\n                        log.lock().unwrap().finish();\n                        break;\n                    }\n                }\n            },\n            RecvHalf::Loopback(mut recv) => {\n                let mut buf = vec![0u8; STREAM_READ_CHUNK];\n                loop {\n                    await_ring_room(&log, &room).await;\n                    match recv.read(&mut buf).await {\n                        Ok(0) | Err(_) => {\n                            log.lock().unwrap().finish();\n                            break;\n                        }\n                        Ok(n) => log.lock().unwrap().append(&buf[..n]),\n                    }\n                }\n            }\n        }\n    });\n\n    id\n}\n\n/// Backpressure gate for a retentive (tunnel) read pump: park until the undrained\n/// ring has room below `retentive_cap`, woken by a `drain` on `room`. A no-op for\n…\n    // [impl->REQ-DAEMON-2]\n    pub fn send_stream(&self, stream_id: u64, bytes: &[u8], finish: bool) -> io::Result<()> {\n        let entry = {\n            let streams = self.shared.streams.lock().unwrap();\n            Arc::clone(streams.get(&stream_id).ok_or_else(|| {\n                io::Error::new(\n                    io::ErrorKind::NotFound,\n                    format!(\"no such stream {stream_id}\"),\n                )\n            })?)\n        };\n        let bytes = bytes.to_vec();\n        // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): `write_all` / `finish` on a\n        // black-holed QUIC stream blocks on flow-control the dead peer never grants.\n        // Loopback (in-process) never hangs, so the bound is a no-op for it.\n        self.bounded_block_on(async move {\n            let mut send = entry.send.lock().await;\n            if !bytes.is_empty() {\n                send.write_all(&bytes).await?;\n            }\n            if finish {\n                send.finish().await?;\n            }\n            Ok(())\n        })\n    }\n\n    /// Every held stream's info — the query a re-attaching brain uses to find\n    /// streams (including peer-initiated ones that arrived while it was down).\n    pub fn stream_infos(&self) -> Vec<NetStreamInfo> {\n        let streams = self.shared.streams.lock().unwrap();\n        streams\n            .iter()\n            .map(|(id, e)| {\n                let log = e.log.lock().unwrap();\n                NetStreamInfo {\n                    stream_id: *id,\n                    conn_id: e.conn_id,\n                    remote_id_hex: e.remote_id_hex.clone(),\n                    next_seq: log.next_seq,\n                    finished: log.finished,\n                    initiated_locally: e.initiated_locally,\n                }\n            })\n            .collect()\n    }\n\n    /// Mark a held stream **retentive** with backpressure `cap` chunks (M11-W3,\n    /// REQ-SHELL-4): its read log stops evicting (no byte loss) and its read pump\n    /// lags the sender once the undrained ring reaches `cap` (no unbounded growth).\n    /// Used for shell-tunnel streams; ordinary streams stay bounded-overwrite.\n    /// No-op if the stream is gone.\n    // [impl->REQ-SHELL-4]\n    pub fn mark_stream_retentive(&self, stream_id: u64, cap: usize) {\n        if let Some(e) = self.shared.streams.lock().unwrap().get(&stream_id) {\n            e.log.lock().unwrap().set_retentive(cap);\n        }\n    }\n\n    /// Drain a retentive stream's undrained bytes (the shell-tunnel recv cursor):\n    /// pops the whole ring in order, concatenated, and wakes the read pump (which\n    /// may have been lagging the sender at the backpressure cap). Returns\n    /// `(bytes, finished)`; `finished` ⇒ the read side ended (the poller closes).\n    /// `None` ⇒ no such stream. Empty bytes + `finished == false` ⇒ nothing pending.\n    // [impl->REQ-SHELL-4]\n    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {\n        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n        let drained = entry.log.lock().unwrap().drain();\n        // The drain freed ring room — wake a pump that was backpressure-parked.\n        entry.room.notify_one();\n        Some(drained)\n    }\n\n    /// A held stream's current undrained ring depth in chunks (test introspection\n    /// for the retentive backpressure bound). `None` ⇒ no such stream.\n    #[cfg(test)]\n    fn ring_len(&self, stream_id: u64) -> Option<usize> {\n        self.shared\n            .streams\n            .lock()\n            .unwrap()\n            .get(&stream_id)\n            .map(|e| e.log.lock().unwrap().ring.len())\n    }\n\n    /// Inbound (peer-initiated) stream rows currently held on `conn_id` — the\n    /// candidate set [`Self::open_loopback_pair`] diffs to find the peer it just\n    /// minted.\n    fn inbound_rows_on(&self, conn_id: u64) -> HashSet<u64> {\n        self.shared\n            .streams\n            .lock()\n            .unwrap()\n            .iter()\n            .filter(|(_, e)| e.conn_id == conn_id && !e.initiated_locally)\n            .map(|(id, _)| *id)\n            .collect()\n    }\n\n    /// Open a same-node loopback tunnel stream PAIR (M11-W3, REQ-SHELL-4) and\n    /// return `(owner_stream, shell_stream)` — the operator row the owner side\n    /// drives and the cross-wired peer row the shell side drives. The loopback\n    /// `open_stream` mints both rows in-process (one `duplex` wires them, so a\n    /// write to either send appears on the other's read log). Serialized so the\n    /// before/after diff sees exactly the one peer row this open minted.\n    // [impl->REQ-SHELL-4]\n    pub fn open_loopback_pair(&self) -> io::Result<(u64, u64)> {\n        let _guard = self.loopback_pair_lock.lock().unwrap();\n        let (conn_id, _) = self.dial_loopback();\n        let before = self.inbound_rows_on(conn_id);\n        let owner_stream = self.open_stream(conn_id)?;\n        let shell_stream = self\n            .inbound_rows_on(conn_id)\n            .difference(&before)\n            .copied()\n            .next()\n            .ok_or_else(|| io::Error::other(\"loopback peer row not registered\"))?;\n        Ok((owner_stream, shell_stream))\n    }\n\n    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`\n    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).\n    // [impl->REQ-DAEMON-2]\n    pub(crate) fn subscribe_stream(\n        &self,\n        stream_id: u64,\n        sub: SharedSend,\n        from_seq: u64,\n    ) -> io::Result<()> {\n        let log = {\n            let streams = self.shared.streams.lock().unwrap();\n            let e = streams.get(&stream_id).ok_or_else(|| {\n                io::Error::new(\n                    io::ErrorKind::NotFound,\n                    format!(\"no such stream {stream_id}\"),\n                )\n            })?;\n            Arc::clone(&e.log)\n        };\n        log.lock().unwrap().attach(sub, from_seq);\n        Ok(())\n    }\n\n    /// Detach `sub` from a stream iff it is the current subscriber (brain\n    /// disconnect cleanup; the stream + its ring persist).\n    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {\n        let log = {\n            let streams = self.shared.streams.lock().unwrap();\n            streams.get(&stream_id).map(|e| Arc::clone(&e.log))\n        };\n        if let Some(log) = log {\n            log.lock().unwrap().detach_if(sub);\n        }\n    }"}],"details":{"displayContent":{"text":"/// discipline applied to the network (D4b): the stream's single read pump is\n/// the only producer; a brain connection attaches as the subscriber; every\n/// subscriber send happens under the enclosing `Mutex`, so live chunks and a\n/// replay can never interleave out of `seq` order. Chunks produced while no\n/// brain is attached sit in the ring and replay on the next subscribe — a\n/// brain restart loses nothing (Spike #3 invariant B/D).\n// [impl->REQ-DAEMON-2]\nstruct StreamLog {\n    stream_id: u64,\n    /// TRANSPORT read counter: bumped once per [`append`](Self::append) (one\n    /// read-pump `read_chunk`), INDEPENDENT of byte size. NOT a logical-chunk\n    /// count — QUIC may coalesce several sender writes into one receiver\n    /// `read_chunk` (or split one), so `next_seq` can be < the number of logical\n    /// chunks the sender issued for the SAME bytes. Correctness (byte-exact +\n    /// exactly-once) holds via these transport seqs + the recv cursor regardless;\n    /// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows\n    /// coalescing flake) and must gate on BYTES instead.\n    next_seq: u64,\n    ring: VecDeque<(u64, Vec<u8>)>,\n    cap_chunks: usize,\n    subscriber: Option<SharedSend>,\n    /// Read side finished cleanly (peer called finish / conn ended).\n    finished: bool,\n    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n    /// congestion surfaces as lag, never loss.\" So a retentive log never cap-evicts\n    /// on append (no loss), and the read pump REFUSES to pull the next chunk once\n    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the\n    /// transport's native flow-control then lags the *sender*. A chunk leaves the\n    /// ring ONLY when [`drain`](Self::drain) (the recv cursor) pops it; the pump is\n    /// woken to resume via the entry's `room` notifier. Ordinary streams keep\n    /// `retentive_cap = 0` (a slow/absent brain must not OOM the broker via an\n    /// unbounded ring) and are drained by subscribe, never the sync cursor.\n    retentive_cap: usize,\n}\n\nimpl StreamLog {\n    fn new(stream_id: u64, cap_chunks: usize) -> Self {\n        StreamLog {\n            stream_id,\n            next_seq: 0,\n            ring: VecDeque::new(),\n            cap_chunks,\n            subscriber: None,\n            finished: false,\n            retentive_cap: 0,\n        }\n    }\n\n    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\n    fn set_retentive(&mut self, cap: usize) {\n        self.retentive_cap = cap;\n    }\n\n    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\n    /// live-send to the current subscriber. Read-pump only. A non-retentive log\n    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\n    /// never evicts — bytes leave only via [`drain`](Self::drain).\n    fn append(&mut self, bytes: &[u8]) {\n        let seq = self.next_seq;\n        self.next_seq += 1;\n        self.ring.push_back((seq, bytes.to_vec()));\n        if self.retentive_cap == 0 {\n            while self.ring.len() > self.cap_chunks {\n                self.ring.pop_front();\n            }\n        }\n        if let Some(sub) = &self.subscriber {\n            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Drain every undrained chunk in order (the shell-tunnel recv cursor, W3).\n    /// Pops the whole ring front-to-back (FIFO by `seq`) and concatenates the\n    /// bytes — chunk boundaries are arbitrary read-pump splits, irrelevant to an\n    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,\n    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so\n    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing\n    /// pending yet.\n    fn drain(&mut self) -> (Vec<u8>, bool) {\n        let mut out = Vec::new();\n        while let Some((_, chunk)) = self.ring.pop_front() {\n            out.extend_from_slice(&chunk);\n        }\n        (out, self.finished)\n    }\n\n    /// Mark the read side finished and tell the subscriber (with the final\n    /// cursor, so it can verify completeness).\n    fn finish(&mut self) {\n        self.finished = true;\n        if let Some(sub) = &self.subscriber {\n            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Attach `sub` as the subscriber, replaying every retained chunk with\n    /// `seq >= from_seq` (then eof, if already finished) before live streaming.\n    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\n        // record — this is one of the two RCA-pinned shared-carrier classes.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        sub.describe(&format!(\"stream-subscriber stream={}\", self.stream_id));\n        sub.lifecycle_event(\n            \"stream-sub-attach\",\n            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\n        );\n        self.subscriber = Some(Arc::clone(&sub));\n        for (seq, bytes) in self.ring.iter().filter(|(s, _)| *s >= from_seq) {\n            let frame = net_stream_data_envelope(self.stream_id, *seq, bytes);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n        if self.finished {\n            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n    /// not unsubscribe a newer brain).\n    fn detach_if(&mut self, sub: &SharedSend) {\n        if let Some(cur) = &self.subscriber {\n            if Arc::ptr_eq(cur, sub) {\n                self.subscriber = None;\n            }\n        }\n    }\n}\n\n/// A bounded, sequenced log of connection liveness events — the broker-owned\n…\n/// QUIC reads via quinn's chunk API; loopback via the read end of the duplex.\n/// Both append to the same [`StreamLog`], so the subscriber (and everything\n/// above it) cannot tell the transports apart.\nenum RecvHalf {\n    Quic(RecvStream),\n    Loopback(ReadHalf<DuplexStream>),\n}\n\n/// One live stream the broker holds (QUIC or loopback): the send half (brain\n/// writes ride it over IPC) + the sequenced read log.\nstruct StreamEntry {\n    conn_id: u64,\n    remote_id_hex: String,\n    /// Whether THIS node opened the stream (`open_stream`) vs the peer\n    /// (accept loop). The inbound dispatcher (D9-1) claims only\n    /// peer-initiated streams; without this fact the table is ambiguous —\n    /// it holds locally-opened outbound streams too (gotcha: a dispatcher\n    /// that served its own requester stream would deadlock on itself).\n    initiated_locally: bool,\n    /// The send half, serialized behind an async mutex (writes happen on the\n    /// host's runtime via `block_on`).\n    send: tokio::sync::Mutex<SendHalf>,\n    log: Arc<Mutex<StreamLog>>,\n    /// Wakes the read pump when a retentive (tunnel) stream's ring drains below\n    /// its backpressure cap (M11-W3, REQ-SHELL-4) — see [`StreamLog::retentive_cap`].\n    /// Unused by ordinary (non-retentive) streams.\n    room: Arc<tokio::sync::Notify>,\n}\n\n/// Tables shared between the host's sync surface and its runtime tasks\n/// (accept loop, per-conn stream acceptors, per-stream read pumps).\nstruct NetShared {\n    next_conn_id: AtomicU64,\n    conns: Mutex<HashMap<u64, ConnEntry>>,\n    next_stream_id: AtomicU64,\n    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,\n    ring_cap: usize,\n    /// The conn-liveness event log (D4c) — one per host, all conns interleaved.\n    presence: Mutex<PresenceLog>,\n}\n\nimpl NetShared {\n    fn new() -> Self {\n        NetShared {\n            next_conn_id: AtomicU64::new(1),\n            conns: Mutex::new(HashMap::new()),\n            next_stream_id: AtomicU64::new(1),\n            streams: Mutex::new(HashMap::new()),\n            ring_cap: DEFAULT_STREAM_RING_CHUNKS,\n            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),\n        }\n    }\n}\n\n/// Register a live connection: mint its broker id and spawn its inbound-stream\n/// acceptor. Must run inside the host's runtime context (the accept loop, or a\n/// `block_on` future) — it spawns tasks.\n/// The connection's OBSERVED remote address as opaque seed JSON (REQ-CONV-1):\n/// the path address string, or `null` when id-only / path-less (nothing worth\n/// seeding over discovery). Shared by [`register_conn`]'s presence write-back\n/// and [`NetHost::conn_remote_addr_json`] so both seed identically.\n// [impl->REQ-CONV-1]\nfn observed_addr_json(conn: &Connection) -> serde_json::Value {\n    let addr = spt_net::net::endpoint::conn_remote_addr(conn);\n    if addr.is_empty() {\n        return serde_json::Value::Null; // id-only adds nothing over discovery\n    }\n    serde_json::to_value(addr).unwrap_or(serde_json::Value::Null)\n}\n\n/// A `Send + 'static` snapshot of one dial's inputs — the SHARED connect path\n/// for both the blocking [`NetHost::dial`] and the spawned\n/// [`NetHost::submit_dial`], so W1's per-dial bound and the Mesh-D2 seed-proof\n/// stay single-sourced (never a second, drifting dial impl).\nstruct DialPlan {\n    endpoint: Arc<NetEndpoint>,\n    shared: Arc<NetShared>,\n    local_id: PublicKey,\n    membership: Option<MembershipSource>,\n    roster_rx: Option<RosterExchange>,\n    self_addr: serde_json::Value,\n}\n\nimpl DialPlan {\n    /// Connect, prove membership in-band (Mesh-D2 — a failed/empty proof drops\n    /// the dial before it registers; Mesh-D4 roster leg rides the same stream on\n    /// success), then register the conn (which emits `PRESENCE_CONNECTED` with\n    /// the observed addr). The caller bounds this await (the blocking dial via\n    /// `bounded_block_on`; the submit via its own `timeout`).\n    async fn connect(self, addr: EndpointAddr) -> io::Result<(u64, String)> {\n        let conn = self\n            .endpoint\n            .connect(addr)\n            .await\n            .map_err(|e| io::Error::other(e.to_string()))?;\n        let proven = match self.membership {\n            Some(src) => {\n                let creds = src();\n                prove_membership(\n                    &conn,\n                    self.local_id,\n                    ProofRole::Dialer,\n                    &creds,\n                    self.roster_rx.as_ref(),\n                    &self.self_addr,\n                )\n                .await\n                .ok_or_else(|| {\n                    io::Error::other(\"seed-proof failed: peer is not a subnet member\")\n                })?\n            }\n            None => HashSet::new(),\n        };\n        Ok(register_conn(&self.shared, conn, proven))\n    }\n}\n\nfn register_conn(\n    shared: &Arc<NetShared>,\n    conn: Connection,\n    proven_subnets: HashSet<String>,\n) -> (u64, String) {\n    let remote_id_hex = PublicKey::from_bytes(conn.remote_id().as_bytes())\n        .expect(\"an iroh EndpointId is always a valid ed25519 public key\")\n        .to_hex();\n    let id = shared.next_conn_id.fetch_add(1, Ordering::Relaxed);\n    shared.conns.lock().unwrap().insert(\n        id,\n        ConnEntry {\n            kind: ConnKind::Quic(conn.clone()),\n            remote_id_hex: remote_id_hex.clone(),\n            proven_subnets,\n        },\n    );\n\n    // Presence: announce the row BEFORE spawning the closed-watcher, so the\n    // `connected` event can never race behind its own `disconnected` (D4c). The\n    // event carries the OBSERVED remote addr (REQ-CONV-1 seed write-back) so a\n    // non-blocking submit-dial seeds `peer-addrs.json` like the blocking reply\n    // did (id-only / path-less → `null`, nothing worth seeding).\n    let observed = observed_addr_json(&conn);\n    shared\n        .presence\n        .lock()\n        .unwrap()\n        .append_connected(id, &remote_id_hex, observed);\n\n    // Closed-watcher: the row's exit path. Awaits the connection's end (clean\n    // close, conn loss, or our own endpoint close), removes the row — conns\n    // finally LEAVE the table — then appends the `disconnected` event (row\n    // gone before the event is observable).\n    // [impl->REQ-EP-4]\n    let sh_close = Arc::clone(shared);\n    let conn_close = conn.clone();\n    let hex_close = remote_id_hex.clone();\n    tokio::spawn(async move {\n        conn_close.closed().await;\n        sh_close.conns.lock().unwrap().remove(&id);\n        sh_close\n            .presence\n            .lock()\n            .unwrap()\n            .append(PRESENCE_DISCONNECTED, id, &hex_close);\n    });\n\n    // Inbound-stream acceptor: every peer-opened bidi stream joins the table\n    // and gets a read pump. (QUIC streams are lazy — the peer's open becomes\n    // visible here on its first write.) Ends when the connection does.\n    let sh = Arc::clone(shared);\n    let hex = remote_id_hex.clone();\n    tokio::spawn(async move {\n        while let Ok((send, recv)) = conn.accept_bi().await {\n            register_stream(\n                &sh,\n                id,\n                &hex,\n                SendHalf::Quic(send),\n                RecvHalf::Quic(recv),\n                false,\n            );\n        }\n    });\n\n    (id, remote_id_hex)\n}\n\n/// Register a live stream (opened or accepted): mint its broker id and spawn\n/// its read pump. Must run inside the host's runtime context.\n/// `initiated_locally` records the direction fact for the table (true only\n/// from [`NetHost::open_stream`]).\nfn register_stream(\n    shared: &Arc<NetShared>,\n    conn_id: u64,\n    remote_id_hex: &str,\n    send: SendHalf,\n    recv: RecvHalf,\n    initiated_locally: bool,\n) -> u64 {\n    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);\n    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));\n    let room = Arc::new(tokio::sync::Notify::new());\n    shared.streams.lock().unwrap().insert(\n        id,\n        Arc::new(StreamEntry {\n            conn_id,\n            remote_id_hex: remote_id_hex.to_string(),\n            initiated_locally,\n            send: tokio::sync::Mutex::new(send),\n            log: Arc::clone(&log),\n            room: Arc::clone(&room),\n        }),\n    );\n\n    // The read pump — the stream's single producer. A clean end (peer finish)\n    // and a torn end (conn lost) both finish the log; the buffered chunks stay\n    // replayable either way. The transport differs only at this read leaf\n    // (QUIC chunk API vs the loopback duplex's `AsyncRead`); both append to the\n    // same `StreamLog`, so nothing above the pump can tell them apart.\n    //\n    // Backpressure (M11-W3, REQ-SHELL-4): a retentive (tunnel) log never evicts,\n    // so before pulling the next chunk the pump WAITS while the undrained ring is\n    // at its `retentive_cap`. The unread bytes stay in the transport's buffer, so\n    // QUIC stream flow-control (or the loopback duplex's bounded buffer) lags the\n    // SENDER — \"lag, never loss\" (CONTEXT:262). A `drain` wakes `room`. Ordinary\n    // streams have `retentive_cap == 0`, so this gate is a no-op for them.\n    tokio::spawn(async move {\n        match recv {\n            RecvHalf::Quic(mut recv) => loop {\n                await_ring_room(&log, &room).await;\n                match recv.read_chunk(STREAM_READ_CHUNK).await {\n                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),\n                    Ok(None) | Err(_) => {\n                        log.lock().unwrap().finish();\n                        break;\n                    }\n                }\n            },\n            RecvHalf::Loopback(mut recv) => {\n                let mut buf = vec![0u8; STREAM_READ_CHUNK];\n                loop {\n                    await_ring_room(&log, &room).await;\n                    match recv.read(&mut buf).await {\n                        Ok(0) | Err(_) => {\n                            log.lock().unwrap().finish();\n                            break;\n                        }\n                        Ok(n) => log.lock().unwrap().append(&buf[..n]),\n                    }\n                }\n            }\n        }\n    });\n\n    id\n}\n\n/// Backpressure gate for a retentive (tunnel) read pump: park until the undrained\n/// ring has room below `retentive_cap`, woken by a `drain` on `room`. A no-op for\n…\n    // [impl->REQ-DAEMON-2]\n    pub fn send_stream(&self, stream_id: u64, bytes: &[u8], finish: bool) -> io::Result<()> {\n        let entry = {\n            let streams = self.shared.streams.lock().unwrap();\n            Arc::clone(streams.get(&stream_id).ok_or_else(|| {\n                io::Error::new(\n                    io::ErrorKind::NotFound,\n                    format!(\"no such stream {stream_id}\"),\n                )\n            })?)\n        };\n        let bytes = bytes.to_vec();\n        // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): `write_all` / `finish` on a\n        // black-holed QUIC stream blocks on flow-control the dead peer never grants.\n        // Loopback (in-process) never hangs, so the bound is a no-op for it.\n        self.bounded_block_on(async move {\n            let mut send = entry.send.lock().await;\n            if !bytes.is_empty() {\n                send.write_all(&bytes).await?;\n            }\n            if finish {\n                send.finish().await?;\n            }\n            Ok(())\n        })\n    }\n\n    /// Every held stream's info — the query a re-attaching brain uses to find\n    /// streams (including peer-initiated ones that arrived while it was down).\n    pub fn stream_infos(&self) -> Vec<NetStreamInfo> {\n        let streams = self.shared.streams.lock().unwrap();\n        streams\n            .iter()\n            .map(|(id, e)| {\n                let log = e.log.lock().unwrap();\n                NetStreamInfo {\n                    stream_id: *id,\n                    conn_id: e.conn_id,\n                    remote_id_hex: e.remote_id_hex.clone(),\n                    next_seq: log.next_seq,\n                    finished: log.finished,\n                    initiated_locally: e.initiated_locally,\n                }\n            })\n            .collect()\n    }\n\n    /// Mark a held stream **retentive** with backpressure `cap` chunks (M11-W3,\n    /// REQ-SHELL-4): its read log stops evicting (no byte loss) and its read pump\n    /// lags the sender once the undrained ring reaches `cap` (no unbounded growth).\n    /// Used for shell-tunnel streams; ordinary streams stay bounded-overwrite.\n    /// No-op if the stream is gone.\n    // [impl->REQ-SHELL-4]\n    pub fn mark_stream_retentive(&self, stream_id: u64, cap: usize) {\n        if let Some(e) = self.shared.streams.lock().unwrap().get(&stream_id) {\n            e.log.lock().unwrap().set_retentive(cap);\n        }\n    }\n\n    /// Drain a retentive stream's undrained bytes (the shell-tunnel recv cursor):\n    /// pops the whole ring in order, concatenated, and wakes the read pump (which\n    /// may have been lagging the sender at the backpressure cap). Returns\n    /// `(bytes, finished)`; `finished` ⇒ the read side ended (the poller closes).\n    /// `None` ⇒ no such stream. Empty bytes + `finished == false` ⇒ nothing pending.\n    // [impl->REQ-SHELL-4]\n    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {\n        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n        let drained = entry.log.lock().unwrap().drain();\n        // The drain freed ring room — wake a pump that was backpressure-parked.\n        entry.room.notify_one();\n        Some(drained)\n    }\n\n    /// A held stream's current undrained ring depth in chunks (test introspection\n    /// for the retentive backpressure bound). `None` ⇒ no such stream.\n    #[cfg(test)]\n    fn ring_len(&self, stream_id: u64) -> Option<usize> {\n        self.shared\n            .streams\n            .lock()\n            .unwrap()\n            .get(&stream_id)\n            .map(|e| e.log.lock().unwrap().ring.len())\n    }\n\n    /// Inbound (peer-initiated) stream rows currently held on `conn_id` — the\n    /// candidate set [`Self::open_loopback_pair`] diffs to find the peer it just\n    /// minted.\n    fn inbound_rows_on(&self, conn_id: u64) -> HashSet<u64> {\n        self.shared\n            .streams\n            .lock()\n            .unwrap()\n            .iter()\n            .filter(|(_, e)| e.conn_id == conn_id && !e.initiated_locally)\n            .map(|(id, _)| *id)\n            .collect()\n    }\n\n    /// Open a same-node loopback tunnel stream PAIR (M11-W3, REQ-SHELL-4) and\n    /// return `(owner_stream, shell_stream)` — the operator row the owner side\n    /// drives and the cross-wired peer row the shell side drives. The loopback\n    /// `open_stream` mints both rows in-process (one `duplex` wires them, so a\n    /// write to either send appears on the other's read log). Serialized so the\n    /// before/after diff sees exactly the one peer row this open minted.\n    // [impl->REQ-SHELL-4]\n    pub fn open_loopback_pair(&self) -> io::Result<(u64, u64)> {\n        let _guard = self.loopback_pair_lock.lock().unwrap();\n        let (conn_id, _) = self.dial_loopback();\n        let before = self.inbound_rows_on(conn_id);\n        let owner_stream = self.open_stream(conn_id)?;\n        let shell_stream = self\n            .inbound_rows_on(conn_id)\n            .difference(&before)\n            .copied()\n            .next()\n            .ok_or_else(|| io::Error::other(\"loopback peer row not registered\"))?;\n        Ok((owner_stream, shell_stream))\n    }\n\n    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`\n    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).\n    // [impl->REQ-DAEMON-2]\n    pub(crate) fn subscribe_stream(\n        &self,\n        stream_id: u64,\n        sub: SharedSend,\n        from_seq: u64,\n    ) -> io::Result<()> {\n        let log = {\n            let streams = self.shared.streams.lock().unwrap();\n            let e = streams.get(&stream_id).ok_or_else(|| {\n                io::Error::new(\n                    io::ErrorKind::NotFound,\n                    format!(\"no such stream {stream_id}\"),\n                )\n            })?;\n            Arc::clone(&e.log)\n        };\n        log.lock().unwrap().attach(sub, from_seq);\n        Ok(())\n    }\n\n    /// Detach `sub` from a stream iff it is the current subscriber (brain\n    /// disconnect cleanup; the stream + its ring persist).\n    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {\n        let log = {\n            let streams = self.shared.streams.lock().unwrap();\n            streams.get(&stream_id).map(|e| Arc::clone(&e.log))\n        };\n        if let Some(log) = log {\n            log.lock().unwrap().detach_if(sub);\n        }\n    }","startLine":151,"lineNumbers":[151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,null,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,null,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs"}}},"isError":false,"timestamp":1784195886101}}
{"type":"message","id":"e506c273","parentId":"cec88258","timestamp":"2026-07-16T09:58:06.108Z","message":{"role":"toolResult","toolCallId":"call_TKv0kXv0UGEcuCVFtvv0ABF4|fc_047bb49263dfa303016a58ab2d7c248198a7bb87feed9d5368","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## attach.rs\n 554|    brain: &mut Brain,\n*555|    conn_id: u64,\n 556|    session_id: u64,\n 557|    from_seq: u64,\n 558|    open_op: MintedOp,\n...\n 560|) -> io::Result<u64> {\n*561|    request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)\n 562|}\n 563|\n 564|/// Like [`request_attach`], but for the cross-node leg (#4,\n...\n 573|    brain: &mut Brain,\n*574|    conn_id: u64,\n 575|    session_id: u64,\n 576|    from_seq: u64,\n 577|    open_op: MintedOp,\n...\n 580|) -> io::Result<u64> {\n*581|    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n 582|    let line = ndjson::encode_line(&AttachRecord::Request {\n 583|        session_id,\n 584|        from_seq,\n## brain.rs\n 1372|        &mut self,\n*1373|        conn_id: u64,\n 1374|        op: Option<MintedOp>,\n 1375|    ) -> io::Result<NetStreamOpened> {\n 1376|        self.send(\n...\n 1378|            serde_json::to_value(NetStreamOpenReq {\n*1379|                conn_id,\n 1380|                op_id: op.map(|o| o.seq),\n 1381|                minter: op.map(|o| o.minter),\n 1382|            })\n## broker.rs\n 2751|            thread::spawn(move || {\n*2752|                let _ = me.handle_conn(conn);\n 2753|            });\n 2754|        }\n 2755|    }\n...\n 2774|        Ok(thread::spawn(move || {\n*2775|            let _ = me.handle_conn(conn);\n 2776|        }))\n 2777|    }\n 2778|\n...\n 2788|    /// whose writer thread only drains its channel (it NEVER calls\n*2789|    /// [`BrokerConn::write`], so no watchdog is armed and the round-4 physical\n 2790|    /// bound cannot clear the injected wedge). Lets an integration rig\n 2791|    /// exercise the REQ-UPDATE-PROMOTE-DRAINED gate DECISION in isolation —\n 2792|    /// the physical bound itself is `brain_decouple.rs`'s concern. The wedge\n...\n 2830|        let (_recv, send_half) = server.split();\n*2831|        let send: SharedSend = Arc::new(BrokerConn::new(send_half, brain_write_deadline()));\n 2832|        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n 2833|        // Drain-forever (never a socket write, never finishes while tx lives):\n 2834|        // stays un-reapable as a live-but-wedged writer even under child output.\n...\n 2979|    /// restart must not disturb the hosted children).\n*2980|    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n 2981|        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n 2982|        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n 2983|        struct HandlerTally<'a>(&'a AtomicUsize);\n...\n 2996|        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n*2997|        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\n 2998|        // Base attribution fact (F-039 leg b): broker clients are brains; role\n 2999|        // facts (controller/viewer/presence/stream subscriber) accrete at the\n 3000|        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\n...\n 3207|            for id in &my_stream_subs {\n*3208|                host.detach_stream_sub(*id, &send);\n 3209|            }\n 3210|            // And presence: the liveness log + its ring persist (D4c).\n 3211|            if my_presence_sub {\n...\n 3906|    /// brain crash is deduped (no second connection) and answered with the\n*3907|    /// connection the first apply opened. The `conn_id` is recorded *inside*\n 3908|    /// the journaled closure, so a concurrent deduped replay always finds it.\n 3909|    /// A deduped id whose connection this process no longer holds (a broker\n 3910|    /// restart — the journal survives, connections don't) is a typed error\n...\n 3924|                // Direct (non-journaled) dial — no dedup, like a no-op_id input.\n*3925|                let (conn_id, remote) = host.dial(addr).map_err(|e| e.to_string())?;\n*3926|                let observed = host.conn_remote_addr_json(conn_id);\n 3927|                send_frame(\n 3928|                    send,\n*3929|                    &net_dialed_envelope(conn_id, remote, None, true, observed),\n 3930|                );\n 3931|                Ok(())\n 3932|            }\n...\n 3942|                    .apply_once(key, EffectKind::NetDial, || {\n*3943|                        let (conn_id, _) = host.dial(addr)?;\n*3944|                        host.record_dial_op(minter, op, conn_id);\n 3945|                        Ok(())\n 3946|                    })\n 3947|                    .map_err(|e| format!(\"net-dial failed: {e}\"))?;\n...\n 3949|                // the A-4b retry matches one source of truth (ADR-0034 Addendum 2).\n*3950|                let conn_id = host\n 3951|                    .dial_op_conn(minter, op)\n 3952|                    .ok_or_else(crate::effect::net_dial_no_longer_held_message)?;\n 3953|                let remote = host\n*3954|                    .conn_remote_hex(conn_id)\n 3955|                    .ok_or(\"dialed connection missing from the conn table\")?;\n 3956|                let applied_now = outcome == Outcome::Applied;\n*3957|                let observed = host.conn_remote_addr_json(conn_id);\n 3958|                send_frame(\n 3959|                    send,\n*3960|                    &net_dialed_envelope(conn_id, remote, Some(op), applied_now, observed),\n 3961|                );\n 3962|                Ok(())\n 3963|            }\n...\n 3988|        // — same derivation as the roster-member path at dispatch_pair_* uses).\n*3989|        // On a successful connect `register_conn` re-derives the SAME hex from the\n 3990|        // proven pubkey; on failure `submit_dial` tags DIAL_FAILED with it — so an\n 3991|        // outcome always matches the peer the pump submitted for.\n 3992|        let remote_id_hex = spt_proto::identity::PublicKey::from_bytes(addr.id.as_bytes())\n...\n 4013|            .ok_or(\"net disabled: this broker was bound without a network host\")?;\n*4014|        let (conn_id, remote) = host.dial_loopback();\n 4015|        send_frame(\n 4016|            send,\n*4017|            &net_dialed_envelope(conn_id, remote, None, true, serde_json::Value::Null),\n 4018|        );\n 4019|        Ok(())\n 4020|    }\n## conn.rs\n 73|/// Stable per-physical-conn identity mint — a monotonic u64 stamped at\n*74|/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n 75|/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n 76|/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n 77|/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n...\n 134|    inflight: Option<(u64, Instant)>,\n*135|    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n 136|    shutdown: bool,\n 137|}\n 138|\n...\n 144|    /// Accreted attribution facts (subscriber role, endpoint/session where\n*145|    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n 146|    label: Mutex<String>,\n 147|    /// Latch: the once-per-conn `write-start` lifecycle record fired.\n 148|    first_write_logged: AtomicBool,\n...\n 164|    /// A retired (poisoned) half parks here so its OS handle stays alive\n*165|    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\n 166|    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\n 167|    graveyard: Mutex<Option<SendHalf>>,\n 168|    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\n...\n 266|    /// (drop), `writer-exit` / `*-attach` / `*-replaced` (emitted by the\n*267|    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\n 268|    /// timeout/cancel leg is the retirement record itself.\n 269|    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 270|    fn lifecycle(&self, event: &str, extra: &str) {\n...\n 701|        (\n*702|            BrokerConn::new(send, Duration::from_millis(2000)),\n 703|            client,\n 704|        )\n 705|    }\n...\n 789|    #[test]\n*790|    fn conn_ids_are_unique_and_labels_stay_bounded() {\n 791|        let (a, _ca) = make_conn();\n 792|        let (b, _cb) = make_conn();\n 793|        assert_ne!(a.id(), b.id(), \"each physical conn mints a distinct id\");\n## digesthub.rs\n 180|        thread::spawn(move || {\n*181|            let _ = handle_conn(conn, &hub);\n 182|        });\n 183|    }\n 184|}\n## drivehub.rs\n 186|        thread::spawn(move || {\n*187|            let _ = handle_conn(conn, &hub);\n 188|        });\n 189|    }\n 190|}\n## linkhost.rs\n 664|    brain: &mut Brain,\n*665|    conn_id: u64,\n 666|    owner: &str,\n 667|    shell_ref: &str,\n 668|    action: &str,\n...\n 672|) -> io::Result<ShellLinkRequestOutcome> {\n*673|    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n 674|    let line = ndjson::encode_line(&ShellLinkRecord::Request {\n 675|        owner: owner.to_string(),\n 676|        shell_ref: shell_ref.to_string(),\n## msg.rs\n 176|/// The outcome-channel for a fire-and-forget dial (its success rides\n*177|/// [`PRESENCE_CONNECTED`] as today). Carries no live connection → `conn_id: 0`\n 178|/// sentinel; consumers correlate a fail by `kind` + `remote_id_hex`, NEVER\n*179|/// `conn_id`. Additive kind — an older brain tolerates it (the `kind` string is\n 180|/// forward-compat by design, 2.3).\n 181|// [impl->REQ-PUMP-PEER-ISOLATION]\n 182|pub const PRESENCE_DIAL_FAILED: &str = \"dial-failed\";\n...\n 667|    /// The broker-minted id addressing this connection in later net commands.\n*668|    pub conn_id: u64,\n 669|    /// The remote node's stable name (lowercase-hex pubkey), proven by the QUIC\n 670|    /// handshake (the EndpointId *is* the key).\n 671|    pub remote_id_hex: String,\n...\n 691|pub struct NetStreamOpenReq {\n*692|    /// The connection (from [`NetDialed::conn_id`]) to open the stream on.\n*693|    pub conn_id: u64,\n 694|    /// Durable op id for an exactly-once open (shares the net op-id namespace\n 695|    /// with dials/sends — one durable counter across all net effects).\n 696|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n...\n 709|    /// The connection it rides.\n*710|    pub conn_id: u64,\n 711|    /// Echoes the request's durable op id, when one was carried.\n 712|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n 713|    pub op_id: Option<u64>,\n...\n 757|    /// The connection it rides.\n*758|    pub conn_id: u64,\n 759|    /// The remote node's stable name (hex pubkey).\n 760|    pub remote_id_hex: String,\n 761|    /// The next read sequence the broker will assign (= chunks buffered so far).\n...\n 930|    /// documented sentinel for [`PRESENCE_DIAL_FAILED`] (a failed dial opened no\n*931|    /// connection) — never correlate a fail by `conn_id` (W2).\n*932|    pub conn_id: u64,\n 933|    /// The remote node's stable name (hex pubkey). The per-peer correlation key\n 934|    /// (W2): concurrent dial outcomes arrive out of submit order and are matched\n 935|    /// to their peer by THIS, never by arrival position.\n...\n 1097|pub fn net_dialed_envelope(\n*1098|    conn_id: u64,\n 1099|    remote_id_hex: String,\n 1100|    op_id: Option<u64>,\n 1101|    applied_now: bool,\n...\n 1106|        serde_json::to_value(NetDialed {\n*1107|            conn_id,\n 1108|            remote_id_hex,\n 1109|            op_id,\n 1110|            applied_now,\n...\n 1202|        let ev: NetDialed = serde_json::from_value(env.payload).unwrap();\n*1203|        assert_eq!((ev.conn_id, ev.op_id, ev.applied_now), (3, Some(9), false));\n 1204|        assert_eq!(ev.remote_id_hex, \"cafe\");\n 1205|        assert_eq!(ev.remote_addr, observed, \"observed addr rides the ack\");\n 1206|\n...\n 1209|        // skips it).\n*1210|        let old = json!({ \"conn_id\": 1, \"remote_id_hex\": \"ab\" });\n 1211|        let ev: NetDialed = serde_json::from_value(old).unwrap();\n 1212|        assert!(ev.remote_addr.is_null());\n 1213|    }\n...\n 1221|            kind: PRESENCE_DISCONNECTED.to_string(),\n*1222|            conn_id: 2,\n 1223|            remote_id_hex: \"cafe\".into(),\n 1224|            reason: None,\n 1225|            remote_addr: serde_json::Value::Null,\n...\n 1229|        let back: NetPresenceEvent = serde_json::from_value(env.payload).unwrap();\n*1230|        assert_eq!((back.seq, back.conn_id), (4, 2));\n 1231|        assert_eq!(back.kind, PRESENCE_DISCONNECTED);\n 1232|        assert_eq!(back.remote_id_hex, \"cafe\");\n 1233|        // Absent optional fields stay absent on the wire (old-brain-safe).\n...\n 1236|        let wire = json!({\n*1237|            \"seq\": 9, \"kind\": \"throttled-at-d9\", \"conn_id\": 1, \"remote_id_hex\": \"ab\",\n 1238|            \"field_added_later\": true\n 1239|        });\n 1240|        let future: NetPresenceEvent = serde_json::from_value(wire).unwrap();\n...\n 1242|\n*1243|        // W2 additive fields: a DIAL_FAILED carries a reason + the conn_id:0\n 1244|        // sentinel; a CONNECTED can carry the observed remote_addr (REQ-CONV-1).\n 1245|        // [unit->REQ-PUMP-PEER-ISOLATION]\n 1246|        let failed = NetPresenceEvent {\n...\n 1248|            kind: PRESENCE_DIAL_FAILED.to_string(),\n*1249|            conn_id: 0,\n 1250|            remote_id_hex: \"dead\".into(),\n 1251|            reason: Some(\"no route\".into()),\n 1252|            remote_addr: serde_json::Value::Null,\n...\n 1256|        assert_eq!(back.kind, PRESENCE_DIAL_FAILED);\n*1257|        assert_eq!(back.conn_id, 0, \"failed dial uses the conn_id:0 sentinel\");\n 1258|        assert_eq!(back.reason.as_deref(), Some(\"no route\"));\n 1259|    }\n 1260|\n## nethost.rs\n 296|/// PresenceChannel seam (D4c, REQ-EP-4): the StreamLog ring/cursor discipline\n*297|/// applied to the conn table itself. `register_conn` appends `connected`; each\n 298|/// connection's closed-watcher appends `disconnected` after removing the row.\n 299|/// A brain subscribes with a cursor and replays events produced while it was\n 300|/// down, so presence survives a brain restart exactly like stream reads do.\n...\n 319|    /// Append a liveness event: assign the next `seq`, retain it in the bounded\n*320|    /// ring, live-send to the current subscriber. Producers: `register_conn`\n 321|    /// (connected) and the per-conn closed-watchers (disconnected) — every\n 322|    /// append serializes on the enclosing `Mutex`, so subscriber frames arrive\n 323|    /// in strict `seq` order.\n*324|    fn append(&mut self, kind: &str, conn_id: u64, remote_id_hex: &str) {\n 325|        self.push(NetPresenceEvent {\n 326|            seq: 0, // assigned in push\n 327|            kind: kind.to_string(),\n*328|            conn_id,\n 329|            remote_id_hex: remote_id_hex.to_string(),\n 330|            reason: None,\n 331|            remote_addr: serde_json::Value::Null,\n...\n 339|    // [impl->REQ-CONV-1]\n*340|    fn append_connected(&mut self, conn_id: u64, remote_id_hex: &str, remote_addr: serde_json::Value) {\n 341|        self.push(NetPresenceEvent {\n 342|            seq: 0,\n 343|            kind: PRESENCE_CONNECTED.to_string(),\n*344|            conn_id,\n 345|            remote_id_hex: remote_id_hex.to_string(),\n 346|            reason: None,\n 347|            remote_addr,\n...\n 351|    /// A `dial-failed` event (PUMP-TRUTH W2): a non-blocking [`KIND_NET_DIAL_SUBMIT`]\n*352|    /// dial failed. No connection opened → the documented `conn_id: 0` sentinel;\n*353|    /// the pump correlates by `remote_id_hex` + `kind`, never `conn_id`.\n 354|    // [impl->REQ-PUMP-PEER-ISOLATION]\n 355|    fn append_dial_failed(&mut self, remote_id_hex: &str, reason: String) {\n 356|        self.push(NetPresenceEvent {\n...\n 358|            kind: PRESENCE_DIAL_FAILED.to_string(),\n*359|            conn_id: 0,\n 360|            remote_id_hex: remote_id_hex.to_string(),\n 361|            reason: Some(reason),\n 362|            remote_addr: serde_json::Value::Null,\n...\n 458|struct StreamEntry {\n*459|    conn_id: u64,\n 460|    remote_id_hex: String,\n 461|    /// Whether THIS node opened the stream (`open_stream`) vs the peer\n 462|    /// (accept loop). The inbound dispatcher (D9-1) claims only\n...\n 479|struct NetShared {\n*480|    next_conn_id: AtomicU64,\n 481|    conns: Mutex<HashMap<u64, ConnEntry>>,\n 482|    next_stream_id: AtomicU64,\n 483|    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,\n...\n 491|        NetShared {\n*492|            next_conn_id: AtomicU64::new(1),\n 493|            conns: Mutex::new(HashMap::new()),\n 494|            next_stream_id: AtomicU64::new(1),\n 495|            streams: Mutex::new(HashMap::new()),\n...\n 506|/// the path address string, or `null` when id-only / path-less (nothing worth\n*507|/// seeding over discovery). Shared by [`register_conn`]'s presence write-back\n 508|/// and [`NetHost::conn_remote_addr_json`] so both seed identically.\n 509|// [impl->REQ-CONV-1]\n 510|fn observed_addr_json(conn: &Connection) -> serde_json::Value {\n...\n 533|    /// the dial before it registers; Mesh-D4 roster leg rides the same stream on\n*534|    /// success), then register the conn (which emits `PRESENCE_CONNECTED` with\n 535|    /// the observed addr). The caller bounds this await (the blocking dial via\n 536|    /// `bounded_block_on`; the submit via its own `timeout`).\n 537|    async fn connect(self, addr: EndpointAddr) -> io::Result<(u64, String)> {\n...\n 560|        };\n*561|        Ok(register_conn(&self.shared, conn, proven))\n 562|    }\n 563|}\n 564|\n*565|fn register_conn(\n 566|    shared: &Arc<NetShared>,\n 567|    conn: Connection,\n 568|    proven_subnets: HashSet<String>,\n...\n 572|        .to_hex();\n*573|    let id = shared.next_conn_id.fetch_add(1, Ordering::Relaxed);\n 574|    shared.conns.lock().unwrap().insert(\n 575|        id,\n 576|        ConnEntry {\n...\n 639|    shared: &Arc<NetShared>,\n*640|    conn_id: u64,\n 641|    remote_id_hex: &str,\n 642|    send: SendHalf,\n 643|    recv: RecvHalf,\n...\n 651|        Arc::new(StreamEntry {\n*652|            conn_id,\n 653|            remote_id_hex: remote_id_hex.to_string(),\n 654|            initiated_locally,\n 655|            send: tokio::sync::Mutex::new(send),\n...\n 727|    shared: Arc<NetShared>,\n*728|    /// `(minter, op_id) → conn_id` for exactly-once dial replies: a deduped replay\n 729|    /// of a journaled dial is answered with the connection the first apply opened.\n 730|    /// Keyed by `(minter, op_id)` (ADR-0034) — the minter namespaces the op so two\n 731|    /// counters that both start at 1 cannot clobber each other's dial record.\n## propagate.rs\n 332|\n*333|/// Requester side: pull a newer release from the peer over `conn_id`, gate it\n 334|/// per-node, and stage it on success. `open_op` is the durable id for the\n 335|/// exactly-once stream open; everything after rides fire-and-forget. A torn\n 336|/// pull errors — recovery is a fresh `request_update` (the query is\n...\n 346|    brain: &mut Brain,\n*347|    conn_id: u64,\n 348|    open_op: MintedOp,\n 349|    running: &BrokerAbi,\n 350|    policy: &VerifyPolicy,\n...\n 353|) -> io::Result<UpdatePullOutcome> {\n*354|    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n 355|    let stream_id = opened.stream_id;\n 356|    brain.net_stream_subscribe(stream_id, 0)?;\n 357|\n...\n 554|// [impl->REQ-UPD-6]\n*555|pub fn request_update_status(brain: &mut Brain, conn_id: u64) -> io::Result<UpdateStatusReport> {\n*556|    let opened = brain.net_open_stream(conn_id, None)?;\n 557|    let stream_id = opened.stream_id;\n 558|    brain.net_stream_subscribe(stream_id, 0)?;\n 559|\n## resthost.rs\n 185|    brain: &mut Brain,\n*186|    conn_id: u64,\n 187|    endpoint: &str,\n 188|    event_tag: &str,\n 189|    op_id: &str,\n...\n 191|) -> io::Result<RestRequestOutcome> {\n*192|    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n 193|    let line = ndjson::encode_line(&RestRecord::Request {\n 194|        endpoint: endpoint.to_string(),\n 195|        event: event_tag.to_string(),\n## seedmap.rs\n 142|        thread::spawn(move || {\n*143|            let _ = handle_conn(conn, &reg, &stop, &name);\n 144|        });\n 145|    }\n 146|}\n## seedproofx.rs\n 24|//! Dialer opens it (`open_bi`); acceptor accepts it (`accept_bi`) *before*\n*25|//! `register_conn` wires the general app-stream acceptor, so it is never\n 26|//! confused with an app stream. Then a four-leg ping-pong:\n 27|//!\n 28|//! 1. dialer → `Hello { nonce_d, subnets = its local names }`\n...\n 457|/// (nothing full, nothing re-seeded) drops the conn (`None`). A re-seed-only\n*458|/// exchange registers with an EMPTY proven set: the conn is kept alive only so\n 459|/// the seed push it just carried is actually delivered (dropping it here races\n 460|/// the QUIC CLOSE against the peer's read) — it serves no subnet and is replaced\n 461|/// when the healed peer reconnects full. Otherwise the proven (full) set joins.\n## serveprobe.rs\n 113|// [impl->REQ-SUBNET-5]\n*114|pub fn request_subnet_probe(brain: &mut Brain, conn_id: u64, subnet: &str) -> io::Result<bool> {\n*115|    let opened = brain.net_open_stream(conn_id, None)?;\n 116|    let line = ndjson::encode_line(&ServeProbeRecord::Request {\n 117|        serve_probe: subnet.to_string(),\n 118|    });\n## shellwake.rs\n 388|                &mut brain,\n*389|                conn.conn_id,\n 390|                owner,\n 391|                spt_net::net::rest::REST_EVENT_WAKE,\n 392|                &op_id,\n## sync.rs\n 328|\n*329|/// Requester side: pull `refs` from the peer over `conn_id`, apply each\n 330|/// fetched tip with the vector driver, and report per-branch verdicts.\n 331|/// `open_op` is the durable id for the exactly-once stream open; everything\n 332|/// after rides fire-and-forget. A torn pull errors — recovery is a fresh\n...\n 336|    brain: &mut Brain,\n*337|    conn_id: u64,\n 338|    refs: &[String],\n 339|    open_op: MintedOp,\n 340|    cs: &ContextStore,\n...\n 342|) -> io::Result<SyncPullReport> {\n*343|    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n 344|    let stream_id = opened.stream_id;\n 345|    brain.net_stream_subscribe(stream_id, 0)?;\n 346|\n## tunnelhub.rs\n 279|        thread::spawn(move || {\n*280|            let _ = handle_conn(conn, &hub, &broker);\n 281|        });\n 282|    }\n 283|}\n\n## pump/\n### mod.rs\n 481|    /// The established peer conn id for this step.\n*482|    conn_id: u64,\n 483|    /// The pump's sole durable open-op counter. Workers mint open-ops only\n 484|    /// through [`PeerIo::open_op`] (the sync + update legs); a worker-owned\n 485|    /// `EpochSource` over this counter would be the KH epoch-lease double-mint.\n...\n 492|    fn push_feed(&mut self, feed: &[u8]) -> io::Result<()> {\n*493|        push_feed(self.brain, self.conn_id, feed)\n 494|    }\n 495|\n 496|    /// Mint the next durable open-op from the pump's sole counter (exactly-once\n...\n 620|        for (peer_hex, subs) in &peer_subnets {\n*621|            if let Some(&conn_id) = conns.get(peer_hex) {\n 622|                run_peer_subnets(\n 623|                    &mut brain, &mut ops, &mut workers, &due_flags, &mut conns, &mut sched,\n*624|                    conn_id, subs, peer_hex, &ctx, round_start,\n 625|                )?;\n 626|            } else if peer_eligible(&sched, peer_hex, round_start) {\n 627|                if let Some(addr) = resolve_submit_addr(&resolver, peer_hex, &paths.peer_addrs) {\n...\n 771|    sched: &mut HashMap<String, PeerSched>,\n*772|    conn_id: u64,\n 773|    subs: &[String],\n 774|    peer_hex: &str,\n 775|    ctx: &RoundCtx,\n...\n 781|                brain: &mut *brain,\n*782|                conn_id,\n 783|                ops: &mut *ops,\n 784|            };\n 785|            workers[i].peer_step(&mut io, sub_name, peer_hex, ctx)\n...\n 829|                sched,\n*830|                ev.conn_id,\n 831|                subs,\n 832|                &ev.remote_id_hex,\n 833|                ctx,\n...\n 858|        PRESENCE_CONNECTED => {\n*859|            conns.insert(ev.remote_id_hex.clone(), ev.conn_id);\n 860|            sched.remove(&ev.remote_id_hex); // live peer → reset backoff\n 861|            pending.remove(&ev.remote_id_hex);\n 862|            // REQ-CONV-1 write-back: seed the OBSERVED addr from the async connect.\n...\n 884|            // Drop the cached conn — redial-eligible immediately, NO backoff (Q3).\n*885|            conns.retain(|_, cid| *cid != ev.conn_id);\n 886|            false\n 887|        }\n 888|        _ => false, // a future presence kind an older pump tolerates\n...\n 960|\n*961|/// Open a fresh stream on `conn_id`, write one whole feed fire-and-forget,\n 962|/// and finish it (the receiver's dispatcher classifies + applies). One\n 963|/// stream per feed: streams are single-purpose by the demux contract.\n*964|fn push_feed(brain: &mut Brain, conn_id: u64, feed: &[u8]) -> io::Result<()> {\n*965|    let opened = brain.net_open_stream(conn_id, None)?;\n 966|    brain.net_stream_send(opened.stream_id, feed, None, true)?;\n 967|    Ok(())\n 968|}\n...\n 1496|\n*1497|        let ev = |kind: &str, conn_id: u64, hex: &str| NetPresenceEvent {\n 1498|            seq: 0,\n 1499|            kind: kind.to_string(),\n*1500|            conn_id,\n 1501|            remote_id_hex: hex.to_string(),\n 1502|            reason: None,\n 1503|            remote_addr: serde_json::Value::Null,\n...\n 1580|            kind: PRESENCE_CONNECTED.to_string(),\n*1581|            conn_id: 1,\n 1582|            remote_id_hex: \"aa\".into(),\n 1583|            reason: None,\n 1584|            remote_addr: serde_json::Value::Null,\n### sync.rs\n 106|            let open_op = io.open_op()?;\n*107|            let report = request_sync(&mut *io.brain, io.conn_id, &want, open_op, &cs, &self.scratch)?;\n 108|            (self.hooks.on_pull)(peer_hex, &report);\n 109|        }\n 110|        Ok(())\n### update.rs\n 114|            &mut *io.brain,\n*115|            io.conn_id,\n 116|            open_op,\n 117|            &BrokerAbi::current(),\n 118|            &policy,\n\nShowing files 1-20 of 22+. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"crates/spt-daemon/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":110,"fileCount":20,"files":["crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/digesthub.rs","crates/spt-daemon/src/drivehub.rs","crates/spt-daemon/src/linkhost.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/propagate.rs","crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/pump/sync.rs","crates/spt-daemon/src/pump/update.rs","crates/spt-daemon/src/resthost.rs","crates/spt-daemon/src/seedmap.rs","crates/spt-daemon/src/seedproofx.rs","crates/spt-daemon/src/serveprobe.rs","crates/spt-daemon/src/shellwake.rs","crates/spt-daemon/src/sync.rs","crates/spt-daemon/src/tunnelhub.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/attach.rs","count":4},{"path":"crates/spt-daemon/src/brain.rs","count":2},{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/conn.rs","count":7},{"path":"crates/spt-daemon/src/digesthub.rs","count":1},{"path":"crates/spt-daemon/src/drivehub.rs","count":1},{"path":"crates/spt-daemon/src/linkhost.rs","count":2},{"path":"crates/spt-daemon/src/msg.rs","count":19},{"path":"crates/spt-daemon/src/nethost.rs","count":20},{"path":"crates/spt-daemon/src/propagate.rs","count":5},{"path":"crates/spt-daemon/src/pump/mod.rs","count":15},{"path":"crates/spt-daemon/src/pump/sync.rs","count":1},{"path":"crates/spt-daemon/src/pump/update.rs","count":1},{"path":"crates/spt-daemon/src/resthost.rs","count":2},{"path":"crates/spt-daemon/src/seedmap.rs","count":1},{"path":"crates/spt-daemon/src/seedproofx.rs","count":2},{"path":"crates/spt-daemon/src/serveprobe.rs","count":2},{"path":"crates/spt-daemon/src/shellwake.rs","count":1},{"path":"crates/spt-daemon/src/sync.rs","count":3},{"path":"crates/spt-daemon/src/tunnelhub.rs","count":1}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## attach.rs\n 554│    brain: &mut Brain,\n*555│    conn_id: u64,\n 556│    session_id: u64,\n 557│    from_seq: u64,\n 558│    open_op: MintedOp,\n    │...\n 560│) -> io::Result<u64> {\n*561│    request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)\n 562│}\n 563│\n 564│/// Like [`request_attach`], but for the cross-node leg (#4,\n    │...\n 573│    brain: &mut Brain,\n*574│    conn_id: u64,\n 575│    session_id: u64,\n 576│    from_seq: u64,\n 577│    open_op: MintedOp,\n    │...\n 580│) -> io::Result<u64> {\n*581│    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n 582│    let line = ndjson::encode_line(&AttachRecord::Request {\n 583│        session_id,\n 584│        from_seq,\n## brain.rs\n 1372│        &mut self,\n*1373│        conn_id: u64,\n 1374│        op: Option<MintedOp>,\n 1375│    ) -> io::Result<NetStreamOpened> {\n 1376│        self.send(\n     │...\n 1378│            serde_json::to_value(NetStreamOpenReq {\n*1379│                conn_id,\n 1380│                op_id: op.map(|o| o.seq),\n 1381│                minter: op.map(|o| o.minter),\n 1382│            })\n## broker.rs\n 2751│            thread::spawn(move || {\n*2752│                let _ = me.handle_conn(conn);\n 2753│            });\n 2754│        }\n 2755│    }\n     │...\n 2774│        Ok(thread::spawn(move || {\n*2775│            let _ = me.handle_conn(conn);\n 2776│        }))\n 2777│    }\n 2778│\n     │...\n 2788│    /// whose writer thread only drains its channel (it NEVER calls\n*2789│    /// [`BrokerConn::write`], so no watchdog is armed and the round-4 physical\n 2790│    /// bound cannot clear the injected wedge). Lets an integration rig\n 2791│    /// exercise the REQ-UPDATE-PROMOTE-DRAINED gate DECISION in isolation —\n 2792│    /// the physical bound itself is `brain_decouple.rs`'s concern. The wedge\n     │...\n 2830│        let (_recv, send_half) = server.split();\n*2831│        let send: SharedSend = Arc::new(BrokerConn::new(send_half, brain_write_deadline()));\n 2832│        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n 2833│        // Drain-forever (never a socket write, never finishes while tx lives):\n 2834│        // stays un-reapable as a live-but-wedged writer even under child output.\n     │...\n 2979│    /// restart must not disturb the hosted children).\n*2980│    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n 2981│        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n 2982│        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n 2983│        struct HandlerTally<'a>(&'a AtomicUsize);\n     │...\n 2996│        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n*2997│        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\n 2998│        // Base attribution fact (F-039 leg b): broker clients are brains; role\n 2999│        // facts (controller/viewer/presence/stream subscriber) accrete at the\n 3000│        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\n     │...\n 3207│            for id in &my_stream_subs {\n*3208│                host.detach_stream_sub(*id, &send);\n 3209│            }\n 3210│            // And presence: the liveness log + its ring persist (D4c).\n 3211│            if my_presence_sub {\n     │...\n 3906│    /// brain crash is deduped (no second connection) and answered with the\n*3907│    /// connection the first apply opened. The `conn_id` is recorded *inside*\n 3908│    /// the journaled closure, so a concurrent deduped replay always finds it.\n 3909│    /// A deduped id whose connection this process no longer holds (a broker\n 3910│    /// restart — the journal survives, connections don't) is a typed error\n     │...\n 3924│                // Direct (non-journaled) dial — no dedup, like a no-op_id input.\n*3925│                let (conn_id, remote) = host.dial(addr).map_err(|e| e.to_string())?;\n*3926│                let observed = host.conn_remote_addr_json(conn_id);\n 3927│                send_frame(\n 3928│                    send,\n*3929│                    &net_dialed_envelope(conn_id, remote, None, true, observed),\n 3930│                );\n 3931│                Ok(())\n 3932│            }\n     │...\n 3942│                    .apply_once(key, EffectKind::NetDial, || {\n*3943│                        let (conn_id, _) = host.dial(addr)?;\n*3944│                        host.record_dial_op(minter, op, conn_id);\n 3945│                        Ok(())\n 3946│                    })\n 3947│                    .map_err(|e| format!(\"net-dial failed: {e}\"))?;\n     │...\n 3949│                // the A-4b retry matches one source of truth (ADR-0034 Addendum 2).\n*3950│                let conn_id = host\n 3951│                    .dial_op_conn(minter, op)\n 3952│                    .ok_or_else(crate::effect::net_dial_no_longer_held_message)?;\n 3953│                let remote = host\n*3954│                    .conn_remote_hex(conn_id)\n 3955│                    .ok_or(\"dialed connection missing from the conn table\")?;\n 3956│                let applied_now = outcome == Outcome::Applied;\n*3957│                let observed = host.conn_remote_addr_json(conn_id);\n 3958│                send_frame(\n 3959│                    send,\n*3960│                    &net_dialed_envelope(conn_id, remote, Some(op), applied_now, observed),\n 3961│                );\n 3962│                Ok(())\n 3963│            }\n     │...\n 3988│        // — same derivation as the roster-member path at dispatch_pair_* uses).\n*3989│        // On a successful connect `register_conn` re-derives the SAME hex from the\n 3990│        // proven pubkey; on failure `submit_dial` tags DIAL_FAILED with it — so an\n 3991│        // outcome always matches the peer the pump submitted for.\n 3992│        let remote_id_hex = spt_proto::identity::PublicKey::from_bytes(addr.id.as_bytes())\n     │...\n 4013│            .ok_or(\"net disabled: this broker was bound without a network host\")?;\n*4014│        let (conn_id, remote) = host.dial_loopback();\n 4015│        send_frame(\n 4016│            send,\n*4017│            &net_dialed_envelope(conn_id, remote, None, true, serde_json::Value::Null),\n 4018│        );\n 4019│        Ok(())\n 4020│    }\n## conn.rs\n  73│/// Stable per-physical-conn identity mint — a monotonic u64 stamped at\n *74│/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n  75│/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n  76│/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n  77│/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n    │...\n 134│    inflight: Option<(u64, Instant)>,\n*135│    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n 136│    shutdown: bool,\n 137│}\n 138│\n    │...\n 144│    /// Accreted attribution facts (subscriber role, endpoint/session where\n*145│    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n 146│    label: Mutex<String>,\n 147│    /// Latch: the once-per-conn `write-start` lifecycle record fired.\n 148│    first_write_logged: AtomicBool,\n    │...\n 164│    /// A retired (poisoned) half parks here so its OS handle stays alive\n*165│    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\n 166│    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\n 167│    graveyard: Mutex<Option<SendHalf>>,\n 168│    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\n    │...\n 266│    /// (drop), `writer-exit` / `*-attach` / `*-replaced` (emitted by the\n*267│    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\n 268│    /// timeout/cancel leg is the retirement record itself.\n 269│    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 270│    fn lifecycle(&self, event: &str, extra: &str) {\n    │...\n 701│        (\n*702│            BrokerConn::new(send, Duration::from_millis(2000)),\n 703│            client,\n 704│        )\n 705│    }\n    │...\n 789│    #[test]\n*790│    fn conn_ids_are_unique_and_labels_stay_bounded() {\n 791│        let (a, _ca) = make_conn();\n 792│        let (b, _cb) = make_conn();\n 793│        assert_ne!(a.id(), b.id(), \"each physical conn mints a distinct id\");\n## digesthub.rs\n 180│        thread::spawn(move || {\n*181│            let _ = handle_conn(conn, &hub);\n 182│        });\n 183│    }\n 184│}\n## drivehub.rs\n 186│        thread::spawn(move || {\n*187│            let _ = handle_conn(conn, &hub);\n 188│        });\n 189│    }\n 190│}\n## linkhost.rs\n 664│    brain: &mut Brain,\n*665│    conn_id: u64,\n 666│    owner: &str,\n 667│    shell_ref: &str,\n 668│    action: &str,\n    │...\n 672│) -> io::Result<ShellLinkRequestOutcome> {\n*673│    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n 674│    let line = ndjson::encode_line(&ShellLinkRecord::Request {\n 675│        owner: owner.to_string(),\n 676│        shell_ref: shell_ref.to_string(),\n## msg.rs\n  176│/// The outcome-channel for a fire-and-forget dial (its success rides\n *177│/// [`PRESENCE_CONNECTED`] as today). Carries no live connection → `conn_id: 0`\n  178│/// sentinel; consumers correlate a fail by `kind` + `remote_id_hex`, NEVER\n *179│/// `conn_id`. Additive kind — an older brain tolerates it (the `kind` string is\n  180│/// forward-compat by design, 2.3).\n  181│// [impl->REQ-PUMP-PEER-ISOLATION]\n  182│pub const PRESENCE_DIAL_FAILED: &str = \"dial-failed\";\n     │...\n  667│    /// The broker-minted id addressing this connection in later net commands.\n *668│    pub conn_id: u64,\n  669│    /// The remote node's stable name (lowercase-hex pubkey), proven by the QUIC\n  670│    /// handshake (the EndpointId *is* the key).\n  671│    pub remote_id_hex: String,\n     │...\n  691│pub struct NetStreamOpenReq {\n *692│    /// The connection (from [`NetDialed::conn_id`]) to open the stream on.\n *693│    pub conn_id: u64,\n  694│    /// Durable op id for an exactly-once open (shares the net op-id namespace\n  695│    /// with dials/sends — one durable counter across all net effects).\n  696│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n     │...\n  709│    /// The connection it rides.\n *710│    pub conn_id: u64,\n  711│    /// Echoes the request's durable op id, when one was carried.\n  712│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n  713│    pub op_id: Option<u64>,\n     │...\n  757│    /// The connection it rides.\n *758│    pub conn_id: u64,\n  759│    /// The remote node's stable name (hex pubkey).\n  760│    pub remote_id_hex: String,\n  761│    /// The next read sequence the broker will assign (= chunks buffered so far).\n     │...\n  930│    /// documented sentinel for [`PRESENCE_DIAL_FAILED`] (a failed dial opened no\n *931│    /// connection) — never correlate a fail by `conn_id` (W2).\n *932│    pub conn_id: u64,\n  933│    /// The remote node's stable name (hex pubkey). The per-peer correlation key\n  934│    /// (W2): concurrent dial outcomes arrive out of submit order and are matched\n  935│    /// to their peer by THIS, never by arrival position.\n     │...\n 1097│pub fn net_dialed_envelope(\n*1098│    conn_id: u64,\n 1099│    remote_id_hex: String,\n 1100│    op_id: Option<u64>,\n 1101│    applied_now: bool,\n     │...\n 1106│        serde_json::to_value(NetDialed {\n*1107│            conn_id,\n 1108│            remote_id_hex,\n 1109│            op_id,\n 1110│            applied_now,\n     │...\n 1202│        let ev: NetDialed = serde_json::from_value(env.payload).unwrap();\n*1203│        assert_eq!((ev.conn_id, ev.op_id, ev.applied_now), (3, Some(9), false));\n 1204│        assert_eq!(ev.remote_id_hex, \"cafe\");\n 1205│        assert_eq!(ev.remote_addr, observed, \"observed addr rides the ack\");\n 1206│\n     │...\n 1209│        // skips it).\n*1210│        let old = json!({ \"conn_id\": 1, \"remote_id_hex\": \"ab\" });\n 1211│        let ev: NetDialed = serde_json::from_value(old).unwrap();\n 1212│        assert!(ev.remote_addr.is_null());\n 1213│    }\n     │...\n 1221│            kind: PRESENCE_DISCONNECTED.to_string(),\n*1222│            conn_id: 2,\n 1223│            remote_id_hex: \"cafe\".into(),\n 1224│            reason: None,\n 1225│            remote_addr: serde_json::Value::Null,\n     │...\n 1229│        let back: NetPresenceEvent = serde_json::from_value(env.payload).unwrap();\n*1230│        assert_eq!((back.seq, back.conn_id), (4, 2));\n 1231│        assert_eq!(back.kind, PRESENCE_DISCONNECTED);\n 1232│        assert_eq!(back.remote_id_hex, \"cafe\");\n 1233│        // Absent optional fields stay absent on the wire (old-brain-safe).\n     │...\n 1236│        let wire = json!({\n*1237│            \"seq\": 9, \"kind\": \"throttled-at-d9\", \"conn_id\": 1, \"remote_id_hex\": \"ab\",\n 1238│            \"field_added_later\": true\n 1239│        });\n 1240│        let future: NetPresenceEvent = serde_json::from_value(wire).unwrap();\n     │...\n 1242│\n*1243│        // W2 additive fields: a DIAL_FAILED carries a reason + the conn_id:0\n 1244│        // sentinel; a CONNECTED can carry the observed remote_addr (REQ-CONV-1).\n 1245│        // [unit->REQ-PUMP-PEER-ISOLATION]\n 1246│        let failed = NetPresenceEvent {\n     │...\n 1248│            kind: PRESENCE_DIAL_FAILED.to_string(),\n*1249│            conn_id: 0,\n 1250│            remote_id_hex: \"dead\".into(),\n 1251│            reason: Some(\"no route\".into()),\n 1252│            remote_addr: serde_json::Value::Null,\n     │...\n 1256│        assert_eq!(back.kind, PRESENCE_DIAL_FAILED);\n*1257│        assert_eq!(back.conn_id, 0, \"failed dial uses the conn_id:0 sentinel\");\n 1258│        assert_eq!(back.reason.as_deref(), Some(\"no route\"));\n 1259│    }\n 1260│\n## nethost.rs\n 296│/// PresenceChannel seam (D4c, REQ-EP-4): the StreamLog ring/cursor discipline\n*297│/// applied to the conn table itself. `register_conn` appends `connected`; each\n 298│/// connection's closed-watcher appends `disconnected` after removing the row.\n 299│/// A brain subscribes with a cursor and replays events produced while it was\n 300│/// down, so presence survives a brain restart exactly like stream reads do.\n    │...\n 319│    /// Append a liveness event: assign the next `seq`, retain it in the bounded\n*320│    /// ring, live-send to the current subscriber. Producers: `register_conn`\n 321│    /// (connected) and the per-conn closed-watchers (disconnected) — every\n 322│    /// append serializes on the enclosing `Mutex`, so subscriber frames arrive\n 323│    /// in strict `seq` order.\n*324│    fn append(&mut self, kind: &str, conn_id: u64, remote_id_hex: &str) {\n 325│        self.push(NetPresenceEvent {\n 326│            seq: 0, // assigned in push\n 327│            kind: kind.to_string(),\n*328│            conn_id,\n 329│            remote_id_hex: remote_id_hex.to_string(),\n 330│            reason: None,\n 331│            remote_addr: serde_json::Value::Null,\n    │...\n 339│    // [impl->REQ-CONV-1]\n*340│    fn append_connected(&mut self, conn_id: u64, remote_id_hex: &str, remote_addr: serde_json::Value) {\n 341│        self.push(NetPresenceEvent {\n 342│            seq: 0,\n 343│            kind: PRESENCE_CONNECTED.to_string(),\n*344│            conn_id,\n 345│            remote_id_hex: remote_id_hex.to_string(),\n 346│            reason: None,\n 347│            remote_addr,\n    │...\n 351│    /// A `dial-failed` event (PUMP-TRUTH W2): a non-blocking [`KIND_NET_DIAL_SUBMIT`]\n*352│    /// dial failed. No connection opened → the documented `conn_id: 0` sentinel;\n*353│    /// the pump correlates by `remote_id_hex` + `kind`, never `conn_id`.\n 354│    // [impl->REQ-PUMP-PEER-ISOLATION]\n 355│    fn append_dial_failed(&mut self, remote_id_hex: &str, reason: String) {\n 356│        self.push(NetPresenceEvent {\n    │...\n 358│            kind: PRESENCE_DIAL_FAILED.to_string(),\n*359│            conn_id: 0,\n 360│            remote_id_hex: remote_id_hex.to_string(),\n 361│            reason: Some(reason),\n 362│            remote_addr: serde_json::Value::Null,\n    │...\n 458│struct StreamEntry {\n*459│    conn_id: u64,\n 460│    remote_id_hex: String,\n 461│    /// Whether THIS node opened the stream (`open_stream`) vs the peer\n 462│    /// (accept loop). The inbound dispatcher (D9-1) claims only\n    │...\n 479│struct NetShared {\n*480│    next_conn_id: AtomicU64,\n 481│    conns: Mutex<HashMap<u64, ConnEntry>>,\n 482│    next_stream_id: AtomicU64,\n 483│    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,\n    │...\n 491│        NetShared {\n*492│            next_conn_id: AtomicU64::new(1),\n 493│            conns: Mutex::new(HashMap::new()),\n 494│            next_stream_id: AtomicU64::new(1),\n 495│            streams: Mutex::new(HashMap::new()),\n    │...\n 506│/// the path address string, or `null` when id-only / path-less (nothing worth\n*507│/// seeding over discovery). Shared by [`register_conn`]'s presence write-back\n 508│/// and [`NetHost::conn_remote_addr_json`] so both seed identically.\n 509│// [impl->REQ-CONV-1]\n 510│fn observed_addr_json(conn: &Connection) -> serde_json::Value {\n    │...\n 533│    /// the dial before it registers; Mesh-D4 roster leg rides the same stream on\n*534│    /// success), then register the conn (which emits `PRESENCE_CONNECTED` with\n 535│    /// the observed addr). The caller bounds this await (the blocking dial via\n 536│    /// `bounded_block_on`; the submit via its own `timeout`).\n 537│    async fn connect(self, addr: EndpointAddr) -> io::Result<(u64, String)> {\n    │...\n 560│        };\n*561│        Ok(register_conn(&self.shared, conn, proven))\n 562│    }\n 563│}\n 564│\n*565│fn register_conn(\n 566│    shared: &Arc<NetShared>,\n 567│    conn: Connection,\n 568│    proven_subnets: HashSet<String>,\n    │...\n 572│        .to_hex();\n*573│    let id = shared.next_conn_id.fetch_add(1, Ordering::Relaxed);\n 574│    shared.conns.lock().unwrap().insert(\n 575│        id,\n 576│        ConnEntry {\n    │...\n 639│    shared: &Arc<NetShared>,\n*640│    conn_id: u64,\n 641│    remote_id_hex: &str,\n 642│    send: SendHalf,\n 643│    recv: RecvHalf,\n    │...\n 651│        Arc::new(StreamEntry {\n*652│            conn_id,\n 653│            remote_id_hex: remote_id_hex.to_string(),\n 654│            initiated_locally,\n 655│            send: tokio::sync::Mutex::new(send),\n    │...\n 727│    shared: Arc<NetShared>,\n*728│    /// `(minter, op_id) → conn_id` for exactly-once dial replies: a deduped replay\n 729│    /// of a journaled dial is answered with the connection the first apply opened.\n 730│    /// Keyed by `(minter, op_id)` (ADR-0034) — the minter namespaces the op so two\n 731│    /// counters that both start at 1 cannot clobber each other's dial record.\n## propagate.rs\n 332│\n*333│/// Requester side: pull a newer release from the peer over `conn_id`, gate it\n 334│/// per-node, and stage it on success. `open_op` is the durable id for the\n 335│/// exactly-once stream open; everything after rides fire-and-forget. A torn\n 336│/// pull errors — recovery is a fresh `request_update` (the query is\n    │...\n 346│    brain: &mut Brain,\n*347│    conn_id: u64,\n 348│    open_op: MintedOp,\n 349│    running: &BrokerAbi,\n 350│    policy: &VerifyPolicy,\n    │...\n 353│) -> io::Result<UpdatePullOutcome> {\n*354│    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n 355│    let stream_id = opened.stream_id;\n 356│    brain.net_stream_subscribe(stream_id, 0)?;\n 357│\n    │...\n 554│// [impl->REQ-UPD-6]\n*555│pub fn request_update_status(brain: &mut Brain, conn_id: u64) -> io::Result<UpdateStatusReport> {\n*556│    let opened = brain.net_open_stream(conn_id, None)?;\n 557│    let stream_id = opened.stream_id;\n 558│    brain.net_stream_subscribe(stream_id, 0)?;\n 559│\n## resthost.rs\n 185│    brain: &mut Brain,\n*186│    conn_id: u64,\n 187│    endpoint: &str,\n 188│    event_tag: &str,\n 189│    op_id: &str,\n    │...\n 191│) -> io::Result<RestRequestOutcome> {\n*192│    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n 193│    let line = ndjson::encode_line(&RestRecord::Request {\n 194│        endpoint: endpoint.to_string(),\n 195│        event: event_tag.to_string(),\n## seedmap.rs\n 142│        thread::spawn(move || {\n*143│            let _ = handle_conn(conn, &reg, &stop, &name);\n 144│        });\n 145│    }\n 146│}\n## seedproofx.rs\n  24│//! Dialer opens it (`open_bi`); acceptor accepts it (`accept_bi`) *before*\n *25│//! `register_conn` wires the general app-stream acceptor, so it is never\n  26│//! confused with an app stream. Then a four-leg ping-pong:\n  27│//!\n  28│//! 1. dialer → `Hello { nonce_d, subnets = its local names }`\n    │...\n 457│/// (nothing full, nothing re-seeded) drops the conn (`None`). A re-seed-only\n*458│/// exchange registers with an EMPTY proven set: the conn is kept alive only so\n 459│/// the seed push it just carried is actually delivered (dropping it here races\n 460│/// the QUIC CLOSE against the peer's read) — it serves no subnet and is replaced\n 461│/// when the healed peer reconnects full. Otherwise the proven (full) set joins.\n## serveprobe.rs\n 113│// [impl->REQ-SUBNET-5]\n*114│pub fn request_subnet_probe(brain: &mut Brain, conn_id: u64, subnet: &str) -> io::Result<bool> {\n*115│    let opened = brain.net_open_stream(conn_id, None)?;\n 116│    let line = ndjson::encode_line(&ServeProbeRecord::Request {\n 117│        serve_probe: subnet.to_string(),\n 118│    });\n## shellwake.rs\n 388│                &mut brain,\n*389│                conn.conn_id,\n 390│                owner,\n 391│                spt_net::net::rest::REST_EVENT_WAKE,\n 392│                &op_id,\n## sync.rs\n 328│\n*329│/// Requester side: pull `refs` from the peer over `conn_id`, apply each\n 330│/// fetched tip with the vector driver, and report per-branch verdicts.\n 331│/// `open_op` is the durable id for the exactly-once stream open; everything\n 332│/// after rides fire-and-forget. A torn pull errors — recovery is a fresh\n    │...\n 336│    brain: &mut Brain,\n*337│    conn_id: u64,\n 338│    refs: &[String],\n 339│    open_op: MintedOp,\n 340│    cs: &ContextStore,\n    │...\n 342│) -> io::Result<SyncPullReport> {\n*343│    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n 344│    let stream_id = opened.stream_id;\n 345│    brain.net_stream_subscribe(stream_id, 0)?;\n 346│\n## tunnelhub.rs\n 279│        thread::spawn(move || {\n*280│            let _ = handle_conn(conn, &hub, &broker);\n 281│        });\n 282│    }\n 283│}\n\n## pump/\n### mod.rs\n  481│    /// The established peer conn id for this step.\n *482│    conn_id: u64,\n  483│    /// The pump's sole durable open-op counter. Workers mint open-ops only\n  484│    /// through [`PeerIo::open_op`] (the sync + update legs); a worker-owned\n  485│    /// `EpochSource` over this counter would be the KH epoch-lease double-mint.\n     │...\n  492│    fn push_feed(&mut self, feed: &[u8]) -> io::Result<()> {\n *493│        push_feed(self.brain, self.conn_id, feed)\n  494│    }\n  495│\n  496│    /// Mint the next durable open-op from the pump's sole counter (exactly-once\n     │...\n  620│        for (peer_hex, subs) in &peer_subnets {\n *621│            if let Some(&conn_id) = conns.get(peer_hex) {\n  622│                run_peer_subnets(\n  623│                    &mut brain, &mut ops, &mut workers, &due_flags, &mut conns, &mut sched,\n *624│                    conn_id, subs, peer_hex, &ctx, round_start,\n  625│                )?;\n  626│            } else if peer_eligible(&sched, peer_hex, round_start) {\n  627│                if let Some(addr) = resolve_submit_addr(&resolver, peer_hex, &paths.peer_addrs) {\n     │...\n  771│    sched: &mut HashMap<String, PeerSched>,\n *772│    conn_id: u64,\n  773│    subs: &[String],\n  774│    peer_hex: &str,\n  775│    ctx: &RoundCtx,\n     │...\n  781│                brain: &mut *brain,\n *782│                conn_id,\n  783│                ops: &mut *ops,\n  784│            };\n  785│            workers[i].peer_step(&mut io, sub_name, peer_hex, ctx)\n     │...\n  829│                sched,\n *830│                ev.conn_id,\n  831│                subs,\n  832│                &ev.remote_id_hex,\n  833│                ctx,\n     │...\n  858│        PRESENCE_CONNECTED => {\n *859│            conns.insert(ev.remote_id_hex.clone(), ev.conn_id);\n  860│            sched.remove(&ev.remote_id_hex); // live peer → reset backoff\n  861│            pending.remove(&ev.remote_id_hex);\n  862│            // REQ-CONV-1 write-back: seed the OBSERVED addr from the async connect.\n     │...\n  884│            // Drop the cached conn — redial-eligible immediately, NO backoff (Q3).\n *885│            conns.retain(|_, cid| *cid != ev.conn_id);\n  886│            false\n  887│        }\n  888│        _ => false, // a future presence kind an older pump tolerates\n     │...\n  960│\n *961│/// Open a fresh stream on `conn_id`, write one whole feed fire-and-forget,\n  962│/// and finish it (the receiver's dispatcher classifies + applies). One\n  963│/// stream per feed: streams are single-purpose by the demux contract.\n *964│fn push_feed(brain: &mut Brain, conn_id: u64, feed: &[u8]) -> io::Result<()> {\n *965│    let opened = brain.net_open_stream(conn_id, None)?;\n  966│    brain.net_stream_send(opened.stream_id, feed, None, true)?;\n  967│    Ok(())\n  968│}\n     │...\n 1496│\n*1497│        let ev = |kind: &str, conn_id: u64, hex: &str| NetPresenceEvent {\n 1498│            seq: 0,\n 1499│            kind: kind.to_string(),\n*1500│            conn_id,\n 1501│            remote_id_hex: hex.to_string(),\n 1502│            reason: None,\n 1503│            remote_addr: serde_json::Value::Null,\n     │...\n 1580│            kind: PRESENCE_CONNECTED.to_string(),\n*1581│            conn_id: 1,\n 1582│            remote_id_hex: \"aa\".into(),\n 1583│            reason: None,\n 1584│            remote_addr: serde_json::Value::Null,\n### sync.rs\n 106│            let open_op = io.open_op()?;\n*107│            let report = request_sync(&mut *io.brain, io.conn_id, &want, open_op, &cs, &self.scratch)?;\n 108│            (self.hooks.on_pull)(peer_hex, &report);\n 109│        }\n 110│        Ok(())\n### update.rs\n 114│            &mut *io.brain,\n*115│            io.conn_id,\n 116│            open_op,\n 117│            &BrokerAbi::current(),\n 118│            &policy,"},"isError":false,"timestamp":1784195886108}}
{"type":"message","id":"39155672","parentId":"e506c273","timestamp":"2026-07-16T09:58:06.117Z","message":{"role":"toolResult","toolCallId":"call_Z1VEJrIMqSajGDa5rhpK3rz7|fc_047bb49263dfa303016a58ab2d7c188198b59c8118a531cd6f","toolName":"read","content":[{"type":"text","text":"//! Bounded, cancelable, poison-on-failure framed writes for one physical\n//! broker connection (UPDATE-WEDGE round-4).\n//!\n//! ## The failure class this kills\n//!\n//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard\n//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes\n//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\n//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\n//! A connected consumer that stops reading (an `rc --take` whose process\n//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n//! logical stall-evict (`stall_evict_controller`) releases only the controller\n//! ROLE — it never cancels the in-flight pipe write or closes the physical\n//! connection, so the writer (and every write queued behind the same gate)\n//! stays wedged until the peer process dies (~128 s in the field capture, on a\n//! brain restart).\n//!\n//! ## The primitive\n//!\n//! [`BrokerConn`] owns the send half outright — it never leaves this module,\n//! so an unbounded bypass write cannot compile. Every write:\n//!\n//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE\n//!    deadline stamped at write entry covers BOTH the gate-wait and the OS\n//!    write);\n//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute\n//!    deadline, then performs the blocking [`write_frame`] OUT of every lock;\n//! 3. on success, returns the half to the gate.\n//!\n//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\n//! write returning, nor on opportunistic stall-evict sampling: it poisons the\n//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\n//! completion APC makes the wedged `SleepEx` loop return with\n//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\n//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\n//! the same abort (the stuck op it waited behind is the wedge).\n//!\n//! ## Poison / retire invariant (both OSes)\n//!\n//! On deadline, write failure, or cancellation: (1) the WHOLE physical\n//! connection is poisoned, idempotently — a length-prefixed frame may have\n//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\n//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\n//! (3) the buffer of the canceled op is released only after the op reports\n//! completion (`write_frame` returns only after the completion APC — the\n//! synchronous return IS the completion handshake); (4) the retired half is\n//! parked in a graveyard slot — never dropped while the watchdog might still\n//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\n//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\n//! pipe is then severed (`DisconnectNamedPipe` — the server end forces the\n//! client off, so the old client's next access fails, a re-issued server read\n//! fails, and the `interprocess` drop-limbo flush cannot block on unread\n//! data), followed by one more `CancelIoEx` covering a read re-issued in the\n//! cancel→disconnect window.\n//!\n//! No new output queue (the bounded per-sink queues + isolated writer threads\n//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\n//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\n//! healthy connections; only the poison/retire path is new.\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n\nuse std::io;\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\nuse std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};\nuse std::thread::{self, JoinHandle};\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n\nuse interprocess::local_socket::SendHalf;\n\nuse crate::codec::write_frame;\nuse crate::frame::Envelope;\n\n/// Stable per-physical-conn identity mint — a monotonic u64 stamped at\n/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\nstatic CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n\n/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n/// startup ([`init_log_anchor`], both broker and brain entries) so every\n/// record in one process shares one base; lazily initialized on first stamp\n/// otherwise (tests, in-process brokers) — correlation needs a COMMON base,\n/// not an absolute one.\nstatic MONO_ANCHOR: OnceLock<Instant> = OnceLock::new();\n\n/// Anchor the monotonic half of [`log_stamp`] at process start — called first\n/// thing by both daemon entries, next to `scrub_identity_env`.\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\npub(crate) fn init_log_anchor() {\n    let _ = MONO_ANCHOR.set(Instant::now());\n}\n\n/// Wall-clock + monotonic stamp for daemon-stderr correlation records\n/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\n/// stderr has no per-line timestamps and broker+brain share one file — without\n/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\n/// to field incident times (wall).\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\npub(crate) fn log_stamp() -> String {\n    let wall_ms = SystemTime::now()\n        .duration_since(UNIX_EPOCH)\n        .map(|d| d.as_millis())\n        .unwrap_or(0);\n    let mono_ms = MONO_ANCHOR.get_or_init(Instant::now).elapsed().as_millis();\n    format!(\"wall_ms={wall_ms} mono_ms={mono_ms}\")\n}\n\n/// Attribution-label cap: facts accrete per role attach (a long-lived brain\n/// conn can be controller + viewer + presence subscriber over its life), and\n/// the label must stay BOUNDED — one truncation marker, then no further growth.\nconst LABEL_CAP: usize = 240;\n\n/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\n#[derive(Debug, PartialEq, Eq)]\npub(crate) enum ConnWrite {\n    /// The frame was fully written.\n    Done,\n    /// The pre-write gate closure declined (e.g. a superseded controller\n    /// writer's epoch check) — nothing was written; the conn stays healthy.\n    Superseded,\n}\n\n/// The serialized write gate. `half: Some` = free; `None` = a write is in\n/// flight (checked out) or the conn has retired (then `poisoned` is set).\nstruct Gate {\n    half: Option<SendHalf>,\n}\n\n/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\nstruct Dog {\n    /// `Some((op_id, deadline))` while a write is in flight.\n    inflight: Option<(u64, Instant)>,\n    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n    shutdown: bool,\n}\n\n/// The state shared with the watchdog thread (no `Arc<BrokerConn>` cycle —\n/// the watchdog must not keep the conn alive).\nstruct Inner {\n    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n    id: u64,\n    /// Accreted attribution facts (subscriber role, endpoint/session where\n    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n    label: Mutex<String>,\n    /// Latch: the once-per-conn `write-start` lifecycle record fired.\n    first_write_logged: AtomicBool,\n    gate: Mutex<Gate>,\n    gate_cv: Condvar,\n    dog: Mutex<Dog>,\n    dog_cv: Condvar,\n    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\n    /// conn refuses every future write and is never re-gated.\n    poisoned: AtomicBool,\n    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\n    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\n    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\n    /// count the wedge (the W2 stall-evict tally) without conflating it with\n    /// an ordinary disconnect.\n    timed_out: AtomicBool,\n    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\n    severed: AtomicBool,\n    /// A retired (poisoned) half parks here so its OS handle stays alive\n    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\n    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\n    graveyard: Mutex<Option<SendHalf>>,\n    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\n    op_seq: AtomicU64,\n    /// The absolute per-write bound (gate-wait + OS write completion).\n    bound: Duration,\n    /// The raw OS handle/fd of the underlying stream, captured at\n    /// construction for the out-of-band abort. The recv and send halves share\n    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\n    /// aborting this handle covers a parked read too. Valid for the whole\n    /// conn lifetime: the half lives in `gate`, a writer's stack, or\n    /// `graveyard` until drop.\n    #[cfg(windows)]\n    raw: isize,\n    #[cfg(unix)]\n    raw: i32,\n}\n\n/// Recover a mutex guard even if a holder panicked — the state these guards\n/// protect is a plain slot/flag pair, valid on every panic boundary.\nfn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\n    match m.lock() {\n        Ok(g) => g,\n        Err(p) => p.into_inner(),\n    }\n}\n\n#[cfg(windows)]\nfn raw_of(half: &SendHalf) -> isize {\n    use std::os::windows::io::{AsHandle, AsRawHandle};\n    match half {\n        SendHalf::NamedPipe(h) => h.as_handle().as_raw_handle() as isize,\n    }\n}\n\n#[cfg(unix)]\nfn raw_of(half: &SendHalf) -> i32 {\n    use std::os::fd::{AsFd, AsRawFd};\n    match half {\n        SendHalf::UdSocket(h) => h.as_fd().as_raw_fd(),\n    }\n}\n\n#[cfg(windows)]\nmod win {\n    // kernel32 is in the MSVC default link set — raw externs, no new dep\n    // (the deelevate.rs / daemon.rs posture).\n    extern \"system\" {\n        pub fn CancelIoEx(handle: isize, overlapped: *mut core::ffi::c_void) -> i32;\n        pub fn DisconnectNamedPipe(handle: isize) -> i32;\n    }\n}\n\nimpl Inner {\n    /// The attribution snapshot every lifecycle record carries: stable conn id,\n    /// accreted role/endpoint/session facts, wall + monotonic stamps.\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    fn attribution(&self) -> String {\n        let label = recover(&self.label);\n        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\n        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\n    }\n\n    /// Render the once-per-conn retirement record — the F-039 token split\n    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n    /// already-dead counterpart, the offline-peer dial churn) emits the\n    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n    /// SAME attribution fields — quieter class, never thinner (the retired\n    /// class is what discriminates fresh-carrier churn from interleave\n    /// artifact in the field). Behavior is identical either way: poison,\n    /// abort, retire, never reuse.\n    // [impl->REQ-CONN-POISON-DIAL-SCOPE]\n    fn render_retirement(&self, timed_out: bool, cause: Option<&io::Error>) -> String {\n        if timed_out {\n            format!(\n                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\n                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\n                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\n                self.attribution(),\n                self.bound.as_millis()\n            )\n        } else {\n            let cause = match cause {\n                Some(e) => format!(\"{:?}: {e}\", e.kind()),\n                None => \"write failed\".to_string(),\n            };\n            format!(\n                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\n                 broker conn poisoned + retired, never reused (same retire behavior \\\n                 as POISONED — only the failure class differs) \\\n                 [REQ-CONN-POISON-DIAL-SCOPE]\",\n                self.attribution()\n            )\n        }\n    }\n\n    /// One BOUNDED per-conn lifecycle record (leg d, doyle-confirmed\n    /// UNCONDITIONAL): `write-start` (first write only), `transport-close`\n    /// (drop), `writer-exit` / `*-attach` / `*-replaced` (emitted by the\n    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\n    /// timeout/cancel leg is the retirement record itself.\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    fn lifecycle(&self, event: &str, extra: &str) {\n        let sep = if extra.is_empty() { \"\" } else { \" \" };\n        eprintln!(\n            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\n            self.attribution()\n        );\n    }\n\n    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\n    /// the halves share one OS object) and latch the poison. Idempotent and\n    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\n    /// no-op, and re-cancel closes the race where a write was issued a beat\n    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\n    /// `cause` (the organic caller's write error) rides the retirement record\n    /// when the retirement is NOT deadline-driven.\n    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\n        if !self.poisoned.swap(true, Ordering::AcqRel) {\n            // First poison of this conn: loud, once. The token is class-split\n            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n…\n        }\n…\n    }\n…\n}\n\n[Showing lines 1-288 of 815. Use :289 to continue]"}],"details":{"truncation":{"content":"//! Bounded, cancelable, poison-on-failure framed writes for one physical\r\n//! broker connection (UPDATE-WEDGE round-4).\r\n//!\r\n//! ## The failure class this kills\r\n//!\r\n//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard\r\n//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes\r\n//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\r\n//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\r\n//! A connected consumer that stops reading (an `rc --take` whose process\r\n//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\r\n//! logical stall-evict (`stall_evict_controller`) releases only the controller\r\n//! ROLE — it never cancels the in-flight pipe write or closes the physical\r\n//! connection, so the writer (and every write queued behind the same gate)\r\n//! stays wedged until the peer process dies (~128 s in the field capture, on a\r\n//! brain restart).\r\n//!\r\n//! ## The primitive\r\n//!\r\n//! [`BrokerConn`] owns the send half outright — it never leaves this module,\r\n//! so an unbounded bypass write cannot compile. Every write:\r\n//!\r\n//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE\r\n//!    deadline stamped at write entry covers BOTH the gate-wait and the OS\r\n//!    write);\r\n//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute\r\n//!    deadline, then performs the blocking [`write_frame`] OUT of every lock;\r\n//! 3. on success, returns the half to the gate.\r\n//!\r\n//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\r\n//! write returning, nor on opportunistic stall-evict sampling: it poisons the\r\n//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\r\n//! completion APC makes the wedged `SleepEx` loop return with\r\n//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\r\n//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\r\n//! the same abort (the stuck op it waited behind is the wedge).\r\n//!\r\n//! ## Poison / retire invariant (both OSes)\r\n//!\r\n//! On deadline, write failure, or cancellation: (1) the WHOLE physical\r\n//! connection is poisoned, idempotently — a length-prefixed frame may have\r\n//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\r\n//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\r\n//! (3) the buffer of the canceled op is released only after the op reports\r\n//! completion (`write_frame` returns only after the completion APC — the\r\n//! synchronous return IS the completion handshake); (4) the retired half is\r\n//! parked in a graveyard slot — never dropped while the watchdog might still\r\n//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\r\n//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\r\n//! pipe is then severed (`DisconnectNamedPipe` — the server end forces the\r\n//! client off, so the old client's next access fails, a re-issued server read\r\n//! fails, and the `interprocess` drop-limbo flush cannot block on unread\r\n//! data), followed by one more `CancelIoEx` covering a read re-issued in the\r\n//! cancel→disconnect window.\r\n//!\r\n//! No new output queue (the bounded per-sink queues + isolated writer threads\r\n//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\r\n//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\r\n//! healthy connections; only the poison/retire path is new.\r\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n\r\nuse std::io;\r\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\r\nuse std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};\r\nuse std::thread::{self, JoinHandle};\r\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\r\n\r\nuse interprocess::local_socket::SendHalf;\r\n\r\nuse crate::codec::write_frame;\r\nuse crate::frame::Envelope;\r\n\r\n/// Stable per-physical-conn identity mint — a monotonic u64 stamped at\r\n/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\r\n/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\r\n/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\r\n/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\r\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\r\nstatic CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\r\n\r\n/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\r\n/// startup ([`init_log_anchor`], both broker and brain entries) so every\r\n/// record in one process shares one base; lazily initialized on first stamp\r\n/// otherwise (tests, in-process brokers) — correlation needs a COMMON base,\r\n/// not an absolute one.\r\nstatic MONO_ANCHOR: OnceLock<Instant> = OnceLock::new();\r\n\r\n/// Anchor the monotonic half of [`log_stamp`] at process start — called first\r\n/// thing by both daemon entries, next to `scrub_identity_env`.\r\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\r\npub(crate) fn init_log_anchor() {\r\n    let _ = MONO_ANCHOR.set(Instant::now());\r\n}\r\n\r\n/// Wall-clock + monotonic stamp for daemon-stderr correlation records\r\n/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\r\n/// stderr has no per-line timestamps and broker+brain share one file — without\r\n/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\r\n/// to field incident times (wall).\r\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\r\npub(crate) fn log_stamp() -> String {\r\n    let wall_ms = SystemTime::now()\r\n        .duration_since(UNIX_EPOCH)\r\n        .map(|d| d.as_millis())\r\n        .unwrap_or(0);\r\n    let mono_ms = MONO_ANCHOR.get_or_init(Instant::now).elapsed().as_millis();\r\n    format!(\"wall_ms={wall_ms} mono_ms={mono_ms}\")\r\n}\r\n\r\n/// Attribution-label cap: facts accrete per role attach (a long-lived brain\r\n/// conn can be controller + viewer + presence subscriber over its life), and\r\n/// the label must stay BOUNDED — one truncation marker, then no further growth.\r\nconst LABEL_CAP: usize = 240;\r\n\r\n/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\r\n#[derive(Debug, PartialEq, Eq)]\r\npub(crate) enum ConnWrite {\r\n    /// The frame was fully written.\r\n    Done,\r\n    /// The pre-write gate closure declined (e.g. a superseded controller\r\n    /// writer's epoch check) — nothing was written; the conn stays healthy.\r\n    Superseded,\r\n}\r\n\r\n/// The serialized write gate. `half: Some` = free; `None` = a write is in\r\n/// flight (checked out) or the conn has retired (then `poisoned` is set).\r\nstruct Gate {\r\n    half: Option<SendHalf>,\r\n}\r\n\r\n/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\r\nstruct Dog {\r\n    /// `Some((op_id, deadline))` while a write is in flight.\r\n    inflight: Option<(u64, Instant)>,\r\n    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\r\n    shutdown: bool,\r\n}\r\n\r\n/// The state shared with the watchdog thread (no `Arc<BrokerConn>` cycle —\r\n/// the watchdog must not keep the conn alive).\r\nstruct Inner {\r\n    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\r\n    id: u64,\r\n    /// Accreted attribution facts (subscriber role, endpoint/session where\r\n    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\r\n    label: Mutex<String>,\r\n    /// Latch: the once-per-conn `write-start` lifecycle record fired.\r\n    first_write_logged: AtomicBool,\r\n    gate: Mutex<Gate>,\r\n    gate_cv: Condvar,\r\n    dog: Mutex<Dog>,\r\n    dog_cv: Condvar,\r\n    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\r\n    /// conn refuses every future write and is never re-gated.\r\n    poisoned: AtomicBool,\r\n    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\r\n    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\r\n    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\r\n    /// count the wedge (the W2 stall-evict tally) without conflating it with\r\n    /// an ordinary disconnect.\r\n    timed_out: AtomicBool,\r\n    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\r\n    severed: AtomicBool,\r\n    /// A retired (poisoned) half parks here so its OS handle stays alive\r\n    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\r\n    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\r\n    graveyard: Mutex<Option<SendHalf>>,\r\n    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\r\n    op_seq: AtomicU64,\r\n    /// The absolute per-write bound (gate-wait + OS write completion).\r\n    bound: Duration,\r\n    /// The raw OS handle/fd of the underlying stream, captured at\r\n    /// construction for the out-of-band abort. The recv and send halves share\r\n    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\r\n    /// aborting this handle covers a parked read too. Valid for the whole\r\n    /// conn lifetime: the half lives in `gate`, a writer's stack, or\r\n    /// `graveyard` until drop.\r\n    #[cfg(windows)]\r\n    raw: isize,\r\n    #[cfg(unix)]\r\n    raw: i32,\r\n}\r\n\r\n/// Recover a mutex guard even if a holder panicked — the state these guards\r\n/// protect is a plain slot/flag pair, valid on every panic boundary.\r\nfn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\r\n    match m.lock() {\r\n        Ok(g) => g,\r\n        Err(p) => p.into_inner(),\r\n    }\r\n}\r\n\r\n#[cfg(windows)]\r\nfn raw_of(half: &SendHalf) -> isize {\r\n    use std::os::windows::io::{AsHandle, AsRawHandle};\r\n    match half {\r\n        SendHalf::NamedPipe(h) => h.as_handle().as_raw_handle() as isize,\r\n    }\r\n}\r\n\r\n#[cfg(unix)]\r\nfn raw_of(half: &SendHalf) -> i32 {\r\n    use std::os::fd::{AsFd, AsRawFd};\r\n    match half {\r\n        SendHalf::UdSocket(h) => h.as_fd().as_raw_fd(),\r\n    }\r\n}\r\n\r\n#[cfg(windows)]\r\nmod win {\r\n    // kernel32 is in the MSVC default link set — raw externs, no new dep\r\n    // (the deelevate.rs / daemon.rs posture).\r\n    extern \"system\" {\r\n        pub fn CancelIoEx(handle: isize, overlapped: *mut core::ffi::c_void) -> i32;\r\n        pub fn DisconnectNamedPipe(handle: isize) -> i32;\r\n    }\r\n}\r\n\r\nimpl Inner {\r\n    /// The attribution snapshot every lifecycle record carries: stable conn id,\r\n    /// accreted role/endpoint/session facts, wall + monotonic stamps.\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    fn attribution(&self) -> String {\r\n        let label = recover(&self.label);\r\n        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\r\n        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\r\n    }\r\n\r\n    /// Render the once-per-conn retirement record — the F-039 token split\r\n    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\r\n    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\r\n    /// watches); an ORGANIC write failure (peer death / broken pipe on an\r\n    /// already-dead counterpart, the offline-peer dial churn) emits the\r\n    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\r\n    /// SAME attribution fields — quieter class, never thinner (the retired\r\n    /// class is what discriminates fresh-carrier churn from interleave\r\n    /// artifact in the field). Behavior is identical either way: poison,\r\n    /// abort, retire, never reuse.\r\n    // [impl->REQ-CONN-POISON-DIAL-SCOPE]\r\n    fn render_retirement(&self, timed_out: bool, cause: Option<&io::Error>) -> String {\r\n        if timed_out {\r\n            format!(\r\n                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\r\n                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\r\n                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\r\n                self.attribution(),\r\n                self.bound.as_millis()\r\n            )\r\n        } else {\r\n            let cause = match cause {\r\n                Some(e) => format!(\"{:?}: {e}\", e.kind()),\r\n                None => \"write failed\".to_string(),\r\n            };\r\n            format!(\r\n                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\r\n                 broker conn poisoned + retired, never reused (same retire behavior \\\r\n                 as POISONED — only the failure class differs) \\\r\n                 [REQ-CONN-POISON-DIAL-SCOPE]\",\r\n                self.attribution()\r\n            )\r\n        }\r\n    }\r\n\r\n    /// One BOUNDED per-conn lifecycle record (leg d, doyle-confirmed\r\n    /// UNCONDITIONAL): `write-start` (first write only), `transport-close`\r\n    /// (drop), `writer-exit` / `*-attach` / `*-replaced` (emitted by the\r\n    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\r\n    /// timeout/cancel leg is the retirement record itself.\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    fn lifecycle(&self, event: &str, extra: &str) {\r\n        let sep = if extra.is_empty() { \"\" } else { \" \" };\r\n        eprintln!(\r\n            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\r\n            self.attribution()\r\n        );\r\n    }\r\n\r\n    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\r\n    /// the halves share one OS object) and latch the poison. Idempotent and\r\n    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\r\n    /// no-op, and re-cancel closes the race where a write was issued a beat\r\n    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\r\n    /// `cause` (the organic caller's write error) rides the retirement record\r\n    /// when the retirement is NOT deadline-driven.\r\n    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\r\n        if !self.poisoned.swap(true, Ordering::AcqRel) {\r\n            // First poison of this conn: loud, once. The token is class-split\r\n            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\r","truncated":true,"truncatedBy":"lines","totalLines":815,"totalBytes":13922,"outputLines":288,"outputBytes":13922,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Bounded, cancelable, poison-on-failure framed writes for one physical\r\n//! broker connection (UPDATE-WEDGE round-4).\r\n//!\r\n//! ## The failure class this kills\r\n//!\r\n//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard\r\n//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes\r\n//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\r\n//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\r\n//! A connected consumer that stops reading (an `rc --take` whose process\r\n//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\r\n//! logical stall-evict (`stall_evict_controller`) releases only the controller\r\n//! ROLE — it never cancels the in-flight pipe write or closes the physical\r\n//! connection, so the writer (and every write queued behind the same gate)\r\n//! stays wedged until the peer process dies (~128 s in the field capture, on a\r\n//! brain restart).\r\n//!\r\n//! ## The primitive\r\n//!\r\n//! [`BrokerConn`] owns the send half outright — it never leaves this module,\r\n//! so an unbounded bypass write cannot compile. Every write:\r\n//!\r\n//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE\r\n//!    deadline stamped at write entry covers BOTH the gate-wait and the OS\r\n//!    write);\r\n//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute\r\n//!    deadline, then performs the blocking [`write_frame`] OUT of every lock;\r\n//! 3. on success, returns the half to the gate.\r\n//!\r\n//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\r\n//! write returning, nor on opportunistic stall-evict sampling: it poisons the\r\n//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\r\n//! completion APC makes the wedged `SleepEx` loop return with\r\n//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\r\n//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\r\n//! the same abort (the stuck op it waited behind is the wedge).\r\n//!\r\n//! ## Poison / retire invariant (both OSes)\r\n//!\r\n//! On deadline, write failure, or cancellation: (1) the WHOLE physical\r\n//! connection is poisoned, idempotently — a length-prefixed frame may have\r\n//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\r\n//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\r\n//! (3) the buffer of the canceled op is released only after the op reports\r\n//! completion (`write_frame` returns only after the completion APC — the\r\n//! synchronous return IS the completion handshake); (4) the retired half is\r\n//! parked in a graveyard slot — never dropped while the watchdog might still\r\n//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\r\n//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\r\n//! pipe is then severed (`DisconnectNamedPipe` — the server end forces the\r\n//! client off, so the old client's next access fails, a re-issued server read\r\n//! fails, and the `interprocess` drop-limbo flush cannot block on unread\r\n//! data), followed by one more `CancelIoEx` covering a read re-issued in the\r\n//! cancel→disconnect window.\r\n//!\r\n//! No new output queue (the bounded per-sink queues + isolated writer threads\r\n//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\r\n//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\r\n//! healthy connections; only the poison/retire path is new.\r\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n\r\nuse std::io;\r\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\r\nuse std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};\r\nuse std::thread::{self, JoinHandle};\r\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\r\n\r\nuse interprocess::local_socket::SendHalf;\r\n\r\nuse crate::codec::write_frame;\r\nuse crate::frame::Envelope;\r\n\r\n/// Stable per-physical-conn identity mint — a monotonic u64 stamped at\r\n/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\r\n/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\r\n/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\r\n/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\r\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\r\nstatic CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\r\n\r\n/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\r\n/// startup ([`init_log_anchor`], both broker and brain entries) so every\r\n/// record in one process shares one base; lazily initialized on first stamp\r\n/// otherwise (tests, in-process brokers) — correlation needs a COMMON base,\r\n/// not an absolute one.\r\nstatic MONO_ANCHOR: OnceLock<Instant> = OnceLock::new();\r\n\r\n/// Anchor the monotonic half of [`log_stamp`] at process start — called first\r\n/// thing by both daemon entries, next to `scrub_identity_env`.\r\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\r\npub(crate) fn init_log_anchor() {\r\n    let _ = MONO_ANCHOR.set(Instant::now());\r\n}\r\n\r\n/// Wall-clock + monotonic stamp for daemon-stderr correlation records\r\n/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\r\n/// stderr has no per-line timestamps and broker+brain share one file — without\r\n/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\r\n/// to field incident times (wall).\r\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\r\npub(crate) fn log_stamp() -> String {\r\n    let wall_ms = SystemTime::now()\r\n        .duration_since(UNIX_EPOCH)\r\n        .map(|d| d.as_millis())\r\n        .unwrap_or(0);\r\n    let mono_ms = MONO_ANCHOR.get_or_init(Instant::now).elapsed().as_millis();\r\n    format!(\"wall_ms={wall_ms} mono_ms={mono_ms}\")\r\n}\r\n\r\n/// Attribution-label cap: facts accrete per role attach (a long-lived brain\r\n/// conn can be controller + viewer + presence subscriber over its life), and\r\n/// the label must stay BOUNDED — one truncation marker, then no further growth.\r\nconst LABEL_CAP: usize = 240;\r\n\r\n/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\r\n#[derive(Debug, PartialEq, Eq)]\r\npub(crate) enum ConnWrite {\r\n    /// The frame was fully written.\r\n    Done,\r\n    /// The pre-write gate closure declined (e.g. a superseded controller\r\n    /// writer's epoch check) — nothing was written; the conn stays healthy.\r\n    Superseded,\r\n}\r\n\r\n/// The serialized write gate. `half: Some` = free; `None` = a write is in\r\n/// flight (checked out) or the conn has retired (then `poisoned` is set).\r\nstruct Gate {\r\n    half: Option<SendHalf>,\r\n}\r\n\r\n/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\r\nstruct Dog {\r\n    /// `Some((op_id, deadline))` while a write is in flight.\r\n    inflight: Option<(u64, Instant)>,\r\n    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\r\n    shutdown: bool,\r\n}\r\n\r\n/// The state shared with the watchdog thread (no `Arc<BrokerConn>` cycle —\r\n/// the watchdog must not keep the conn alive).\r\nstruct Inner {\r\n    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\r\n    id: u64,\r\n    /// Accreted attribution facts (subscriber role, endpoint/session where\r\n    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\r\n    label: Mutex<String>,\r\n    /// Latch: the once-per-conn `write-start` lifecycle record fired.\r\n    first_write_logged: AtomicBool,\r\n    gate: Mutex<Gate>,\r\n    gate_cv: Condvar,\r\n    dog: Mutex<Dog>,\r\n    dog_cv: Condvar,\r\n    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\r\n    /// conn refuses every future write and is never re-gated.\r\n    poisoned: AtomicBool,\r\n    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\r\n    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\r\n    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\r\n    /// count the wedge (the W2 stall-evict tally) without conflating it with\r\n    /// an ordinary disconnect.\r\n    timed_out: AtomicBool,\r\n    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\r\n    severed: AtomicBool,\r\n    /// A retired (poisoned) half parks here so its OS handle stays alive\r\n    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\r\n    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\r\n    graveyard: Mutex<Option<SendHalf>>,\r\n    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\r\n    op_seq: AtomicU64,\r\n    /// The absolute per-write bound (gate-wait + OS write completion).\r\n    bound: Duration,\r\n    /// The raw OS handle/fd of the underlying stream, captured at\r\n    /// construction for the out-of-band abort. The recv and send halves share\r\n    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\r\n    /// aborting this handle covers a parked read too. Valid for the whole\r\n    /// conn lifetime: the half lives in `gate`, a writer's stack, or\r\n    /// `graveyard` until drop.\r\n    #[cfg(windows)]\r\n    raw: isize,\r\n    #[cfg(unix)]\r\n    raw: i32,\r\n}\r\n\r\n/// Recover a mutex guard even if a holder panicked — the state these guards\r\n/// protect is a plain slot/flag pair, valid on every panic boundary.\r\nfn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\r\n    match m.lock() {\r\n        Ok(g) => g,\r\n        Err(p) => p.into_inner(),\r\n    }\r\n}\r\n\r\n#[cfg(windows)]\r\nfn raw_of(half: &SendHalf) -> isize {\r\n    use std::os::windows::io::{AsHandle, AsRawHandle};\r\n    match half {\r\n        SendHalf::NamedPipe(h) => h.as_handle().as_raw_handle() as isize,\r\n    }\r\n}\r\n\r\n#[cfg(unix)]\r\nfn raw_of(half: &SendHalf) -> i32 {\r\n    use std::os::fd::{AsFd, AsRawFd};\r\n    match half {\r\n        SendHalf::UdSocket(h) => h.as_fd().as_raw_fd(),\r\n    }\r\n}\r\n\r\n#[cfg(windows)]\r\nmod win {\r\n    // kernel32 is in the MSVC default link set — raw externs, no new dep\r\n    // (the deelevate.rs / daemon.rs posture).\r\n    extern \"system\" {\r\n        pub fn CancelIoEx(handle: isize, overlapped: *mut core::ffi::c_void) -> i32;\r\n        pub fn DisconnectNamedPipe(handle: isize) -> i32;\r\n    }\r\n}\r\n\r\nimpl Inner {\r\n    /// The attribution snapshot every lifecycle record carries: stable conn id,\r\n    /// accreted role/endpoint/session facts, wall + monotonic stamps.\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    fn attribution(&self) -> String {\r\n        let label = recover(&self.label);\r\n        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\r\n        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\r\n    }\r\n\r\n    /// Render the once-per-conn retirement record — the F-039 token split\r\n    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\r\n    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\r\n    /// watches); an ORGANIC write failure (peer death / broken pipe on an\r\n    /// already-dead counterpart, the offline-peer dial churn) emits the\r\n    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\r\n    /// SAME attribution fields — quieter class, never thinner (the retired\r\n    /// class is what discriminates fresh-carrier churn from interleave\r\n    /// artifact in the field). Behavior is identical either way: poison,\r\n    /// abort, retire, never reuse.\r\n    // [impl->REQ-CONN-POISON-DIAL-SCOPE]\r\n    fn render_retirement(&self, timed_out: bool, cause: Option<&io::Error>) -> String {\r\n        if timed_out {\r\n            format!(\r\n                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\r\n                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\r\n                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\r\n                self.attribution(),\r\n                self.bound.as_millis()\r\n            )\r\n        } else {\r\n            let cause = match cause {\r\n                Some(e) => format!(\"{:?}: {e}\", e.kind()),\r\n                None => \"write failed\".to_string(),\r\n            };\r\n            format!(\r\n                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\r\n                 broker conn poisoned + retired, never reused (same retire behavior \\\r\n                 as POISONED — only the failure class differs) \\\r\n                 [REQ-CONN-POISON-DIAL-SCOPE]\",\r\n                self.attribution()\r\n            )\r\n        }\r\n    }\r\n\r\n    /// One BOUNDED per-conn lifecycle record (leg d, doyle-confirmed\r\n    /// UNCONDITIONAL): `write-start` (first write only), `transport-close`\r\n    /// (drop), `writer-exit` / `*-attach` / `*-replaced` (emitted by the\r\n    /// broker's sink machinery through [`BrokerConn::lifecycle_event`]). The\r\n    /// timeout/cancel leg is the retirement record itself.\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    fn lifecycle(&self, event: &str, extra: &str) {\r\n        let sep = if extra.is_empty() { \"\" } else { \" \" };\r\n        eprintln!(\r\n            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\r\n            self.attribution()\r\n        );\r\n    }\r\n\r\n    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\r\n    /// the halves share one OS object) and latch the poison. Idempotent and\r\n    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\r\n    /// no-op, and re-cancel closes the race where a write was issued a beat\r\n    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\r\n    /// `cause` (the organic caller's write error) rides the retirement record\r\n    /// when the retirement is NOT deadline-driven.\r\n    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\r\n        if !self.poisoned.swap(true, Ordering::AcqRel) {\r\n            // First poison of this conn: loud, once. The token is class-split\r\n            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\r\n…\n        }\n…\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,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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,null,294,null,312,null,410]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\conn.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":815,"totalBytes":13922,"outputLines":288,"outputBytes":13922,"shownRange":{"start":1,"end":288},"nextOffset":289}}},"isError":false,"timestamp":1784195886117}}
{"type":"message","id":"5be79095","parentId":"39155672","timestamp":"2026-07-16T09:58:06.136Z","message":{"role":"toolResult","toolCallId":"call_nfM7pZBFKYAnSxeGPUJpLxN1|fc_047bb49263dfa303016a58ab2d7c308198aca7f642dabe556b","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/\n\n### tests/\n#### attach.rs\n 772|// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain\n*773|// crash mid-attach: the successor re-serves the SAME broker-held stream from\n 774|// seq 0 (worst-case stale cursor) — the replayed Request/input/output all\n 775|// dedup through the journals, input B sent INTO the dead window arrives, and\n 776|// B's viewport holds every marker exactly once.\n...\n 854|\n*855|    // Life 2: handoff, then re-serve the SAME stream from seq 0 — the\n 856|    // worst-case replay. serve_attach re-handles the replayed Request (gate +\n 857|    // re-attach from 0, so every output record re-transmits — B's render\n 858|    // cursor drops the duplicates), replays MARKER_ONE's input (PTY-write\n...\n 1126|\n*1127|    // The \"successor\" re-serves the SAME operator's stream (same by) on a new\n 1128|    // connection — the brain-restart re-take.\n 1129|    let mut successor = connect_retry(&name);\n 1130|    successor.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"successor control\");\n#### dispatch.rs\n 108|    thread::spawn(move || {\n*109|        let _ = run_dispatch_loop(&name, &paths, registry, &flag, Duration::from_millis(25));\n 110|    });\n 111|    stop\n 112|}\n...\n 343|// and the direction fact is recorded right: A's table shows its opened\n*344|// stream as locally-initiated; B's shows the same stream as peer-initiated\n*345|// (what the dispatcher claimed it by).\n 346|#[test]\n 347|fn dispatcher_funnels_wan_messages_undriven() {\n 348|    init_home();\n...\n 382|\n*383|    // Direction fact, both sides of the same stream.\n 384|    let a_rows = a.net_streams().expect(\"A streams\").streams;\n 385|    assert!(\n 386|        a_rows\n#### mesh.rs\n 250|            thread::spawn(move || {\n*251|                let _ = run_dispatch_loop(&name, &disp, reg, &flag, Duration::from_millis(25));\n 252|            });\n 253|        }\n 254|        stop\n#### pump.rs\n 239|        thread::spawn(move || {\n*240|            let _ = run_dispatch_loop(&name, &paths, reg, &flag, Duration::from_millis(25));\n 241|        });\n 242|    }\n 243|\n#### rosterprop.rs\n 147|\n*148|    // A dials B: the proof verifies, then the roster leg runs on the same stream.\n 149|    // dial() blocks through the dialer leg, so A's sink has merged B's slice by\n 150|    // the time it returns.\n 151|    let (_conn_id, remote) = a.dial(b.addr()).expect(\"dial member\");\n#### twohost.rs\n 342|        thread::spawn(move || {\n*343|            let _ = run_dispatch_loop(&name, &paths, reg, &flag, Duration::from_millis(50));\n 344|        });\n 345|    }\n 346|    {\n#### wake_single_flight.rs\n 17|//! claim (that would add another info.json writer, the very write-stomp this kills). An\n*18|//! endpoint id is claimed for the duration of one spawn; a duplicate wake for a claimed-\n 19|//! or-already-live endpoint stands down and no-op acks to the existing session.\n 20|//!\n 21|//! ## What this rig asserts\n#### wanmsg.rs\n 155|// [unit->REQ-MSG-5] WAN fail-closed at the FUNNEL (doyle G2 condition, KH 7.5):\n*156|// a claimed `user-msg` arriving through receive_wan from an origin that is NOT\n 157|// provably user-backed is re-stamped to plain `msg` BEFORE it reaches the\n 158|// target spool — the security-critical row of the identity-gate truth table on\n 159|// the cross-node surface. A fail-OPEN here would grant the user's authority to\n#### xfer.rs\n 14|//!   dies mid-push with a partial temp file on disk; the successor re-serves\n*15|//!   the SAME stream from seq 0 — the replayed chunks rewrite in place\n 16|//!   (positional idempotency), the commit renames atomically, and the file is\n 17|//!   byte-identical with no stray temp left behind.\n 18|\n...\n 323|    // Life 2: a successor brain connects fresh, then the REAL serve loop\n*324|    // re-serves the SAME broker-held stream from seq 0 — the worst-case full\n 325|    // replay over the partial temp.\n 326|    let mut life2 = connect_retry(&name_a);\n 327|    let outcome =\n\n### src/\n#### broker.rs\n 204|/// exit (success, an early `?` spawn/drain error, or a panic), so a failed spawn never\n*205|/// strands the endpoint permanently claimed.\n 206|struct WakeClaimGuard<'a> {\n 207|    set: &'a Mutex<HashSet<String>>,\n 208|    endpoint: String,\n...\n 231|// [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\n*232|fn wake_gate_decision(live: bool, claimed: bool, past_deadline: bool) -> WakeGate {\n 233|    if live {\n 234|        WakeGate::AlreadyLive\n*235|    } else if claimed && !past_deadline {\n 236|        WakeGate::Racing\n 237|    } else {\n 238|        WakeGate::Claim\n...\n 1906|/// (reused; only the translation child is created). Extracted verbatim from\n*1907|/// `dispatch_spawn` so a live-update restart (`dispatch_adapter_apply`, W3d) can\n 1908|/// rebuild a session's translation child from the NEW binary without disturbing\n 1909|/// the PTY / output log / session (brain-parity).\n 1910|// [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\n...\n 2558|    /// perch-record claim (that would add another info.json writer, the exact\n*2559|    /// write-stomp this REQ kills). An id is claimed for the duration of one spawn;\n*2560|    /// a duplicate wake for a claimed-or-already-live endpoint stands down (no-op ack).\n 2561|    wake_inflight: Mutex<HashSet<String>>,\n 2562|    /// Live per-connection handler-thread tally: incremented on [`handle_conn`]\n 2563|    /// entry, decremented (RAII, panic-safe) when the handler returns. The\n...\n 3190|                }\n*3191|                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n 3192|                // An unknown kind is tolerated (forward-compat): report, don't die.\n 3193|                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n 3194|            }\n...\n 3246|                enum Gate<'a> {\n*3247|                    Claimed(WakeClaimGuard<'a>),\n 3248|                    AlreadyLive(u64, Option<u32>),\n 3249|                    Racing,\n 3250|                }\n...\n 3269|                            inflight.insert(req.endpoint.clone());\n*3270|                            Gate::Claimed(WakeClaimGuard {\n 3271|                                set: &self.wake_inflight,\n 3272|                                endpoint: req.endpoint.clone(),\n 3273|                            })\n...\n 3277|                match gate {\n*3278|                    Gate::Claimed(guard) => break Some(guard),\n 3279|                    Gate::AlreadyLive(sid, spid) => {\n 3280|                        eprintln!(\n 3281|                            \"SPAWN_DEDUP:{}: endpoint already has a live session {sid} — \\\n...\n 3884|    // [impl->REQ-UPD-3]\n*3885|    fn dispatch_brain_restart(&self, send: &SharedSend) {\n 3886|        let honored = match self.brain_restart.get() {\n 3887|            Some(signal) => {\n 3888|                signal.request();\n...\n 5822|    // already-live endpoint dedups (AlreadyLive) regardless of claim/deadline; a\n*5823|    // claimed-and-within-window endpoint stands down (Racing) waiting for the racer's\n 5824|    // session; a free endpoint — or a claim that OVERRAN the wait window (leaked from a\n 5825|    // dead spawn) — proceeds (Claim).\n 5826|    #[test]\n...\n 5836|        assert_eq!(wake_gate_decision(false, true, true), Claim);\n*5837|        // Not live, unclaimed → claim + spawn (both inside + past the window).\n 5838|        assert_eq!(wake_gate_decision(false, false, false), Claim);\n 5839|        assert_eq!(wake_gate_decision(false, false, true), Claim);\n 5840|    }\n#### dispatch.rs\n 45|//!\n*46|//! Each claimed stream gets its own worker thread with its **own** brain IPC\n 47|//! connection (the broker is multi-client by design; the probe-brain pattern\n 48|//! every E2E test uses). The peek and the serve use **separate** connections:\n 49|//! the peek brain subscribes from 0 to read the first line and is dropped;\n...\n 197|/// stream).\n*198|pub fn run_dispatch_loop(\n 199|    broker_name: &str,\n 200|    paths: &DispatchPaths,\n 201|    registry: Arc<RegistryHost>,\n...\n 205|    let mut brain = connect(broker_name)?;\n*206|    let mut claimed: HashSet<u64> = HashSet::new();\n 207|\n 208|    while !stop.load(Ordering::Relaxed) {\n 209|        let reply = brain.net_streams()?;\n...\n 213|            // next_seq guard also skips a row whose pump hasn't appended yet).\n*214|            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {\n 215|                continue;\n 216|            }\n 217|            let name = broker_name.to_string();\n...\n 282|\n*283|/// One claimed stream's worker: peek → classify → run the family's serve\n 284|/// loop on a fresh brain connection.\n 285|fn worker(\n 286|    broker_name: &str,\n...\n 693|        if let Err(e) =\n*694|            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n 695|        {\n 696|            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\n 697|        }\n#### inject.rs\n 122|    );\n*123|    let claimed =\n 124|        match spt_store::spool::claim_idle_edge_audited_at(&perch_path, include_deferred, &audit) {\n 125|            Ok(rows) if !rows.is_empty() => rows,\n 126|            _ => return 0,\n...\n 129|    let mut release_ids: Vec<i64> = Vec::new();\n*130|    let mut rest = claimed.into_iter();\n 131|    for (msg_id, from, body) in rest.by_ref() {\n 132|        match try_spt_hosted_inject(id, &from, &body, owlery, false) {\n 133|            Some((true, _)) => delivered += 1,\n#### nethost.rs\n 532|    /// Connect, prove membership in-band (Mesh-D2 — a failed/empty proof drops\n*533|    /// the dial before it registers; Mesh-D4 roster leg rides the same stream on\n 534|    /// success), then register the conn (which emits `PRESENCE_CONNECTED` with\n 535|    /// the observed addr). The caller bounds this await (the blocking dial via\n 536|    /// `bounded_block_on`; the submit via its own `timeout`).\n...\n 877|                        // closes it. With membership disabled, register as before.\n*878|                        // Mesh-D4: on success, a roster leg rides the same stream.\n 879|                        let proven = match membership {\n 880|                            Some(src) => {\n 881|                                let creds = src();\n#### tunnelhub.rs\n 42|//! The cross-node-on-LAN real-Iroh wire is the W5 `[twohost]` rung (doyle gate R2)\n*43|//! — never claimed as proven by this loopback mock.\n 44|// [impl->REQ-SHELL-4]\n 45|\n 46|use std::collections::HashMap;\n#### wan.rs\n 112|pub enum WanOutcome {\n*113|    /// Delivered live over TCP to the target's listener (op claimed after).\n 114|    DeliveredTcp,\n 115|    /// Delivered by injecting into a broker-held (spt-hosted, relay-less) PTY via\n*116|    /// the live translation binary (op claimed after) — the F-023 WAN-ingress leg.\n 117|    /// Rides the SAME \"delivered\" wire token as [`WanOutcome::DeliveredTcp`] (no\n 118|    /// wire change): both are a confirmed live delivery to the sender.\n 119|    DeliveredInject,\n...\n 121|    Spooled,\n*122|    /// The op_id was already claimed — a replay; nothing written.\n 123|    Duplicate,\n 124|    /// Refused at the access gate (ADR-0009) — dropped before any delivery.\n 125|    Refused,\n*126|    /// No perch for the target on this node — dropped (not claimed: if the\n 127|    /// perch appears later, a replayed record may still land).\n 128|    NoPerch,\n 129|}\n...\n 156|    Spooled,\n*157|    /// A replay the remote node had already claimed (idempotent no-op).\n 158|    Duplicate,\n 159|    /// Refused at the remote access gate (ADR-0009).\n 160|    Refused,\n...\n 171|    /// Map a reply token back to an outcome; an unknown token is treated as\n*172|    /// `NoReply` (unconfirmed — never optimistically claimed as delivered).\n 173|    // [impl->REQ-WAN-SEND-DELIVERY]\n 174|    pub fn from_token(token: &str) -> Self {\n 175|        match token {\n...\n 272|\n*273|    // Replay check first: a claimed op is done regardless of which path\n*274|    // claimed it.\n 275|    match spool::wan_seen_at(&perch_path, &msg.op_id) {\n 276|        Ok(true) => return WanOutcome::Duplicate,\n 277|        Ok(false) => {}\n...\n 353|    // back to the matching request outcome, and an unknown/garbled token is\n*354|    // treated as NoReply (unconfirmed — never optimistically claimed delivered).\n 355|    #[test]\n 356|    fn wan_outcome_token_round_trips_and_unknown_is_noreply() {\n 357|        let pairs = [\n\n## spt/tests/\n### idle_edge_drain_e2e.rs\n 227|\n*228|    // ── (8) The spooled deferred row must be DELIVERED (claimed by the idle-edge\n 229|    //    take + injected via the shared leg) → the spool clears. Poll briefly in\n 230|    //    case the inject worker's commit lags the CLI return. ──\n 231|    let mut pending_after = pending_before;"}],"details":{"scopePath":"crates/spt-daemon/tests, crates/spt/tests, crates/spt-daemon/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":49,"fileCount":16,"files":["crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/tests/mesh.rs","crates/spt-daemon/tests/pump.rs","crates/spt-daemon/tests/rosterprop.rs","crates/spt-daemon/tests/twohost.rs","crates/spt-daemon/tests/wake_single_flight.rs","crates/spt-daemon/tests/wanmsg.rs","crates/spt-daemon/tests/xfer.rs","crates/spt/tests/idle_edge_drain_e2e.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/inject.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/tunnelhub.rs","crates/spt-daemon/src/wan.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/attach.rs","count":3},{"path":"crates/spt-daemon/tests/dispatch.rs","count":4},{"path":"crates/spt-daemon/tests/mesh.rs","count":1},{"path":"crates/spt-daemon/tests/pump.rs","count":1},{"path":"crates/spt-daemon/tests/rosterprop.rs","count":1},{"path":"crates/spt-daemon/tests/twohost.rs","count":1},{"path":"crates/spt-daemon/tests/wake_single_flight.rs","count":1},{"path":"crates/spt-daemon/tests/wanmsg.rs","count":1},{"path":"crates/spt-daemon/tests/xfer.rs","count":2},{"path":"crates/spt/tests/idle_edge_drain_e2e.rs","count":1},{"path":"crates/spt-daemon/src/broker.rs","count":13},{"path":"crates/spt-daemon/src/dispatch.rs","count":6},{"path":"crates/spt-daemon/src/inject.rs","count":2},{"path":"crates/spt-daemon/src/nethost.rs","count":2},{"path":"crates/spt-daemon/src/tunnelhub.rs","count":1},{"path":"crates/spt-daemon/src/wan.rs","count":9}],"truncated":false,"displayContent":"# crates/\n\n## spt-daemon/\n\n### tests/\n#### attach.rs\n  772│// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain\n *773│// crash mid-attach: the successor re-serves the SAME broker-held stream from\n  774│// seq 0 (worst-case stale cursor) — the replayed Request/input/output all\n  775│// dedup through the journals, input B sent INTO the dead window arrives, and\n  776│// B's viewport holds every marker exactly once.\n     │...\n  854│\n *855│    // Life 2: handoff, then re-serve the SAME stream from seq 0 — the\n  856│    // worst-case replay. serve_attach re-handles the replayed Request (gate +\n  857│    // re-attach from 0, so every output record re-transmits — B's render\n  858│    // cursor drops the duplicates), replays MARKER_ONE's input (PTY-write\n     │...\n 1126│\n*1127│    // The \"successor\" re-serves the SAME operator's stream (same by) on a new\n 1128│    // connection — the brain-restart re-take.\n 1129│    let mut successor = connect_retry(&name);\n 1130│    successor.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"successor control\");\n#### dispatch.rs\n 108│    thread::spawn(move || {\n*109│        let _ = run_dispatch_loop(&name, &paths, registry, &flag, Duration::from_millis(25));\n 110│    });\n 111│    stop\n 112│}\n    │...\n 343│// and the direction fact is recorded right: A's table shows its opened\n*344│// stream as locally-initiated; B's shows the same stream as peer-initiated\n*345│// (what the dispatcher claimed it by).\n 346│#[test]\n 347│fn dispatcher_funnels_wan_messages_undriven() {\n 348│    init_home();\n    │...\n 382│\n*383│    // Direction fact, both sides of the same stream.\n 384│    let a_rows = a.net_streams().expect(\"A streams\").streams;\n 385│    assert!(\n 386│        a_rows\n#### mesh.rs\n 250│            thread::spawn(move || {\n*251│                let _ = run_dispatch_loop(&name, &disp, reg, &flag, Duration::from_millis(25));\n 252│            });\n 253│        }\n 254│        stop\n#### pump.rs\n 239│        thread::spawn(move || {\n*240│            let _ = run_dispatch_loop(&name, &paths, reg, &flag, Duration::from_millis(25));\n 241│        });\n 242│    }\n 243│\n#### rosterprop.rs\n 147│\n*148│    // A dials B: the proof verifies, then the roster leg runs on the same stream.\n 149│    // dial() blocks through the dialer leg, so A's sink has merged B's slice by\n 150│    // the time it returns.\n 151│    let (_conn_id, remote) = a.dial(b.addr()).expect(\"dial member\");\n#### twohost.rs\n 342│        thread::spawn(move || {\n*343│            let _ = run_dispatch_loop(&name, &paths, reg, &flag, Duration::from_millis(50));\n 344│        });\n 345│    }\n 346│    {\n#### wake_single_flight.rs\n 17│//! claim (that would add another info.json writer, the very write-stomp this kills). An\n*18│//! endpoint id is claimed for the duration of one spawn; a duplicate wake for a claimed-\n 19│//! or-already-live endpoint stands down and no-op acks to the existing session.\n 20│//!\n 21│//! ## What this rig asserts\n#### wanmsg.rs\n 155│// [unit->REQ-MSG-5] WAN fail-closed at the FUNNEL (doyle G2 condition, KH 7.5):\n*156│// a claimed `user-msg` arriving through receive_wan from an origin that is NOT\n 157│// provably user-backed is re-stamped to plain `msg` BEFORE it reaches the\n 158│// target spool — the security-critical row of the identity-gate truth table on\n 159│// the cross-node surface. A fail-OPEN here would grant the user's authority to\n#### xfer.rs\n  14│//!   dies mid-push with a partial temp file on disk; the successor re-serves\n *15│//!   the SAME stream from seq 0 — the replayed chunks rewrite in place\n  16│//!   (positional idempotency), the commit renames atomically, and the file is\n  17│//!   byte-identical with no stray temp left behind.\n  18│\n    │...\n 323│    // Life 2: a successor brain connects fresh, then the REAL serve loop\n*324│    // re-serves the SAME broker-held stream from seq 0 — the worst-case full\n 325│    // replay over the partial temp.\n 326│    let mut life2 = connect_retry(&name_a);\n 327│    let outcome =\n\n### src/\n#### broker.rs\n  204│/// exit (success, an early `?` spawn/drain error, or a panic), so a failed spawn never\n *205│/// strands the endpoint permanently claimed.\n  206│struct WakeClaimGuard<'a> {\n  207│    set: &'a Mutex<HashSet<String>>,\n  208│    endpoint: String,\n     │...\n  231│// [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\n *232│fn wake_gate_decision(live: bool, claimed: bool, past_deadline: bool) -> WakeGate {\n  233│    if live {\n  234│        WakeGate::AlreadyLive\n *235│    } else if claimed && !past_deadline {\n  236│        WakeGate::Racing\n  237│    } else {\n  238│        WakeGate::Claim\n     │...\n 1906│/// (reused; only the translation child is created). Extracted verbatim from\n*1907│/// `dispatch_spawn` so a live-update restart (`dispatch_adapter_apply`, W3d) can\n 1908│/// rebuild a session's translation child from the NEW binary without disturbing\n 1909│/// the PTY / output log / session (brain-parity).\n 1910│// [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\n     │...\n 2558│    /// perch-record claim (that would add another info.json writer, the exact\n*2559│    /// write-stomp this REQ kills). An id is claimed for the duration of one spawn;\n*2560│    /// a duplicate wake for a claimed-or-already-live endpoint stands down (no-op ack).\n 2561│    wake_inflight: Mutex<HashSet<String>>,\n 2562│    /// Live per-connection handler-thread tally: incremented on [`handle_conn`]\n 2563│    /// entry, decremented (RAII, panic-safe) when the handler returns. The\n     │...\n 3190│                }\n*3191│                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n 3192│                // An unknown kind is tolerated (forward-compat): report, don't die.\n 3193│                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n 3194│            }\n     │...\n 3246│                enum Gate<'a> {\n*3247│                    Claimed(WakeClaimGuard<'a>),\n 3248│                    AlreadyLive(u64, Option<u32>),\n 3249│                    Racing,\n 3250│                }\n     │...\n 3269│                            inflight.insert(req.endpoint.clone());\n*3270│                            Gate::Claimed(WakeClaimGuard {\n 3271│                                set: &self.wake_inflight,\n 3272│                                endpoint: req.endpoint.clone(),\n 3273│                            })\n     │...\n 3277│                match gate {\n*3278│                    Gate::Claimed(guard) => break Some(guard),\n 3279│                    Gate::AlreadyLive(sid, spid) => {\n 3280│                        eprintln!(\n 3281│                            \"SPAWN_DEDUP:{}: endpoint already has a live session {sid} — \\\n     │...\n 3884│    // [impl->REQ-UPD-3]\n*3885│    fn dispatch_brain_restart(&self, send: &SharedSend) {\n 3886│        let honored = match self.brain_restart.get() {\n 3887│            Some(signal) => {\n 3888│                signal.request();\n     │...\n 5822│    // already-live endpoint dedups (AlreadyLive) regardless of claim/deadline; a\n*5823│    // claimed-and-within-window endpoint stands down (Racing) waiting for the racer's\n 5824│    // session; a free endpoint — or a claim that OVERRAN the wait window (leaked from a\n 5825│    // dead spawn) — proceeds (Claim).\n 5826│    #[test]\n     │...\n 5836│        assert_eq!(wake_gate_decision(false, true, true), Claim);\n*5837│        // Not live, unclaimed → claim + spawn (both inside + past the window).\n 5838│        assert_eq!(wake_gate_decision(false, false, false), Claim);\n 5839│        assert_eq!(wake_gate_decision(false, false, true), Claim);\n 5840│    }\n#### dispatch.rs\n  45│//!\n *46│//! Each claimed stream gets its own worker thread with its **own** brain IPC\n  47│//! connection (the broker is multi-client by design; the probe-brain pattern\n  48│//! every E2E test uses). The peek and the serve use **separate** connections:\n  49│//! the peek brain subscribes from 0 to read the first line and is dropped;\n    │...\n 197│/// stream).\n*198│pub fn run_dispatch_loop(\n 199│    broker_name: &str,\n 200│    paths: &DispatchPaths,\n 201│    registry: Arc<RegistryHost>,\n    │...\n 205│    let mut brain = connect(broker_name)?;\n*206│    let mut claimed: HashSet<u64> = HashSet::new();\n 207│\n 208│    while !stop.load(Ordering::Relaxed) {\n 209│        let reply = brain.net_streams()?;\n    │...\n 213│            // next_seq guard also skips a row whose pump hasn't appended yet).\n*214│            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {\n 215│                continue;\n 216│            }\n 217│            let name = broker_name.to_string();\n    │...\n 282│\n*283│/// One claimed stream's worker: peek → classify → run the family's serve\n 284│/// loop on a fresh brain connection.\n 285│fn worker(\n 286│    broker_name: &str,\n    │...\n 693│        if let Err(e) =\n*694│            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n 695│        {\n 696│            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\n 697│        }\n#### inject.rs\n 122│    );\n*123│    let claimed =\n 124│        match spt_store::spool::claim_idle_edge_audited_at(&perch_path, include_deferred, &audit) {\n 125│            Ok(rows) if !rows.is_empty() => rows,\n 126│            _ => return 0,\n    │...\n 129│    let mut release_ids: Vec<i64> = Vec::new();\n*130│    let mut rest = claimed.into_iter();\n 131│    for (msg_id, from, body) in rest.by_ref() {\n 132│        match try_spt_hosted_inject(id, &from, &body, owlery, false) {\n 133│            Some((true, _)) => delivered += 1,\n#### nethost.rs\n 532│    /// Connect, prove membership in-band (Mesh-D2 — a failed/empty proof drops\n*533│    /// the dial before it registers; Mesh-D4 roster leg rides the same stream on\n 534│    /// success), then register the conn (which emits `PRESENCE_CONNECTED` with\n 535│    /// the observed addr). The caller bounds this await (the blocking dial via\n 536│    /// `bounded_block_on`; the submit via its own `timeout`).\n    │...\n 877│                        // closes it. With membership disabled, register as before.\n*878│                        // Mesh-D4: on success, a roster leg rides the same stream.\n 879│                        let proven = match membership {\n 880│                            Some(src) => {\n 881│                                let creds = src();\n#### tunnelhub.rs\n 42│//! The cross-node-on-LAN real-Iroh wire is the W5 `[twohost]` rung (doyle gate R2)\n*43│//! — never claimed as proven by this loopback mock.\n 44│// [impl->REQ-SHELL-4]\n 45│\n 46│use std::collections::HashMap;\n#### wan.rs\n 112│pub enum WanOutcome {\n*113│    /// Delivered live over TCP to the target's listener (op claimed after).\n 114│    DeliveredTcp,\n 115│    /// Delivered by injecting into a broker-held (spt-hosted, relay-less) PTY via\n*116│    /// the live translation binary (op claimed after) — the F-023 WAN-ingress leg.\n 117│    /// Rides the SAME \"delivered\" wire token as [`WanOutcome::DeliveredTcp`] (no\n 118│    /// wire change): both are a confirmed live delivery to the sender.\n 119│    DeliveredInject,\n    │...\n 121│    Spooled,\n*122│    /// The op_id was already claimed — a replay; nothing written.\n 123│    Duplicate,\n 124│    /// Refused at the access gate (ADR-0009) — dropped before any delivery.\n 125│    Refused,\n*126│    /// No perch for the target on this node — dropped (not claimed: if the\n 127│    /// perch appears later, a replayed record may still land).\n 128│    NoPerch,\n 129│}\n    │...\n 156│    Spooled,\n*157│    /// A replay the remote node had already claimed (idempotent no-op).\n 158│    Duplicate,\n 159│    /// Refused at the remote access gate (ADR-0009).\n 160│    Refused,\n    │...\n 171│    /// Map a reply token back to an outcome; an unknown token is treated as\n*172│    /// `NoReply` (unconfirmed — never optimistically claimed as delivered).\n 173│    // [impl->REQ-WAN-SEND-DELIVERY]\n 174│    pub fn from_token(token: &str) -> Self {\n 175│        match token {\n    │...\n 272│\n*273│    // Replay check first: a claimed op is done regardless of which path\n*274│    // claimed it.\n 275│    match spool::wan_seen_at(&perch_path, &msg.op_id) {\n 276│        Ok(true) => return WanOutcome::Duplicate,\n 277│        Ok(false) => {}\n    │...\n 353│    // back to the matching request outcome, and an unknown/garbled token is\n*354│    // treated as NoReply (unconfirmed — never optimistically claimed delivered).\n 355│    #[test]\n 356│    fn wan_outcome_token_round_trips_and_unknown_is_noreply() {\n 357│        let pairs = [\n\n## spt/tests/\n### idle_edge_drain_e2e.rs\n 227│\n*228│    // ── (8) The spooled deferred row must be DELIVERED (claimed by the idle-edge\n 229│    //    take + injected via the shared leg) → the spool clears. Poll briefly in\n 230│    //    case the inject worker's commit lags the CLI return. ──\n 231│    let mut pending_after = pending_before;"},"isError":false,"timestamp":1784195886136}}
{"type":"message","id":"0f4d31bd","parentId":"5be79095","timestamp":"2026-07-16T09:58:12.841Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying missing dispatcher restart test**"},{"type":"toolCall","id":"call_7PnHdSeYzTG3WD6f0euDV5xZ|fc_047bb49263dfa303016a58ab3402c4819899e54de99e109f35","name":"read","arguments":{"i":"Reading brain refresh startup sequence","path":"crates/spt-daemon/src/brainproc.rs","selector":"175-288"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":26358,"output":127,"cacheRead":111104,"cacheWrite":0,"totalTokens":137589,"reasoningTokens":84,"cost":{"input":0.13179000000000002,"output":0.00381,"cacheRead":0.055552,"cacheWrite":0,"total":0.19115200000000002}},"stopReason":"toolUse","timestamp":1784195886182,"responseId":"resp_047bb49263dfa303016a58ab2eb33481988e9963f8795b2073","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58ab31223481989f8995d2acd10ddc","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKs072xUd8DEKXMmDDZeq3W8gf8pTSJcWIvTXdA6MtEGEA1XlUUkBsJrBHo1OqTK7PWJp-3BkMrZLrrVQuXOzmGqIhAa3_eMe4o6a-zx26ZftHg-mQUBUqLS94aC1EA0v0iWhpWuVO7-w5xwXmWlnpMPviVnooSIMCrkZ5i10MXTDVHayQMIdvA2utJOfQG3l5GmoIBH59RoOZCuQhDY7bC7BtBO32kqNq9QMS_fsj5TtOTGy3NldC7xnaZzHrCmjQRz4-zVx-1T24jVNmFjyHhvXqbLYkzJAZPuFfgoyVYAsqUBMrMG_A75JEcyTIdBG8J2XXGANtxJHUXx5vIIXyt1d5WDsrB9zKwVCSxJPXhDqRoVZlO6jVul8H4jwHuJTvEzJKcQOiuWQb1KCgLyQDRN1apkAZgd_6Pi5j96OMznHTJk16qxd1ovB2_6jMKaKAmt-XyYem1QV4NyI87U8pHYGexf0R_0OMORVhlRNZtFQzPaLkSUGfyWpxaqibdTvefKoGGZsWdYNGbWFwQv_IkpIpGbPiJ6009TEKa39x9nThVNqSRjS98rThxZdwxFLQq0BncP8WPlu-MtYxtkwW4Wxqu27K8k4hzP_Mex4Qgsgy8gSNMlT6KDhR5kx6j05JBIulcfvd4aBaIHWP6dHyQ_i-Eh8lPkvLkUFR8CiEzin9xfzqjEekzr5ewe6KSVi5KTjhGYkKFG5Zjpz17wq9Ikvh_JYFcrhkkNlLJ5ABNj9sm1_8d0EykdGW62DCaTLXLRBacohU0W3U8k1o07iutZbptNV3TBi7ObQ7GIm-TBl89s31AEqGPDt50RllxqBlRkl5yrcMiF46DsA04tp2i2nz-E-poH0khEi8i7qs_0FXOw2ApEaL6cFHK5zDCFq1Mwl1W53CiYjv0ODkd_gbqTgjzw2LaJVn1b0uhBKWpZl8n9UbadBzy5Pklkcy6j0F4ysuwq3zjxhGv3kNp6Ew418U760s1mPOnWlmuWqf2z4liD6yWc_M1-q7EqMIiwmhkA-fbdoeCqUvIlko3LHBepuE9UmW6y7TX1sNkIY6Ue_6opzhflVdiX2i8-xktH1c1yHJCT7d1q4o_qeOE5_fwAvLN43WHrE3CDzR4E8W0qmDr3atmEKHjfdNE1rxs4QT6jdjtu9IX4s06dlJlVYb6zDKedm_rMOzjVURq89lJiv_VhS5Tich0lT7LrwlHByfkekkBnGBjQmpTAn8q2W_mujI5MsSGQG15bKkshKdPXKcov1NjrGbyoXUJUWVLmb66I5ZVtpji-aPml9ckoustD5JFWwp-AiwrInVUVJQzUcZLyxWi-FwkMIdY45tCP1C6y1kC19-4jmDvrrIcZ0J9UJpCdq8RN1w6J05OCQR88pR3_xMlKSPWQ16tBxUlIbWSihCFB9P2-8J_L5l3LVoWXdg==","summary":[{"type":"summary_text","text":"**Identifying missing dispatcher restart test**"}]},{"id":"fc_047bb49263dfa303016a58ab3402c4819899e54de99e109f35","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain refresh startup sequence\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"175-288\"}","call_id":"call_7PnHdSeYzTG3WD6f0euDV5xZ","name":"read"}]},"duration":6658.16579999961,"ttft":2930.308799999766,"contextSnapshot":{"promptTokens":137462,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_7PnHdSeYzTG3WD6f0euDV5xZ|fc_047bb49263dfa303016a58ab3402c4819899e54de99e109f35","toolName":"read","startedAt":"2026-07-16T09:58:12.841Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading brain refresh startup sequence"},"id":"896e530d","parentId":"0f4d31bd","timestamp":"2026-07-16T09:58:12.841Z"}
{"type":"message","id":"7021f03b","parentId":"896e530d","timestamp":"2026-07-16T09:58:12.857Z","message":{"role":"toolResult","toolCallId":"call_7PnHdSeYzTG3WD6f0euDV5xZ|fc_047bb49263dfa303016a58ab3402c4819899e54de99e109f35","toolName":"read","content":[{"type":"text","text":"    // *counter* is rehydrated, never the wall-clock stamp.\n    eprintln!(\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\n        std::process::id(),\n        reason.as_arg(),\n    );\n\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\n    // from its delivered cursor, the production replacement for the retired\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    match brain.resume_session_cursors() {\n        Ok(ids) if !ids.is_empty() => {\n            eprintln!(\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n                ids.len()\n            )\n        }\n        Ok(_) => {}\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\n    }\n\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\n    // breadcrumb would accept a brain that connected but never rebuilt\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\n    // this is forward-correct: the gate is defined on the real readiness\n    // semantic the live-agent adapter inherits.\n    write_ready(generation);\n\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n    // online/offline exclusivity — a reconcile loop that runs each offline\n    // instance's `wake_command` as a supervised watcher child. Relocated from\n    // the broker process to the brain child: shellwake is brain-owned logic\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\n    // posture. The stop flag is held for symmetry, never raised (the watchers\n    // die with the brain process and the next brain re-sweeps them).\n    // [impl->REQ-SHELL-2]\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\n\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\n    // shell waker). Online-only is staged behind the interim `api listen`\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\n    // brain the first-host. The stop flag is held for symmetry; the threads die\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\n    // [impl->REQ-DAEMON-1]\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\n\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\n    // reconcile in the background (ready-without-warm is preserved because\n    // write_ready already fired above), then drain/debounce/coalesce the\n    // invalidation queue. Same posture as the hosts above: the stop flag is\n    // held for symmetry, the thread dies with the brain, and the next brain\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n\n    let mut consumers_started = false;\n    loop {\n        thread::sleep(BRAIN_HEARTBEAT);\n        // `net-status` is both the net-capability probe and the broker-liveness\n        // probe: an `Err` is the broker gone (or a future planned\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n        let status = match brain.net_status() {\n            Ok(s) => s,\n            Err(e) => {\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\n                return Err(e);\n            }\n        };\n        if consumer_gate(status.enabled, consumers_started) {\n            let hex = status.node_id_hex.clone().unwrap_or_default();\n            spawn_net_consumers(&hex);\n            consumers_started = true;\n            eprintln!(\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n            );\n        }\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\n        // window, so without this the wedge never drains: the promotion DRAINED gate\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n        // best-effort — a transient sessions error must never fell the heartbeat (the\n…\n    }\n\n[Showing lines 174-291 of 2116. Use :292 to continue]"}],"details":{"truncation":{"content":"    // *counter* is rehydrated, never the wall-clock stamp.\r\n    eprintln!(\r\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\r\n        std::process::id(),\r\n        reason.as_arg(),\r\n    );\r\n\r\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\r\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\r\n    // from its delivered cursor, the production replacement for the retired\r\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\r\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\r\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\r\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\r\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\r\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\r\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\r\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\r\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\r\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\r\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\r\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\r\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\r\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\r\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\r\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\r\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    match brain.resume_session_cursors() {\r\n        Ok(ids) if !ids.is_empty() => {\r\n            eprintln!(\r\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\r\n                ids.len()\r\n            )\r\n        }\r\n        Ok(_) => {}\r\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\r\n    }\r\n\r\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\r\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\r\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\r\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\r\n    // breadcrumb would accept a brain that connected but never rebuilt\r\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\r\n    // this is forward-correct: the gate is defined on the real readiness\r\n    // semantic the live-agent adapter inherits.\r\n    write_ready(generation);\r\n\r\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\r\n    // online/offline exclusivity — a reconcile loop that runs each offline\r\n    // instance's `wake_command` as a supervised watcher child. Relocated from\r\n    // the broker process to the brain child: shellwake is brain-owned logic\r\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\r\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\r\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\r\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\r\n    // posture. The stop flag is held for symmetry, never raised (the watchers\r\n    // die with the brain process and the next brain re-sweeps them).\r\n    // [impl->REQ-SHELL-2]\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\r\n\r\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\r\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\r\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\r\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\r\n    // shell waker). Online-only is staged behind the interim `api listen`\r\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\r\n    // brain the first-host. The stop flag is held for symmetry; the threads die\r\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\r\n    // [impl->REQ-DAEMON-1]\r\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\r\n\r\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\r\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\r\n    // reconcile in the background (ready-without-warm is preserved because\r\n    // write_ready already fired above), then drain/debounce/coalesce the\r\n    // invalidation queue. Same posture as the hosts above: the stop flag is\r\n    // held for symmetry, the thread dies with the brain, and the next brain\r\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\r\n\r\n    let mut consumers_started = false;\r\n    loop {\r\n        thread::sleep(BRAIN_HEARTBEAT);\r\n        // `net-status` is both the net-capability probe and the broker-liveness\r\n        // probe: an `Err` is the broker gone (or a future planned\r\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\r\n        let status = match brain.net_status() {\r\n            Ok(s) => s,\r\n            Err(e) => {\r\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\r\n                return Err(e);\r\n            }\r\n        };\r\n        if consumer_gate(status.enabled, consumers_started) {\r\n            let hex = status.node_id_hex.clone().unwrap_or_default();\r\n            spawn_net_consumers(&hex);\r\n            consumers_started = true;\r\n            eprintln!(\r\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\r\n            );\r\n        }\r\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\r\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\r\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\r\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\r\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\r\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\r\n        // window, so without this the wedge never drains: the promotion DRAINED gate\r\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\r\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\r\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\r\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\r\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\r\n        // best-effort — a transient sessions error must never fell the heartbeat (the\r","truncated":true,"truncatedBy":"lines","totalLines":1943,"totalBytes":7475,"outputLines":118,"outputBytes":7475,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    // *counter* is rehydrated, never the wall-clock stamp.\r\n    eprintln!(\r\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\r\n        std::process::id(),\r\n        reason.as_arg(),\r\n    );\r\n\r\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\r\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\r\n    // from its delivered cursor, the production replacement for the retired\r\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\r\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\r\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\r\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\r\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\r\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\r\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\r\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\r\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\r\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\r\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\r\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\r\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\r\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\r\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\r\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\r\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    match brain.resume_session_cursors() {\r\n        Ok(ids) if !ids.is_empty() => {\r\n            eprintln!(\r\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\r\n                ids.len()\r\n            )\r\n        }\r\n        Ok(_) => {}\r\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\r\n    }\r\n\r\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\r\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\r\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\r\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\r\n    // breadcrumb would accept a brain that connected but never rebuilt\r\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\r\n    // this is forward-correct: the gate is defined on the real readiness\r\n    // semantic the live-agent adapter inherits.\r\n    write_ready(generation);\r\n\r\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\r\n    // online/offline exclusivity — a reconcile loop that runs each offline\r\n    // instance's `wake_command` as a supervised watcher child. Relocated from\r\n    // the broker process to the brain child: shellwake is brain-owned logic\r\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\r\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\r\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\r\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\r\n    // posture. The stop flag is held for symmetry, never raised (the watchers\r\n    // die with the brain process and the next brain re-sweeps them).\r\n    // [impl->REQ-SHELL-2]\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\r\n\r\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\r\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\r\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\r\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\r\n    // shell waker). Online-only is staged behind the interim `api listen`\r\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\r\n    // brain the first-host. The stop flag is held for symmetry; the threads die\r\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\r\n    // [impl->REQ-DAEMON-1]\r\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\r\n\r\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\r\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\r\n    // reconcile in the background (ready-without-warm is preserved because\r\n    // write_ready already fired above), then drain/debounce/coalesce the\r\n    // invalidation queue. Same posture as the hosts above: the stop flag is\r\n    // held for symmetry, the thread dies with the brain, and the next brain\r\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\r\n\r\n    let mut consumers_started = false;\r\n    loop {\r\n        thread::sleep(BRAIN_HEARTBEAT);\r\n        // `net-status` is both the net-capability probe and the broker-liveness\r\n        // probe: an `Err` is the broker gone (or a future planned\r\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\r\n        let status = match brain.net_status() {\r\n            Ok(s) => s,\r\n            Err(e) => {\r\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\r\n                return Err(e);\r\n            }\r\n        };\r\n        if consumer_gate(status.enabled, consumers_started) {\r\n            let hex = status.node_id_hex.clone().unwrap_or_default();\r\n            spawn_net_consumers(&hex);\r\n            consumers_started = true;\r\n            eprintln!(\r\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\r\n            );\r\n        }\r\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\r\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\r\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\r\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\r\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\r\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\r\n        // window, so without this the wedge never drains: the promotion DRAINED gate\r\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\r\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\r\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\r\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\r\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\r\n        // best-effort — a transient sessions error must never fell the heartbeat (the\r\n…\n    }","startLine":174,"lineNumbers":[174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,null,298]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2116,"totalBytes":7475,"outputLines":118,"outputBytes":7475,"shownRange":{"start":174,"end":291},"nextOffset":292}}},"isError":false,"timestamp":1784195892857}}
{"type":"message","id":"e9e70323","parentId":"7021f03b","timestamp":"2026-07-16T09:58:28.114Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Clarifying dispatcher and rc process roles**\n\n**Investigating subscription cleanup race conditions**"},{"type":"toolCall","id":"call_O70pPpbNXJl2fKXt9236gIgY|fc_047bb49263dfa303016a58ab4395688198b1735a891c2b7338","name":"read","arguments":{"i":"Reading broker disconnect cleanup exact","path":"crates/spt-daemon/src/broker.rs","selector":"2970-3220"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4781,"output":299,"cacheRead":134656,"cacheWrite":0,"totalTokens":139736,"reasoningTokens":255,"cost":{"input":0.023905000000000003,"output":0.00897,"cacheRead":0.067328,"cacheWrite":0,"total":0.100203}},"stopReason":"toolUse","timestamp":1784195892866,"responseId":"resp_047bb49263dfa303016a58ab355b748198999048dc27ce6db9","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58ab3c014c8198886bbdf854f4f6cd","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKtD-foZlOpiNiSWZ23OfXLytjBKUCAGQA28yxcJQNJSXLWnUeOawse1lcOQxGFJbqy6kZhJX3Mb-WbeluLf8Qp9BTaQjzLyu51FBSB31w2LL3PIsmTbUw6Gsvk9Vkqk9k2h67x0SpDweLsBQpwIWVAlvRYM2ROw6zlnPMa8A3PVwoIrzIQIAOVLHQHgr9j8Lgl3PB7nuNFNY-rlDka8oXIFHU2SZCtmwHOl4TJihCxv_OVyqmM1cbv9sr9NyC6BG1U-UlKXRsWIWrZScK1x3-bQjmFjN_EyogWiYib0OcnJGPhsT2W6evLded0tcZuKx7Dj4X3vfHv7nDGyAyuZi8V6ZncfZ3ZXUlsGurEYiU-XhJ1k50-BDc36pbf1JS1zUkqPCUtGxAwd0cdrFiEr_ozUMcMbgTUdfFTobEg_PGt20UO52eUIOhUhcdHyuNeNxRYANeeRD_GuVVBXKLFE_ZwXvXIIrJbaLGtTEH-dhIAF-NwMMQkAewGUNfWv5UriT2n9B6IyCivaMJmVJKkaV5M7hrrJ9rvy3Et026CtM4TzSI9nl3Aeo-7-B9iZ4jJW-BRP-eyS26mSWC3OLbuHeSq1VJG9zqYo5X8W7LElPmcC_wlSsUdS4SeNyl5XYORhWz5AVax0pX7JpXzTFGBGVOukHPWYMlJ1lT0DN53QyI8S4KSiakatoPky3Eq3Ho1FQhQWyzciD0Q4S4qmbC04kZJCtk0602CF_7uQFfyqhIbAhRConEH4um7QDIOJcBedlye64hYYS67gbjGVqqtfiCQxpYpKHg9-cYMzZNGgosWuqkuzpGn-_u85UiCNpQcbtIpc-t4eLOLCx4XiuRPrCVstrWBUizr6NsaNoggyyUIy5WN27_EqOEJihbr0hTuT8wAgj_QJjp8771D9BFgmq4nqT1BiJjut_lg9QAzI7C0HoRBbe7OLOipe0bELH6dKO78pohYo9xdhl7Qdp64NQLvaUBvhiIy3wbMY4LzeZ3fXBW_eGGhuMjvJVDeYil5nkp7sHpkwvM1Hubtl4Td-n3gYSk1qZcq8d6kUDnXCDjWK2jTQOzdostiU4vXOJbJuAQED_5hJOT_McsKHerbsJS6XKEK75G9oc53s6BPh_76DQZF0hwrwdhNHQXRbqhxowYhCFub9hhXP2Mhm4g0FBpWPlHek-wTBJOXLHFHaZBN7AVTeRehHwmtX1ft_grNVC7GXapFWVXjhe-rANCljMaS65UCvvsx_wtOkEW_3dRUZ20RYykpWlxxn4ojD0m5LLwV4kuMIDXsdZEsxYv4vXPSSiRFpBUiB42LtEl9yG_2Aqmt9b3JXhc27d_6cepnyGDfVKJ613vG3Hg0TjNtl2KUcbdIhbimKGjTbjZOEVJZ_6aOfo1roUk_GGEOU7hBEXE5A2mjsWGXKN0Y-luoKCyDZcTb7l-LxGtNjZnGNSJ8L94uYLmgryTnZETRlVItJnZLw3dYYc9fIZAAv2-i7hgmxBzQfBOuygWO8OGUxcAbRQl6qzu-l9chU2rOPTYLi-tvHdETE6JqxdhCKe1PkOaS1DuU8lKx80BWaviyasStL4Y1EWY_goNRJVMlkVdOh2qVfO0BfXAZWgGbMOm3XHLEU9-kdshaY6n-3yloJWE2mOVLsLyPEMN6bFExnIKzpYUOGR1eIaEPqWZwAZQ3ipenBUCuIfbss0RTYGcovNRLxWRG3WfWw-8GXz6A8RtFSq6PVRpMjSMhYwd6kRD5sgCsxAoZBc9DbN4fCE3FTYoeJhshSwjhzkktdZjDKris_FX4VxxPO8seRpcTHu7GInou9z8mVlwkVxaREzoVBUVtA9Ipjq2Zz6chHJpzYw6SGqx9_UN0csO_yj-XnhME0NvrECmHIva_XiX3xUgyTdLPAfkEte1F1l4nZABR_qkyDeUoDqtkMVkpXWkKOQrztQ0KN0U7beCn49FZUyMSuV7Du2iTJ-P0u4S8Sn5i5hq7aL7UWgx7mn_4XCqOHLKMAE5uBZvViVWdPnzvxeg7wKYTg45RqeHzX-eS567c8yuqsoVr_CDYhz3ICnjrke_Dahvr1JE5H32yifG_9emxaJaPz38p1MkVi3Sl1iP3d53TFI_q6_lEbXGT0hJGxwx4JAyskFXZ9jHOduq1o_rPdfWB5xWdYVHkc31-4SYbX6nmJPVMOYG-v52nv7-qA5PBenSg4xU2vAYdXLs60Jltf1TrMRp-Q7Hmy32ceDFNpjWzvMobv36LSdjeoLJ5dMzgaQnF28PeNyueszxPS01C7iuWF85tzStpt_IV42gKUvJasSA3XlbVyD63vuyPUk-WXDJ_OnOuoMrZMpvQUAhJNQF-aWhEcJpAHQIU7nx-UO9KXXuTothtw8decycPJhEkjQFuRUum4nk-pZ4NFGU7bi6GVzzhDbLjwx4b5fYAbvXqlwOaSlviUozxNyt3cL5IOu89BDKtdOOIBcskkK_Ow5uYFbYr9fXpU_w5w-GunKe_uvPzdmok1Rz6F9-Hcj-TCXVaISQAUarWw4UpGb9K1bObdsh_cnEcQ5K-sHwZHmp69SGxk1x75AC3ixRr4_D-8A9AdEsFJsxkZoayPVZTBk0tp49Kk6OWwZeiEduXmMaIiBs2y0OLTXoVIKZGq3-8nMzs7UQwaq1WpDbr-hPAspvI9X12pjgXGW217Wn22fmxKcw0-","summary":[{"type":"summary_text","text":"**Clarifying dispatcher and rc process roles**"},{"type":"summary_text","text":"**Investigating subscription cleanup race conditions**"}]},{"id":"fc_047bb49263dfa303016a58ab4395688198b1735a891c2b7338","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker disconnect cleanup exact\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"2970-3220\"}","call_id":"call_O70pPpbNXJl2fKXt9236gIgY","name":"read"}]},"duration":15246.344199999236,"ttft":7118.722699999809,"contextSnapshot":{"promptTokens":139437,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_O70pPpbNXJl2fKXt9236gIgY|fc_047bb49263dfa303016a58ab4395688198b1735a891c2b7338","toolName":"read","startedAt":"2026-07-16T09:58:28.114Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker disconnect cleanup exact"},"id":"e42cc85c","parentId":"e9e70323","timestamp":"2026-07-16T09:58:28.114Z"}
{"type":"message","id":"8bfb36bf","parentId":"e42cc85c","timestamp":"2026-07-16T09:58:28.153Z","message":{"role":"toolResult","toolCallId":"call_O70pPpbNXJl2fKXt9236gIgY|fc_047bb49263dfa303016a58ab4395688198b1735a891c2b7338","toolName":"read","content":[{"type":"text","text":"    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\n        self.sessions\n            .lock()\n            .unwrap()\n            .get(&id)\n            .map(|h| recover_log(&h.log).next_seq)\n    }\n\n    /// Drive one brain connection: handshake, then dispatch its command frames\n    /// until it disconnects. On disconnect, detach this connection from any\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\n    /// restart must not disturb the hosted children).\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n        struct HandlerTally<'a>(&'a AtomicUsize);\n        impl Drop for HandlerTally<'_> {\n            fn drop(&mut self) {\n                self.0.fetch_sub(1, Ordering::AcqRel);\n            }\n        }\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\n        let _tally = HandlerTally(&self.conn_handlers);\n\n        let (mut recv, send) = conn.split();\n        // Round-4: the send half is owned by the bounded/cancelable conn\n        // primitive from birth — every write on this physical connection is\n        // deadline-bounded, and a wedge poisons + retires the conn.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        send.describe(\"role=brain\");\n\n        // Version handshake: this connection must be a brain (2.3).\n        recv_hello(&mut recv, Role::Brain)?;\n\n        let mut my_subs: Vec<u64> = Vec::new();\n        let mut my_stream_subs: Vec<u64> = Vec::new();\n        let mut my_presence_sub = false;\n\n        loop {\n            let env = match read_frame(&mut recv) {\n                Ok(e) => e,\n                // Any read failure (EOF included) ends the connection.\n                Err(_) => break,\n            };\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_UNSUBSCRIBE => {\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_INPUT => {\n                    if let Err(msg) = self.dispatch_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_RESIZE => {\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_KILL => {\n                    if let Err(msg) = self.dispatch_kill(env) {\n                        send_error(&send, &msg);\n                    }\n                }\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\n                KIND_ADAPTER_APPLY => {\n                    if let Err(msg) = self.dispatch_adapter_apply(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STATUS => self.dispatch_net_status(&send),\n                // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\n                KIND_BROKER_IMAGE => self.dispatch_broker_image(&send),\n                KIND_STALL_EVICTS => self.dispatch_stall_evicts(&send),\n                KIND_NET_DIAL => {\n                    if let Err(msg) = self.dispatch_net_dial(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_DIAL_LOOPBACK => {\n                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                // [impl->REQ-PUMP-PEER-ISOLATION]\n                KIND_NET_DIAL_SUBMIT => {\n                    if let Err(msg) = self.dispatch_net_dial_submit(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_JOIN => {\n                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_MEET => {\n                    if let Err(msg) = self.dispatch_pair_meet(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_CODE_SUBMIT => {\n                    if let Err(msg) = self.dispatch_pair_code_submit(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_OPEN => {\n                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_SEND => {\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAMS => {\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_SESSIONS => {\n                    // Clone the (id, endpoint, log) out from under the sessions\n                    // lock, then read each log's cursor under the log lock alone —\n                    // never both locks at once (the dispatch_subscribe discipline).\n                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {\n                        let sessions = recover(&self.sessions);\n                        sessions\n                            .iter()\n                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))\n                            .collect()\n                    };\n                    // Snapshot each session's truth under its OWN log lock (cursor +\n                    // controller identity + the control/viewer stamp truth), doing NO\n                    // file I/O under the lock (the lock-across-effect discipline, KH\n                    // 7.12/5.16). The convergence re-assert + the reply then run OFF\n                    // the lock.\n                    struct SessSnap {\n                        id: u64,\n                        endpoint: String,\n                        resume_seq: u64,\n                        controller_by: Option<String>,\n                        has_controller: bool,\n                        viewers: u32,\n                    }\n                    let snaps: Vec<SessSnap> = rows\n                        .into_iter()\n                        .map(|(id, endpoint, log)| {\n                            let mut log = recover_log(&log);\n                            // B-2 (REQ-CONTROLLER-LIVENESS-REAP): make the controller\n                            // truth HONEST before reading it — reap a dead-writer\n                            // (severed) controller so both the reply's controller_by\n                            // AND the off-lock converge_perch_stamps below see the\n                            // cleared state and the stale info.json stamp clears.\n                            let _ = log.reap_dead_controller();\n                            SessSnap {\n                                id,\n                                endpoint,\n                                resume_seq: log.delivered_through(),\n                                controller_by: log.controller_by().map(str::to_string),\n                                has_controller: log.has_controller(),\n                                viewers: log.live_viewer_count(),\n                            }\n                        })\n                        .collect();\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\n                    // each live session's control/viewer stamps to broker truth,\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\n                    // pre-bind); converges within one reconcile-poll window after bind.\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n                    for s in &snaps {\n                        converge_perch_stamps(\n                            &s.endpoint,\n                            s.controller_by.as_deref(),\n                            s.has_controller,\n                            s.viewers,\n                        );\n                    }\n                    let reply = SessionsReply {\n                        sessions: snaps\n                            .into_iter()\n                            .map(|s| SessionInfo {\n                                session_id: s.id,\n                                endpoint: s.endpoint,\n                                resume_seq: s.resume_seq,\n                                controller_by: s.controller_by,\n                            })\n                            .collect(),\n                    };\n                    let frame = Envelope::new(\n                        KIND_SESSIONS_REPLY,\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\n                    );\n                    send_frame(&send, &frame);\n                }\n                KIND_ENDPOINT_INPUT => {\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n                    Ok(id) => my_stream_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_NET_PRESENCE_SUBSCRIBE => {\n                    match self.dispatch_net_presence_subscribe(env, &send) {\n                        Ok(()) => my_presence_sub = true,\n                        Err(msg) => send_error(&send, &msg),\n                    }\n                }\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n                // An unknown kind is tolerated (forward-compat): report, don't die.\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n            }\n        }\n\n        // Brain gone: detach as subscriber but leave the sessions running.\n        let sessions = recover(&self.sessions);\n        for id in &my_subs {\n            if let Some(h) = sessions.get(id) {\n                recover_log(&h.log).detach_if(&send);\n            }\n        }\n        drop(sessions);\n        // Same for net streams: the streams + their rings persist (D4b).\n        if let Some(host) = self.net.get() {\n            for id in &my_stream_subs {\n                host.detach_stream_sub(*id, &send);\n            }\n            // And presence: the liveness log + its ring persist (D4c).\n            if my_presence_sub {\n                host.detach_presence_sub(&send);\n            }\n        }\n        Ok(())\n    }\n\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\n    /// connection, start its output drain and exit waiter, and reply `spawned`.\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\n    /// 7.4): a hung child blocks only its own threads — shell instances ride\n    /// exactly this isolation (one owner's hung shell binary stalls neither\n    /// another owner's delivery nor the broker's control dispatch).\n\n[Showing lines 2969-3223 of 6009. Use :3224 to continue]"}],"details":{"truncation":{"content":"        self.sessions\r\n            .lock()\r\n            .unwrap()\r\n            .get(&id)\r\n            .map(|h| recover_log(&h.log).next_seq)\r\n    }\r\n\r\n    /// Drive one brain connection: handshake, then dispatch its command frames\r\n    /// until it disconnects. On disconnect, detach this connection from any\r\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\r\n    /// restart must not disturb the hosted children).\r\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\r\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\r\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\r\n        struct HandlerTally<'a>(&'a AtomicUsize);\r\n        impl Drop for HandlerTally<'_> {\r\n            fn drop(&mut self) {\r\n                self.0.fetch_sub(1, Ordering::AcqRel);\r\n            }\r\n        }\r\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\r\n        let _tally = HandlerTally(&self.conn_handlers);\r\n\r\n        let (mut recv, send) = conn.split();\r\n        // Round-4: the send half is owned by the bounded/cancelable conn\r\n        // primitive from birth — every write on this physical connection is\r\n        // deadline-bounded, and a wedge poisons + retires the conn.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\r\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\r\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\r\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        send.describe(\"role=brain\");\r\n\r\n        // Version handshake: this connection must be a brain (2.3).\r\n        recv_hello(&mut recv, Role::Brain)?;\r\n\r\n        let mut my_subs: Vec<u64> = Vec::new();\r\n        let mut my_stream_subs: Vec<u64> = Vec::new();\r\n        let mut my_presence_sub = false;\r\n\r\n        loop {\r\n            let env = match read_frame(&mut recv) {\r\n                Ok(e) => e,\r\n                // Any read failure (EOF included) ends the connection.\r\n                Err(_) => break,\r\n            };\r\n            match env.kind.as_str() {\r\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_UNSUBSCRIBE => {\r\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_INPUT => {\r\n                    if let Err(msg) = self.dispatch_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_RESIZE => {\r\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_KILL => {\r\n                    if let Err(msg) = self.dispatch_kill(env) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n                KIND_ADAPTER_APPLY => {\r\n                    if let Err(msg) = self.dispatch_adapter_apply(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STATUS => self.dispatch_net_status(&send),\r\n                // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\r\n                KIND_BROKER_IMAGE => self.dispatch_broker_image(&send),\r\n                KIND_STALL_EVICTS => self.dispatch_stall_evicts(&send),\r\n                KIND_NET_DIAL => {\r\n                    if let Err(msg) = self.dispatch_net_dial(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_DIAL_LOOPBACK => {\r\n                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-PUMP-PEER-ISOLATION]\r\n                KIND_NET_DIAL_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_net_dial_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_JOIN => {\r\n                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_MEET => {\r\n                    if let Err(msg) = self.dispatch_pair_meet(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_CODE_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_pair_code_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_OPEN => {\r\n                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SEND => {\r\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAMS => {\r\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_SESSIONS => {\r\n                    // Clone the (id, endpoint, log) out from under the sessions\r\n                    // lock, then read each log's cursor under the log lock alone —\r\n                    // never both locks at once (the dispatch_subscribe discipline).\r\n                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {\r\n                        let sessions = recover(&self.sessions);\r\n                        sessions\r\n                            .iter()\r\n                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))\r\n                            .collect()\r\n                    };\r\n                    // Snapshot each session's truth under its OWN log lock (cursor +\r\n                    // controller identity + the control/viewer stamp truth), doing NO\r\n                    // file I/O under the lock (the lock-across-effect discipline, KH\r\n                    // 7.12/5.16). The convergence re-assert + the reply then run OFF\r\n                    // the lock.\r\n                    struct SessSnap {\r\n                        id: u64,\r\n                        endpoint: String,\r\n                        resume_seq: u64,\r\n                        controller_by: Option<String>,\r\n                        has_controller: bool,\r\n                        viewers: u32,\r\n                    }\r\n                    let snaps: Vec<SessSnap> = rows\r\n                        .into_iter()\r\n                        .map(|(id, endpoint, log)| {\r\n                            let mut log = recover_log(&log);\r\n                            // B-2 (REQ-CONTROLLER-LIVENESS-REAP): make the controller\r\n                            // truth HONEST before reading it — reap a dead-writer\r\n                            // (severed) controller so both the reply's controller_by\r\n                            // AND the off-lock converge_perch_stamps below see the\r\n                            // cleared state and the stale info.json stamp clears.\r\n                            let _ = log.reap_dead_controller();\r\n                            SessSnap {\r\n                                id,\r\n                                endpoint,\r\n                                resume_seq: log.delivered_through(),\r\n                                controller_by: log.controller_by().map(str::to_string),\r\n                                has_controller: log.has_controller(),\r\n                                viewers: log.live_viewer_count(),\r\n                            }\r\n                        })\r\n                        .collect();\r\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\r\n                    // each live session's control/viewer stamps to broker truth,\r\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\r\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\r\n                    // pre-bind); converges within one reconcile-poll window after bind.\r\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\n                    for s in &snaps {\r\n                        converge_perch_stamps(\r\n                            &s.endpoint,\r\n                            s.controller_by.as_deref(),\r\n                            s.has_controller,\r\n                            s.viewers,\r\n                        );\r\n                    }\r\n                    let reply = SessionsReply {\r\n                        sessions: snaps\r\n                            .into_iter()\r\n                            .map(|s| SessionInfo {\r\n                                session_id: s.id,\r\n                                endpoint: s.endpoint,\r\n                                resume_seq: s.resume_seq,\r\n                                controller_by: s.controller_by,\r\n                            })\r\n                            .collect(),\r\n                    };\r\n                    let frame = Envelope::new(\r\n                        KIND_SESSIONS_REPLY,\r\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\r\n                    );\r\n                    send_frame(&send, &frame);\r\n                }\r\n                KIND_ENDPOINT_INPUT => {\r\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\r\n                    Ok(id) => my_stream_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_NET_PRESENCE_SUBSCRIBE => {\r\n                    match self.dispatch_net_presence_subscribe(env, &send) {\r\n                        Ok(()) => my_presence_sub = true,\r\n                        Err(msg) => send_error(&send, &msg),\r\n                    }\r\n                }\r\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\r\n                // An unknown kind is tolerated (forward-compat): report, don't die.\r\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\r\n            }\r\n        }\r\n\r\n        // Brain gone: detach as subscriber but leave the sessions running.\r\n        let sessions = recover(&self.sessions);\r\n        for id in &my_subs {\r\n            if let Some(h) = sessions.get(id) {\r\n                recover_log(&h.log).detach_if(&send);\r\n            }\r\n        }\r\n        drop(sessions);\r\n        // Same for net streams: the streams + their rings persist (D4b).\r\n        if let Some(host) = self.net.get() {\r\n            for id in &my_stream_subs {\r\n                host.detach_stream_sub(*id, &send);\r\n            }\r\n            // And presence: the liveness log + its ring persist (D4c).\r\n            if my_presence_sub {\r\n                host.detach_presence_sub(&send);\r\n            }\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\r\n    /// connection, start its output drain and exit waiter, and reply `spawned`.\r\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\r\n    /// 7.4): a hung child blocks only its own threads — shell instances ride\r\n    /// exactly this isolation (one owner's hung shell binary stalls neither\r\n    /// another owner's delivery nor the broker's control dispatch).\r","truncated":true,"truncatedBy":"lines","totalLines":3041,"totalBytes":12383,"outputLines":255,"outputBytes":12383,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\n        self.sessions\r\n            .lock()\r\n            .unwrap()\r\n            .get(&id)\r\n            .map(|h| recover_log(&h.log).next_seq)\r\n    }\r\n\r\n    /// Drive one brain connection: handshake, then dispatch its command frames\r\n    /// until it disconnects. On disconnect, detach this connection from any\r\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\r\n    /// restart must not disturb the hosted children).\r\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\r\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\r\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\r\n        struct HandlerTally<'a>(&'a AtomicUsize);\r\n        impl Drop for HandlerTally<'_> {\r\n            fn drop(&mut self) {\r\n                self.0.fetch_sub(1, Ordering::AcqRel);\r\n            }\r\n        }\r\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\r\n        let _tally = HandlerTally(&self.conn_handlers);\r\n\r\n        let (mut recv, send) = conn.split();\r\n        // Round-4: the send half is owned by the bounded/cancelable conn\r\n        // primitive from birth — every write on this physical connection is\r\n        // deadline-bounded, and a wedge poisons + retires the conn.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\r\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\r\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\r\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        send.describe(\"role=brain\");\r\n\r\n        // Version handshake: this connection must be a brain (2.3).\r\n        recv_hello(&mut recv, Role::Brain)?;\r\n\r\n        let mut my_subs: Vec<u64> = Vec::new();\r\n        let mut my_stream_subs: Vec<u64> = Vec::new();\r\n        let mut my_presence_sub = false;\r\n\r\n        loop {\r\n            let env = match read_frame(&mut recv) {\r\n                Ok(e) => e,\r\n                // Any read failure (EOF included) ends the connection.\r\n                Err(_) => break,\r\n            };\r\n            match env.kind.as_str() {\r\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_UNSUBSCRIBE => {\r\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_INPUT => {\r\n                    if let Err(msg) = self.dispatch_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_RESIZE => {\r\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_KILL => {\r\n                    if let Err(msg) = self.dispatch_kill(env) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n                KIND_ADAPTER_APPLY => {\r\n                    if let Err(msg) = self.dispatch_adapter_apply(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STATUS => self.dispatch_net_status(&send),\r\n                // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\r\n                KIND_BROKER_IMAGE => self.dispatch_broker_image(&send),\r\n                KIND_STALL_EVICTS => self.dispatch_stall_evicts(&send),\r\n                KIND_NET_DIAL => {\r\n                    if let Err(msg) = self.dispatch_net_dial(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_DIAL_LOOPBACK => {\r\n                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-PUMP-PEER-ISOLATION]\r\n                KIND_NET_DIAL_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_net_dial_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_JOIN => {\r\n                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_MEET => {\r\n                    if let Err(msg) = self.dispatch_pair_meet(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_CODE_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_pair_code_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_OPEN => {\r\n                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SEND => {\r\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAMS => {\r\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_SESSIONS => {\r\n                    // Clone the (id, endpoint, log) out from under the sessions\r\n                    // lock, then read each log's cursor under the log lock alone —\r\n                    // never both locks at once (the dispatch_subscribe discipline).\r\n                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {\r\n                        let sessions = recover(&self.sessions);\r\n                        sessions\r\n                            .iter()\r\n                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))\r\n                            .collect()\r\n                    };\r\n                    // Snapshot each session's truth under its OWN log lock (cursor +\r\n                    // controller identity + the control/viewer stamp truth), doing NO\r\n                    // file I/O under the lock (the lock-across-effect discipline, KH\r\n                    // 7.12/5.16). The convergence re-assert + the reply then run OFF\r\n                    // the lock.\r\n                    struct SessSnap {\r\n                        id: u64,\r\n                        endpoint: String,\r\n                        resume_seq: u64,\r\n                        controller_by: Option<String>,\r\n                        has_controller: bool,\r\n                        viewers: u32,\r\n                    }\r\n                    let snaps: Vec<SessSnap> = rows\r\n                        .into_iter()\r\n                        .map(|(id, endpoint, log)| {\r\n                            let mut log = recover_log(&log);\r\n                            // B-2 (REQ-CONTROLLER-LIVENESS-REAP): make the controller\r\n                            // truth HONEST before reading it — reap a dead-writer\r\n                            // (severed) controller so both the reply's controller_by\r\n                            // AND the off-lock converge_perch_stamps below see the\r\n                            // cleared state and the stale info.json stamp clears.\r\n                            let _ = log.reap_dead_controller();\r\n                            SessSnap {\r\n                                id,\r\n                                endpoint,\r\n                                resume_seq: log.delivered_through(),\r\n                                controller_by: log.controller_by().map(str::to_string),\r\n                                has_controller: log.has_controller(),\r\n                                viewers: log.live_viewer_count(),\r\n                            }\r\n                        })\r\n                        .collect();\r\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\r\n                    // each live session's control/viewer stamps to broker truth,\r\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\r\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\r\n                    // pre-bind); converges within one reconcile-poll window after bind.\r\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\n                    for s in &snaps {\r\n                        converge_perch_stamps(\r\n                            &s.endpoint,\r\n                            s.controller_by.as_deref(),\r\n                            s.has_controller,\r\n                            s.viewers,\r\n                        );\r\n                    }\r\n                    let reply = SessionsReply {\r\n                        sessions: snaps\r\n                            .into_iter()\r\n                            .map(|s| SessionInfo {\r\n                                session_id: s.id,\r\n                                endpoint: s.endpoint,\r\n                                resume_seq: s.resume_seq,\r\n                                controller_by: s.controller_by,\r\n                            })\r\n                            .collect(),\r\n                    };\r\n                    let frame = Envelope::new(\r\n                        KIND_SESSIONS_REPLY,\r\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\r\n                    );\r\n                    send_frame(&send, &frame);\r\n                }\r\n                KIND_ENDPOINT_INPUT => {\r\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\r\n                    Ok(id) => my_stream_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_NET_PRESENCE_SUBSCRIBE => {\r\n                    match self.dispatch_net_presence_subscribe(env, &send) {\r\n                        Ok(()) => my_presence_sub = true,\r\n                        Err(msg) => send_error(&send, &msg),\r\n                    }\r\n                }\r\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\r\n                // An unknown kind is tolerated (forward-compat): report, don't die.\r\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\r\n            }\r\n        }\r\n\r\n        // Brain gone: detach as subscriber but leave the sessions running.\r\n        let sessions = recover(&self.sessions);\r\n        for id in &my_subs {\r\n            if let Some(h) = sessions.get(id) {\r\n                recover_log(&h.log).detach_if(&send);\r\n            }\r\n        }\r\n        drop(sessions);\r\n        // Same for net streams: the streams + their rings persist (D4b).\r\n        if let Some(host) = self.net.get() {\r\n            for id in &my_stream_subs {\r\n                host.detach_stream_sub(*id, &send);\r\n            }\r\n            // And presence: the liveness log + its ring persist (D4c).\r\n            if my_presence_sub {\r\n                host.detach_presence_sub(&send);\r\n            }\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\r\n    /// connection, start its output drain and exit waiter, and reply `spawned`.\r\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\r\n    /// 7.4): a hung child blocks only its own threads — shell instances ride\r\n    /// exactly this isolation (one owner's hung shell binary stalls neither\r\n    /// another owner's delivery nor the broker's control dispatch).\r","startLine":2968,"lineNumbers":[2968,2969,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3113,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156,3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,3170,3171,3172,3173,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3184,3185,3186,3187,3188,3189,3190,3191,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212,3213,3214,3215,3216,3217,3218,3219,3220,3221,3222,3223]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6009,"totalBytes":12383,"outputLines":255,"outputBytes":12383,"shownRange":{"start":2969,"end":3223},"nextOffset":3224}}},"isError":false,"timestamp":1784195908152}}
{"type":"message","id":"79f9e522","parentId":"8bfb36bf","timestamp":"2026-07-16T09:58:28.154Z","message":{"role":"user","content":"Your current interruptible wait was interrupted because an IRC message arrived from your parent agent `Main`.\n\nParent IRC message:\n\nLeading field/log hypothesis: controller conns are broker role=`brain stream-subscriber`; refresh closes old brain connection while hosted PTYs survive, and new brain only logs `BRAIN_RESUMED` session cursors—not active remote control stream subscriptions. Existing remote `spt rc` frontend then hangs until detach+reattach opens a new stream. Please confirm/refute exact call chain and explain partial 4/5 observation.","attribution":"agent","timestamp":1784195901828,"steering":true}}
{"type":"message","id":"3b45a216","parentId":"79f9e522","timestamp":"2026-07-16T09:58:40.120Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Clarifying brain cursor and stream handling**\n\n**Analyzing controller connection cleanup and attachment**"},{"type":"toolCall","id":"call_miSXEUQoLdPhtvWGKwF75a4s|fc_047bb49263dfa303016a58ab4f838481988d747d5a0aed7721","name":"read","arguments":{"i":"Reading serve attach state machine","path":"crates/spt-daemon/src/attach.rs","selector":"136-550"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":5619,"output":353,"cacheRead":136704,"cacheWrite":0,"totalTokens":142676,"reasoningTokens":311,"cost":{"input":0.028095000000000002,"output":0.01059,"cacheRead":0.068352,"cacheWrite":0,"total":0.107037}},"stopReason":"toolUse","timestamp":1784195908156,"responseId":"resp_047bb49263dfa303016a58ab44b7048198818e69f37855346d","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58ab4677808198a2520d455849f2a8","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKtP3W_fd6ugKTYv3YFFAZcw1NdfxnlNyhJ0wkHBk-P1BVkJ1Cjmp0QDuQcmtPhg_MlnElTKKEx0P-IF4buvllEG7ABbPWrO6WmquWcLsNK_ostraoR3xhRpDAx2MdDKL1sMdpoJ9HZUL8RQVRvLHceiiEWNPr-QAzRJZr0_toerEa4UY6t9OiKFBCgN_FPXAqvFQPExqSJ3tW6QHiByifpWg13QQsrqHtrU-k5C5ob3R01hbJjzrqB2R-woFcJQLFeLyUI_WS9yWU3tJ2OJgXVK_aZjty5F-DW3bBN3kigZ_1skPXhp7-CvWdpGRpOg5wS_p_LDNrU2_SlrabHPVSWBsZu0CO07WFEAQ3iKb4To0cxj8sFZnRaBzOlpC6FC4AvZrFXUsztM8Im-yAtOtNVeuwIqVoUiAauLnAEDWTPzEEFuG-ru6igkb11tMN3F0iIQV3kmYZHM7WpEFmXgZmc3pnYNpL0J6czmovsqh8gye4SqZfDNfcDi80jZVu5b3mKVOgVlUSJeKf8AJA1llqVn4zoi-bBUphokqQrbcqJw7wI5PnwYdaGPguGo3KNni2CfOgQW9_mjme8rlvOIGBLCTSiu-eTZJA4cRPvgkJ5bi_BDFrgPtum2eEMfAv9Avs6Rtp8TBXEllkkn0MA4P7HogsIwuyFy5l20aoZr2c2cXoujMYkkd_NCOgIXWstnxjEngwkXxTZdlkkRML5Lm_RXGIKb53QuuDsAazkm27n2y8xs1FSOL5T9uOh2TodOhJr2fYTXaNZD1SQY84CLDlh-L7Sf2vfeREdSa_2AH_ts152-eSTfb1BBMa0YElIwOKp6EsqLjNQ1HcEktY5J3dxgPksfH4zaHw55liv9kkB_oeyFnyQILse0RbsBPBMLUCEDFCdJ0ESnGmbv-akXQfq9qrAPGaoQTyS4jRiOJLz3nsSVmET-qnCpo_W4D1gtN1TH-8lumFJQ7g98rNT0VNW9fn8h5XJqG5YE5iBeCClFz4uioIz0Jad_Gao90guUNcXMHv2pIrzA4M_bKZ0_aVEmNNgwXKYbywZuc7xeT01iOb-O6AUyFtsskk-XhgFnuxIOv4KH3WHaisBeUrfwanc7fF9dr2wzpOi3HDnBoq6t1clPr3brZKdPRRKSEavBNSXwmhgnHFXn8vArFNF9RpxHeGKmWt8C1fCqT0UYXtZa0AQHDJuI5he-LCd3_nVfwJWgV8dZnDSMgwO_3URiU9w6eyoHQnVAV8zOwEU7qh3kJa2YJxswvSemq6oexWlCv_BDi8rRranSwGZt_Nd1m-72sSszHmzI0FHiPWx6YCo1QWf__K_IoXvmthqQRxfW50NEWBe5zyZ7_-uxJVCcZnmbaWzkt1OP5wGASvL873_KQF3hjq2_t1DHJbdXI5rlxoID4vpUDJehngXMVP1Z6L-2p00k_ucmdQ90MaCGTrATbYrfRXBwH3CVYuwCtXIQIiUvlY14UVn52ckB3jhWuj8VJMh0Fyz-5g9vcRqJCatoIyi61gFz-AWF3wxY73FTOk8jlId7MXzG6RYuACDXxdbMlzcL5kWURs6Tr7SJmtSYkjIM_gHW4VKQ92wzXlsjilwLFCZ-IKBWc9ferY_f08hPb782G6tFyTus9Cu-4uhkELfeGd30QnOEXA5hQsks_ERS9-Vs_P9VLecOb1QvWcEdjsKihOMF8EZ9hPBpwHU-GSlb1wTxi9Ewx_hFma8lI0_vcS_AsCfixiuh1G3xAkxKvu9huAB1JRaG-dfuItlmj0OQNACjC5FqBPdLWthp_ywiLnlJFkBIM5gYp5qZO0jU4dLeKIBnzNd1Zg5vp5fPVbPbvgaldf5YT49dmPCBZ5c77AA73RiGQ1MMr6wGqFO7xiPfSizmZ6vYe_nS6Z-t9xmGnVydcrO2dDdBoc-b9g90QlrWGTVTXL8hp_9_gzGjC_OCjHQNwpjN19_wmy6o6_MvQ7WtnVaAscPe4SG81he51_dCRawe41ANLozD0Va4U-0UOY70YGOO79eTvzs71klo189kZLTNjKATaLtz1O469F-wZMOb0T_5_8gRV28FE5pNOAyQNTyp6Mr13Knn6JmvJjDuOxc1zRs_BtOb7DOwnVF8kp6-NhnmDPOzklRNdP7jWQ7Kjg6i7pBZw5x00pTe-54sEv5BmHICpBldwFRHsSnpshzRN7Bf4DTabOEjmcT57LF2beB2a0nmbiF30MKykyvnN5Q-RFGO87HErPnitCrY0jEgsllbhApjDQaTPyAj3g8J08RQfdlHPMhBKcvR2EHQc7I7AI4YmkbTtxPF1Xb_8emujqB11Uxansf9jLqLSkympM-hkosAS3eI3T5Ir0HbZDEg_lXVEfB0DmMMGkacHW_iIRrvA0xBPVK3O2yuntNl3b-jDflhK4-4N1wRqwsQrm1moWYjZY5LoXKR1y5QB59hLxZFLiuqtXoJ4yIitRyesRjprGWiVOj10E5DMtKlr5tpoMYebWfGW5NCx-xWDA7jC-0kktfGg7Z7DYry8Zw7xF7N7OndkdkXXwuqKM-FFRE12mM9L9KGVdph0djyqJM_RAdl9DJkkVNNKtUpSU-lAZk3bKQI2qcoaCIKTYTOdqS4lS18nxv8xEH8FWJqCWpiHYu9mYhmdf5B-9-npGWuPrwsS1SqE_-FjZ5g_uHgG2r4QX8HcQBi3ZvdcZ9CnF4AEnk-3qARj4Zn3bgxHwZfZnxqjkGI4dLlEKEowcuju2NgfEDZFEDu5gDzJ5mVZSuKZ7g5vEGqiqLJXl18zDRDt3yTc5tmdps06GorkTWgCpoP1oipNMYmQ8PbQ4V9O7ZGaquXO_npU11uqTb_UM838XwRIhBBBGgJ_7m7Ptnd5owF1DoUvZRP8qAn-3ZjEojtbgayfVwTbz29hjuMzWykqLKyLRv10fzC1iUGBTkjJjVBSmCnDKfM1mlY","summary":[{"type":"summary_text","text":"**Clarifying brain cursor and stream handling**"},{"type":"summary_text","text":"**Analyzing controller connection cleanup and attachment**"}]},{"id":"fc_047bb49263dfa303016a58ab4f838481988d747d5a0aed7721","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading serve attach state machine\",\"path\":\"crates/spt-daemon/src/attach.rs\",\"selector\":\"136-550\"}","call_id":"call_miSXEUQoLdPhtvWGKwF75a4s","name":"read"}]},"duration":11963.591199999675,"ttft":2286.408499999903,"contextSnapshot":{"promptTokens":142323,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_miSXEUQoLdPhtvWGKwF75a4s|fc_047bb49263dfa303016a58ab4f838481988d747d5a0aed7721","toolName":"read","startedAt":"2026-07-16T09:58:40.121Z","args":{"path":"crates/spt-daemon/src/attach.rs"},"intent":"Reading serve attach state machine"},"id":"0f01a332","parentId":"3b45a216","timestamp":"2026-07-16T09:58:40.121Z"}
{"type":"message","id":"584800ba","parentId":"0f01a332","timestamp":"2026-07-16T09:58:40.127Z","message":{"role":"toolResult","toolCallId":"call_miSXEUQoLdPhtvWGKwF75a4s|fc_047bb49263dfa303016a58ab4f838481988d747d5a0aed7721","toolName":"read","content":[{"type":"text","text":"\n/// Read one [`BrokerEvent`] for an attached CONTROLLER, RESUMING-FROM-FLOOR on a\n/// b4 drop-don't-block output-gap instead of fataling.\n///\n/// b4 made the controller a non-blocking `try_send` that DROPS frames when its\n/// bounded channel is full (a controller that falls behind its own echo under a\n/// hard flood). The next frame this reader sees is then a FORWARD gap that the\n/// strict reject-gap (`read_event`, legacy single-session path) FATALS. A\n/// controller CANNOT snap-above like a viewer (B2: it is authoritative — it\n/// advances `delivered_through`; skipping rolled frames would be a not-exactly-once\n/// resume). So instead it RE-FETCHES: re-subscribe from\n/// [`Brain::controller_resume_floor`] (`= delivered_through`, frozen at the drop by\n/// `contiguous_advance`) and the broker replays the dropped frames from the ring.\n///\n/// Exactly-once holds IFF the ring still retains the floor (`tail - delivered_through\n/// <= cap_chunks`). If it does not — the controller fell behind further than the\n/// ring holds — the re-subscribe's first frame is STILL a forward gap (floor\n/// unchanged across two resumes). That is [`ControllerIrrecoverablyBehind`]: the\n/// caller surfaces it (marked truncation), never silent-skips (B2 lie) and never\n/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\n/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\n/// Size / Exit / …) pass through unchanged.\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\npub fn read_controller_event_resuming(\n    brain: &mut Brain,\n    session_id: u64,\n    by: Option<&str>,\n) -> io::Result<BrokerEvent> {\n    let mut last_resume_floor: Option<u64> = None;\n    let mut swallow_resume_ack = false;\n    loop {\n        match brain.read_event() {\n            // Our own re-subscribe's Subscribed reply: swallow it (role is already\n            // resolved) so it cannot mask a repeated gap as forward progress.\n            Ok(BrokerEvent::Subscribed { .. }) if swallow_resume_ack => {\n                swallow_resume_ack = false;\n                continue;\n            }\n            Ok(ev) => return Ok(ev),\n            Err(e) if is_output_gap(&e) => {\n                let floor = brain.controller_resume_floor();\n                if last_resume_floor == Some(floor) {\n                    // No progress across two resumes → the ring rolled past the\n                    // floor → the dropped frames are gone.\n                    return Err(io::Error::other(ControllerIrrecoverablyBehind { floor }));\n                }\n                last_resume_floor = Some(floor);\n                // RE-FETCH from the frozen delivered_through (same operator `by` →\n                // a silent re-take, no Displaced); the broker replays the dropped\n                // frames from the ring.\n                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\n                swallow_resume_ack = true;\n            }\n            Err(e) => return Err(e),\n        }\n    }\n}\n\n/// Resolve an endpoint label to THIS node's local broker session id (the spawn's\n/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\n/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\n/// its own session table, so a remote operator — which cannot know our local\n/// session id — names the endpoint and we map it here, never trusting a wire\n/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n/// (a stale registry row routed us an attach for something gone → the caller\n/// refuses cleanly, D6).\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n    brain.sessions().ok().and_then(|reply| {\n        reply\n            .sessions\n            .into_iter()\n            .find(|s| s.endpoint == endpoint)\n            .map(|s| s.session_id)\n    })\n}\n\n/// Serve one attach stream against this brain's broker, blocking until the\n/// session exits, the operator detaches, or the gate refuses.\n///\n/// `origin_node` MUST be the handshake-proven remote id from the broker's\n/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes\n/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\n/// is being attached, for the gate. `net_from_seq` is this brain's resume\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n/// journal, and re-pumped output dedups at the operator's render cursor).\n///\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n/// round-trip inside the event loop would swallow interleaved attach records\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\n/// the wire `session_id`.\n// [impl->REQ-INST-8]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub fn serve_attach(\n    brain: &mut Brain,\n    broker_name: &str,\n    stream_id: u64,\n    endpoint: &str,\n    origin_node: &str,\n    net_from_seq: u64,\n    remote_session: Option<u64>,\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    // 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    // 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    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 = match (role, attached_session) {\n            (Some(ServeRole::Controller), Some(sid)) => {\n                match read_controller_event_resuming(brain, sid, 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                        let _ = wire.net_stream_send(stream_id, &line, None, true);\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                    Err(e) => {\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                }\n            }\n            _ => brain.read_event()?,\n        };\n        match event {\n            // ── Wire → session: requests, input, resize. ───────────────────\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Request {\n                            session_id,\n                            from_seq,\n                            intent: req_intent,\n                            // The dispatcher already resolved endpoint_id→session\n                            // (passed as remote_session) BEFORE this loop; ignore\n                            // the wire copy here (a mid-loop re-resolve would swallow\n                            // interleaved attach records — see remote_session docs).\n                            endpoint_id: _,\n                        } => {\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\n                            // the transport-proven origin. Viewing is gated\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\n                            // watching reveals full session contents).\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\n                                == AccessDecision::Deny\n                            {\n                                wire.net_stream_send(stream_id, &[], None, true)?;\n                                return Ok(AttachServeOutcome::Refused);\n                            }\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\n                            // dispatcher-resolved session id (the owning node is\n                            // authoritative for its own table); the LOCAL path uses\n                            // the wire session_id unchanged.\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\n                            let sid = remote_session.unwrap_or(session_id);\n                            intent = req_intent;\n                            attached_session = Some(sid);\n                            // Role-aware subscribe (REQ-RCVIEW-1): the broker\n                            // resolves controller / viewer / busy / take by the\n                            // intent + this handshake-proven origin, and replies a\n                            // `Subscribed`. driven_by / viewer_count are now\n                            // BROKER-owned (single writer — resolves the\n                            // clear-race), so this loop no longer stamps them.\n                            // A VIEWER attach ARMS snap-above at the floor so a\n                            // PRE-eviction ring-roll forward Output gap (the ring\n                            // rolled frames out under flood with NO eviction\n                            // marker) SNAPS to live instead of fataling the\n                            // legacy reject-gap → the viewer keeps forwarding.\n                            // The CONTROLLER keeps strict reject-gap (exactly-once\n                            // resume, B2). Composes with the POST-eviction\n                            // ViewerEvicted skip-to-live arm below.\n                            // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\n                            if req_intent == AttachIntent::Viewer {\n                                brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\n                            } else {\n                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\n                            }\n                            attached = true;\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                                    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                            // Controller → PTY resize (controller-EXCLUSIVE; the\n                            // broker rejects it on a viewer's stream). A viewer's\n                            // Resize is dropped here by the intent gate too.\n                            let _ = brain.resize(rows, cols);\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, .. } => {\n                use crate::msg::SubscribeOutcome as O;\n                match outcome {\n                    O::Controller | O::TookControl => {\n                        role = Some(ServeRole::Controller);\n                        // A driver attached — the wake edge (controller-only;\n                        // idempotent on an already-active seat).\n                        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                        for (op, b) in pending_input.drain(..) {\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\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            }\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\n            BrokerEvent::Size { rows, cols, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\n                wire.net_stream_send(stream_id, &line, None, false)?;\n            }\n            // This controller was kicked by a different operator's `--take`.\n            BrokerEvent::Displaced { by, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\n                wire.net_stream_send(stream_id, &line, None, true)?;\n                // No dormant edge: the endpoint stays driven by the taker (whose\n                // own serve loop already fired the wake).\n                return Ok(AttachServeOutcome::Displaced);\n            }\n\n            // This VIEWER overflowed its broker subscription queue and was evicted\n            // (it forwards slower than the drain fans out under flood). SKIP TO\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\n            // spin. The next live Output carries a forward seq jump; the operator's\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n            BrokerEvent::ViewerEvicted { session_id: sid }\n                if attached && Some(sid) == attached_session =>\n            {\n                if let Some(prev) = last_resubscribe {\n                    let since = prev.elapsed();\n                    if since < RESUBSCRIBE_INTERVAL {\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\n                    }\n                }\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\n                last_resubscribe = Some(Instant::now());\n            }\n\n            // The operator closed its send side: the viewport is over.\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                wire.net_stream_send(stream_id, &[], None, true)?;\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\n                // info.json, carrying the current driven_by through), THEN release\n                // the role over IPC so the broker's driven_by=None is the LAST\n                // write — otherwise a concurrent resting RMW restores the stale\n                // marker (the clear-race the broker-single-writer model averts).\n                if role == Some(ServeRole::Controller) {\n                    // The driver detached — the endpoint rests warm (controller-\n                    // only; a viewer leaving is wake-neutral).\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                if let Some(sid) = attached_session {\n                    let _ = brain.detach_session(sid); // release the role (clears markers)\n                }\n                return Ok(AttachServeOutcome::Detached);\n            }\n\n            // ── Session → wire: output and exit. ────────────────────────────\n            BrokerEvent::Output { seq, bytes, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Output {\n                    seq,\n                    data_b64: encode_bytes(&bytes),\n                });\n                // Unjournaled by design (see module docs): `seq` rides inside\n                // the record, and the operator's render cursor is the dedup.\n                wire.net_stream_send(stream_id, &line, None, false)?;\n            }\n            BrokerEvent::Exit { code, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Exit { code });\n                wire.net_stream_send(stream_id, &line, None, true)?;\n                if role == Some(ServeRole::Controller) {\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                return Ok(AttachServeOutcome::Exited(code));\n            }\n\n            BrokerEvent::Error { message } => {\n                if role == Some(ServeRole::Controller) {\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                return Err(io::Error::other(message));\n            }\n            // Applied/NetSent acks and unrelated events: not ours to handle.\n            _ => {}\n        }\n    }\n}\n\n/// Operator-side: open an attach stream on a dialed connection and send the\n/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\n/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\n/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\n/// durable id for the exactly-once stream open (safe to block for its ack —\n/// no attach traffic can be in flight before the stream exists); the Request\n/// send itself is unjournaled (see module docs — a replayed Request re-gates\n/// and re-attaches idempotently).\n// [impl->REQ-INST-8]\npub fn request_attach(\n…\n) -> io::Result<u64> {\n…\n}\n\n[Showing lines 135-553 of 627. Use :554 to continue]"}],"details":{"truncation":{"content":"\r\n/// Read one [`BrokerEvent`] for an attached CONTROLLER, RESUMING-FROM-FLOOR on a\r\n/// b4 drop-don't-block output-gap instead of fataling.\r\n///\r\n/// b4 made the controller a non-blocking `try_send` that DROPS frames when its\r\n/// bounded channel is full (a controller that falls behind its own echo under a\r\n/// hard flood). The next frame this reader sees is then a FORWARD gap that the\r\n/// strict reject-gap (`read_event`, legacy single-session path) FATALS. A\r\n/// controller CANNOT snap-above like a viewer (B2: it is authoritative — it\r\n/// advances `delivered_through`; skipping rolled frames would be a not-exactly-once\r\n/// resume). So instead it RE-FETCHES: re-subscribe from\r\n/// [`Brain::controller_resume_floor`] (`= delivered_through`, frozen at the drop by\r\n/// `contiguous_advance`) and the broker replays the dropped frames from the ring.\r\n///\r\n/// Exactly-once holds IFF the ring still retains the floor (`tail - delivered_through\r\n/// <= cap_chunks`). If it does not — the controller fell behind further than the\r\n/// ring holds — the re-subscribe's first frame is STILL a forward gap (floor\r\n/// unchanged across two resumes). That is [`ControllerIrrecoverablyBehind`]: the\r\n/// caller surfaces it (marked truncation), never silent-skips (B2 lie) and never\r\n/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\r\n/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\r\n/// Size / Exit / …) pass through unchanged.\r\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\npub fn read_controller_event_resuming(\r\n    brain: &mut Brain,\r\n    session_id: u64,\r\n    by: Option<&str>,\r\n) -> io::Result<BrokerEvent> {\r\n    let mut last_resume_floor: Option<u64> = None;\r\n    let mut swallow_resume_ack = false;\r\n    loop {\r\n        match brain.read_event() {\r\n            // Our own re-subscribe's Subscribed reply: swallow it (role is already\r\n            // resolved) so it cannot mask a repeated gap as forward progress.\r\n            Ok(BrokerEvent::Subscribed { .. }) if swallow_resume_ack => {\r\n                swallow_resume_ack = false;\r\n                continue;\r\n            }\r\n            Ok(ev) => return Ok(ev),\r\n            Err(e) if is_output_gap(&e) => {\r\n                let floor = brain.controller_resume_floor();\r\n                if last_resume_floor == Some(floor) {\r\n                    // No progress across two resumes → the ring rolled past the\r\n                    // floor → the dropped frames are gone.\r\n                    return Err(io::Error::other(ControllerIrrecoverablyBehind { floor }));\r\n                }\r\n                last_resume_floor = Some(floor);\r\n                // RE-FETCH from the frozen delivered_through (same operator `by` →\r\n                // a silent re-take, no Displaced); the broker replays the dropped\r\n                // frames from the ring.\r\n                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\r\n                swallow_resume_ack = true;\r\n            }\r\n            Err(e) => return Err(e),\r\n        }\r\n    }\r\n}\r\n\r\n/// Resolve an endpoint label to THIS node's local broker session id (the spawn's\r\n/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\r\n/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\r\n/// its own session table, so a remote operator — which cannot know our local\r\n/// session id — names the endpoint and we map it here, never trusting a wire\r\n/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\r\n/// (a stale registry row routed us an attach for something gone → the caller\r\n/// refuses cleanly, D6).\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\r\n    brain.sessions().ok().and_then(|reply| {\r\n        reply\r\n            .sessions\r\n            .into_iter()\r\n            .find(|s| s.endpoint == endpoint)\r\n            .map(|s| s.session_id)\r\n    })\r\n}\r\n\r\n/// Serve one attach stream against this brain's broker, blocking until the\r\n/// session exits, the operator detaches, or the gate refuses.\r\n///\r\n/// `origin_node` MUST be the handshake-proven remote id from the broker's\r\n/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes\r\n/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\r\n/// is being attached, for the gate. `net_from_seq` is this brain's resume\r\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\r\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\r\n/// journal, and re-pumped output dedups at the operator's render cursor).\r\n///\r\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\r\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\r\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\r\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\r\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\r\n/// round-trip inside the event loop would swallow interleaved attach records\r\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\r\n/// the wire `session_id`.\r\n// [impl->REQ-INST-8]\r\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub fn serve_attach(\r\n    brain: &mut Brain,\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    endpoint: &str,\r\n    origin_node: &str,\r\n    net_from_seq: u64,\r\n    remote_session: Option<u64>,\r\n) -> io::Result<AttachServeOutcome> {\r\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\r\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\r\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\r\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\r\n    // session output (broker→serve) — including the controller's synchronous,\r\n    // inline, multi-frame ring replay (`become_controller`), which holds the\r\n    // broker's single-threaded per-conn handler for the whole replay. If the\r\n    // forwards rode `brain` too, that stuck handler could not read them →\r\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\r\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\r\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\r\n    // two directions on independent single-threaded handlers cannot block each\r\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\r\n    // a second conn is correct. Order is preserved: this one serve thread reads\r\n    // every Output in seq order on `brain` and forwards it in that same order on\r\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\r\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\r\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\r\n    // the controller onto that async path would remove the inline-replay block at\r\n    // the source, but it touches the authoritative cursor-advancing path.)\r\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut attached = false;\r\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\r\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\r\n    // race on the input gate).\r\n    let mut intent = AttachIntent::Control;\r\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\r\n    // controller input that races ahead of the reply is BUFFERED, not dropped\r\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\r\n    // we know we control the session (the busy-refuse case).\r\n    let mut role: Option<ServeRole> = None;\r\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\r\n    // The attached session — captured from the Request so the terminal paths can\r\n    // explicitly release this worker's role (clearing driven_by / viewer_count\r\n    // promptly, not only on connection drop).\r\n    let mut attached_session: Option<u64> = None;\r\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\r\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\r\n    // intermittent live bursts, never a CPU spin.\r\n    let mut last_resubscribe: Option<Instant> = None;\r\n\r\n    loop {\r\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\r\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\r\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\r\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\r\n        // role resolves no Output flows, so a plain read is correct then.\r\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\n        let event = match (role, attached_session) {\r\n            (Some(ServeRole::Controller), Some(sid)) => {\r\n                match read_controller_event_resuming(brain, sid, Some(origin_node)) {\r\n                    Ok(ev) => ev,\r\n                    Err(e)\r\n                        if e.get_ref()\r\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\r\n                    {\r\n                        // Irrecoverable: the dropped frames rolled out of the ring.\r\n                        // SURFACE a marked truncation to the operator (never a\r\n                        // silent skip = B2 lie, never a spin), then end cleanly.\r\n                        // Graceful continuation is deferred\r\n                        // (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND).\r\n                        let notice =\r\n                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\r\n                        let line = ndjson::encode_line(&AttachRecord::Output {\r\n                            seq: brain.controller_resume_floor(),\r\n                            data_b64: encode_bytes(notice),\r\n                        });\r\n                        let _ = wire.net_stream_send(stream_id, &line, None, true);\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                        return Err(e);\r\n                    }\r\n                    Err(e) => {\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                        return Err(e);\r\n                    }\r\n                }\r\n            }\r\n            _ => brain.read_event()?,\r\n        };\r\n        match event {\r\n            // ── Wire → session: requests, input, resize. ───────────────────\r\n            BrokerEvent::NetStreamData {\r\n                stream_id: sid,\r\n                bytes,\r\n                ..\r\n            } if sid == stream_id => {\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                            intent: req_intent,\r\n                            // The dispatcher already resolved endpoint_id→session\r\n                            // (passed as remote_session) BEFORE this loop; ignore\r\n                            // the wire copy here (a mid-loop re-resolve would swallow\r\n                            // interleaved attach records — see remote_session docs).\r\n                            endpoint_id: _,\r\n                        } => {\r\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\r\n                            // the transport-proven origin. Viewing is gated\r\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\r\n                            // watching reveals full session contents).\r\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\r\n                                == AccessDecision::Deny\r\n                            {\r\n                                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                                return Ok(AttachServeOutcome::Refused);\r\n                            }\r\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\r\n                            // dispatcher-resolved session id (the owning node is\r\n                            // authoritative for its own table); the LOCAL path uses\r\n                            // the wire session_id unchanged.\r\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n                            let sid = remote_session.unwrap_or(session_id);\r\n                            intent = req_intent;\r\n                            attached_session = Some(sid);\r\n                            // Role-aware subscribe (REQ-RCVIEW-1): the broker\r\n                            // resolves controller / viewer / busy / take by the\r\n                            // intent + this handshake-proven origin, and replies a\r\n                            // `Subscribed`. driven_by / viewer_count are now\r\n                            // BROKER-owned (single writer — resolves the\r\n                            // clear-race), so this loop no longer stamps them.\r\n                            // A VIEWER attach ARMS snap-above at the floor so a\r\n                            // PRE-eviction ring-roll forward Output gap (the ring\r\n                            // rolled frames out under flood with NO eviction\r\n                            // marker) SNAPS to live instead of fataling the\r\n                            // legacy reject-gap → the viewer keeps forwarding.\r\n                            // The CONTROLLER keeps strict reject-gap (exactly-once\r\n                            // resume, B2). Composes with the POST-eviction\r\n                            // ViewerEvicted skip-to-live arm below.\r\n                            // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\r\n                            if req_intent == AttachIntent::Viewer {\r\n                                brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\r\n                            } else {\r\n                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\r\n                            }\r\n                            attached = true;\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                            match role {\r\n                                // Confirmed controller — exactly-once at the PTY\r\n                                // (the operator-minted id rides the B5 journal). NO-ACK\r\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\r\n                                // fire-and-forward (the ack is discarded here) and can\r\n                                // flood N input frames on this one conn; an acked write\r\n                                // back would fill the return direction and deadlock the\r\n                                // broker's per-conn handler. Dedup still holds at the\r\n                                // broker (op_id applied-set).\r\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n                                Some(ServeRole::Controller) => {\r\n                                    // The op_id is operator-minted (rc attach OpMinter),\r\n                                    // so it carries the `rc` journal tag (ADR-0034).\r\n                                    brain.send_effect_no_ack(\r\n                                        MintedOp::new(Minter::Rc, op_id),\r\n                                        &bytes,\r\n                                    )?\r\n                                }\r\n                                // Outcome not yet known — buffer; flushed on a\r\n                                // controller confirmation, discarded on busy/viewer.\r\n                                None => pending_input.push((op_id, bytes)),\r\n                                // A viewer never reaches here (intent gate).\r\n                                Some(ServeRole::Viewer) => {}\r\n                            }\r\n                        }\r\n                        AttachRecord::Resize { rows, cols }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\r\n                            // broker rejects it on a viewer's stream). A viewer's\r\n                            // Resize is dropped here by the intent gate too.\r\n                            let _ = brain.resize(rows, cols);\r\n                        }\r\n                        // Input/Resize before Request, a viewer's input/resize,\r\n                        // or target-direction records echoed back: noise — ignore.\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n\r\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\r\n            BrokerEvent::Subscribed { outcome, .. } => {\r\n                use crate::msg::SubscribeOutcome as O;\r\n                match outcome {\r\n                    O::Controller | O::TookControl => {\r\n                        role = Some(ServeRole::Controller);\r\n                        // A driver attached — the wake edge (controller-only;\r\n                        // idempotent on an already-active seat).\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\r\n                        // Flush any input that raced ahead of the reply (operator-\r\n                        // minted ops carry the `rc` tag).\r\n                        for (op, b) in pending_input.drain(..) {\r\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\r\n                        }\r\n                    }\r\n                    O::Viewer => {\r\n                        role = Some(ServeRole::Viewer);\r\n                        // Wake-neutral: a viewer never wakes the endpoint, never\r\n                        // keeps it awake (Gap B). Discard any racing input.\r\n                        pending_input.clear();\r\n                    }\r\n                    O::BusyControlled { .. } => {\r\n                        // A different operator controls it — refuse (no PTY input\r\n                        // ever reached it; the buffer is discarded). The rc\r\n                        // renders the busy guidance client-side from driven_by.\r\n                        wire.net_stream_send(stream_id, &[], None, true)?;\r\n                        return Ok(AttachServeOutcome::Refused);\r\n                    }\r\n                }\r\n            }\r\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\r\n            BrokerEvent::Size { rows, cols, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            // This controller was kicked by a different operator's `--take`.\r\n            BrokerEvent::Displaced { by, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                // No dormant edge: the endpoint stays driven by the taker (whose\r\n                // own serve loop already fired the wake).\r\n                return Ok(AttachServeOutcome::Displaced);\r\n            }\r\n\r\n            // This VIEWER overflowed its broker subscription queue and was evicted\r\n            // (it forwards slower than the drain fans out under flood). SKIP TO\r\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\r\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\r\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\r\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\r\n            // spin. The next live Output carries a forward seq jump; the operator's\r\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\r\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\r\n            BrokerEvent::ViewerEvicted { session_id: sid }\r\n                if attached && Some(sid) == attached_session =>\r\n            {\r\n                if let Some(prev) = last_resubscribe {\r\n                    let since = prev.elapsed();\r\n                    if since < RESUBSCRIBE_INTERVAL {\r\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\r\n                    }\r\n                }\r\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\r\n                last_resubscribe = Some(Instant::now());\r\n            }\r\n\r\n            // The operator closed its send side: the viewport is over.\r\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\r\n                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\r\n                // info.json, carrying the current driven_by through), THEN release\r\n                // the role over IPC so the broker's driven_by=None is the LAST\r\n                // write — otherwise a concurrent resting RMW restores the stale\r\n                // marker (the clear-race the broker-single-writer model averts).\r\n                if role == Some(ServeRole::Controller) {\r\n                    // The driver detached — the endpoint rests warm (controller-\r\n                    // only; a viewer leaving is wake-neutral).\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                if let Some(sid) = attached_session {\r\n                    let _ = brain.detach_session(sid); // release the role (clears markers)\r\n                }\r\n                return Ok(AttachServeOutcome::Detached);\r\n            }\r\n\r\n            // ── Session → wire: output and exit. ────────────────────────────\r\n            BrokerEvent::Output { seq, bytes, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Output {\r\n                    seq,\r\n                    data_b64: encode_bytes(&bytes),\r\n                });\r\n                // Unjournaled by design (see module docs): `seq` rides inside\r\n                // the record, and the operator's render cursor is the dedup.\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            BrokerEvent::Exit { code, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Exit { code });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Ok(AttachServeOutcome::Exited(code));\r\n            }\r\n\r\n            BrokerEvent::Error { message } => {\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Err(io::Error::other(message));\r\n            }\r\n            // Applied/NetSent acks and unrelated events: not ours to handle.\r\n            _ => {}\r\n        }\r\n    }\r\n}\r\n\r\n/// Operator-side: open an attach stream on a dialed connection and send the\r\n/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\r\n/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\r\n/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\r\n/// durable id for the exactly-once stream open (safe to block for its ack —\r\n/// no attach traffic can be in flight before the stream exists); the Request\r\n/// send itself is unjournaled (see module docs — a replayed Request re-gates\r\n/// and re-attaches idempotently).\r\n// [impl->REQ-INST-8]\r\npub fn request_attach(\r","truncated":true,"truncatedBy":"lines","totalLines":493,"totalBytes":24662,"outputLines":419,"outputBytes":24662,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"\r\n/// Read one [`BrokerEvent`] for an attached CONTROLLER, RESUMING-FROM-FLOOR on a\r\n/// b4 drop-don't-block output-gap instead of fataling.\r\n///\r\n/// b4 made the controller a non-blocking `try_send` that DROPS frames when its\r\n/// bounded channel is full (a controller that falls behind its own echo under a\r\n/// hard flood). The next frame this reader sees is then a FORWARD gap that the\r\n/// strict reject-gap (`read_event`, legacy single-session path) FATALS. A\r\n/// controller CANNOT snap-above like a viewer (B2: it is authoritative — it\r\n/// advances `delivered_through`; skipping rolled frames would be a not-exactly-once\r\n/// resume). So instead it RE-FETCHES: re-subscribe from\r\n/// [`Brain::controller_resume_floor`] (`= delivered_through`, frozen at the drop by\r\n/// `contiguous_advance`) and the broker replays the dropped frames from the ring.\r\n///\r\n/// Exactly-once holds IFF the ring still retains the floor (`tail - delivered_through\r\n/// <= cap_chunks`). If it does not — the controller fell behind further than the\r\n/// ring holds — the re-subscribe's first frame is STILL a forward gap (floor\r\n/// unchanged across two resumes). That is [`ControllerIrrecoverablyBehind`]: the\r\n/// caller surfaces it (marked truncation), never silent-skips (B2 lie) and never\r\n/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\r\n/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\r\n/// Size / Exit / …) pass through unchanged.\r\n// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\npub fn read_controller_event_resuming(\r\n    brain: &mut Brain,\r\n    session_id: u64,\r\n    by: Option<&str>,\r\n) -> io::Result<BrokerEvent> {\r\n    let mut last_resume_floor: Option<u64> = None;\r\n    let mut swallow_resume_ack = false;\r\n    loop {\r\n        match brain.read_event() {\r\n            // Our own re-subscribe's Subscribed reply: swallow it (role is already\r\n            // resolved) so it cannot mask a repeated gap as forward progress.\r\n            Ok(BrokerEvent::Subscribed { .. }) if swallow_resume_ack => {\r\n                swallow_resume_ack = false;\r\n                continue;\r\n            }\r\n            Ok(ev) => return Ok(ev),\r\n            Err(e) if is_output_gap(&e) => {\r\n                let floor = brain.controller_resume_floor();\r\n                if last_resume_floor == Some(floor) {\r\n                    // No progress across two resumes → the ring rolled past the\r\n                    // floor → the dropped frames are gone.\r\n                    return Err(io::Error::other(ControllerIrrecoverablyBehind { floor }));\r\n                }\r\n                last_resume_floor = Some(floor);\r\n                // RE-FETCH from the frozen delivered_through (same operator `by` →\r\n                // a silent re-take, no Displaced); the broker replays the dropped\r\n                // frames from the ring.\r\n                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\r\n                swallow_resume_ack = true;\r\n            }\r\n            Err(e) => return Err(e),\r\n        }\r\n    }\r\n}\r\n\r\n/// Resolve an endpoint label to THIS node's local broker session id (the spawn's\r\n/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\r\n/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\r\n/// its own session table, so a remote operator — which cannot know our local\r\n/// session id — names the endpoint and we map it here, never trusting a wire\r\n/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\r\n/// (a stale registry row routed us an attach for something gone → the caller\r\n/// refuses cleanly, D6).\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\r\n    brain.sessions().ok().and_then(|reply| {\r\n        reply\r\n            .sessions\r\n            .into_iter()\r\n            .find(|s| s.endpoint == endpoint)\r\n            .map(|s| s.session_id)\r\n    })\r\n}\r\n\r\n/// Serve one attach stream against this brain's broker, blocking until the\r\n/// session exits, the operator detaches, or the gate refuses.\r\n///\r\n/// `origin_node` MUST be the handshake-proven remote id from the broker's\r\n/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes\r\n/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\r\n/// is being attached, for the gate. `net_from_seq` is this brain's resume\r\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\r\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\r\n/// journal, and re-pumped output dedups at the operator's render cursor).\r\n///\r\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\r\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\r\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\r\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\r\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\r\n/// round-trip inside the event loop would swallow interleaved attach records\r\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\r\n/// the wire `session_id`.\r\n// [impl->REQ-INST-8]\r\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub fn serve_attach(\r\n    brain: &mut Brain,\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    endpoint: &str,\r\n    origin_node: &str,\r\n    net_from_seq: u64,\r\n    remote_session: Option<u64>,\r\n) -> io::Result<AttachServeOutcome> {\r\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\r\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\r\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\r\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\r\n    // session output (broker→serve) — including the controller's synchronous,\r\n    // inline, multi-frame ring replay (`become_controller`), which holds the\r\n    // broker's single-threaded per-conn handler for the whole replay. If the\r\n    // forwards rode `brain` too, that stuck handler could not read them →\r\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\r\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\r\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\r\n    // two directions on independent single-threaded handlers cannot block each\r\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\r\n    // a second conn is correct. Order is preserved: this one serve thread reads\r\n    // every Output in seq order on `brain` and forwards it in that same order on\r\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\r\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\r\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\r\n    // the controller onto that async path would remove the inline-replay block at\r\n    // the source, but it touches the authoritative cursor-advancing path.)\r\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut attached = false;\r\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\r\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\r\n    // race on the input gate).\r\n    let mut intent = AttachIntent::Control;\r\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\r\n    // controller input that races ahead of the reply is BUFFERED, not dropped\r\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\r\n    // we know we control the session (the busy-refuse case).\r\n    let mut role: Option<ServeRole> = None;\r\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\r\n    // The attached session — captured from the Request so the terminal paths can\r\n    // explicitly release this worker's role (clearing driven_by / viewer_count\r\n    // promptly, not only on connection drop).\r\n    let mut attached_session: Option<u64> = None;\r\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\r\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\r\n    // intermittent live bursts, never a CPU spin.\r\n    let mut last_resubscribe: Option<Instant> = None;\r\n\r\n    loop {\r\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\r\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\r\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\r\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\r\n        // role resolves no Output flows, so a plain read is correct then.\r\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\n        let event = match (role, attached_session) {\r\n            (Some(ServeRole::Controller), Some(sid)) => {\r\n                match read_controller_event_resuming(brain, sid, Some(origin_node)) {\r\n                    Ok(ev) => ev,\r\n                    Err(e)\r\n                        if e.get_ref()\r\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\r\n                    {\r\n                        // Irrecoverable: the dropped frames rolled out of the ring.\r\n                        // SURFACE a marked truncation to the operator (never a\r\n                        // silent skip = B2 lie, never a spin), then end cleanly.\r\n                        // Graceful continuation is deferred\r\n                        // (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND).\r\n                        let notice =\r\n                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\r\n                        let line = ndjson::encode_line(&AttachRecord::Output {\r\n                            seq: brain.controller_resume_floor(),\r\n                            data_b64: encode_bytes(notice),\r\n                        });\r\n                        let _ = wire.net_stream_send(stream_id, &line, None, true);\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                        return Err(e);\r\n                    }\r\n                    Err(e) => {\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                        return Err(e);\r\n                    }\r\n                }\r\n            }\r\n            _ => brain.read_event()?,\r\n        };\r\n        match event {\r\n            // ── Wire → session: requests, input, resize. ───────────────────\r\n            BrokerEvent::NetStreamData {\r\n                stream_id: sid,\r\n                bytes,\r\n                ..\r\n            } if sid == stream_id => {\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                            intent: req_intent,\r\n                            // The dispatcher already resolved endpoint_id→session\r\n                            // (passed as remote_session) BEFORE this loop; ignore\r\n                            // the wire copy here (a mid-loop re-resolve would swallow\r\n                            // interleaved attach records — see remote_session docs).\r\n                            endpoint_id: _,\r\n                        } => {\r\n                            // Unsolicited wire inbound — the ADR-0009 gate, under\r\n                            // the transport-proven origin. Viewing is gated\r\n                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:\r\n                            // watching reveals full session contents).\r\n                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)\r\n                                == AccessDecision::Deny\r\n                            {\r\n                                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                                return Ok(AttachServeOutcome::Refused);\r\n                            }\r\n                            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): use the\r\n                            // dispatcher-resolved session id (the owning node is\r\n                            // authoritative for its own table); the LOCAL path uses\r\n                            // the wire session_id unchanged.\r\n                            // [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n                            let sid = remote_session.unwrap_or(session_id);\r\n                            intent = req_intent;\r\n                            attached_session = Some(sid);\r\n                            // Role-aware subscribe (REQ-RCVIEW-1): the broker\r\n                            // resolves controller / viewer / busy / take by the\r\n                            // intent + this handshake-proven origin, and replies a\r\n                            // `Subscribed`. driven_by / viewer_count are now\r\n                            // BROKER-owned (single writer — resolves the\r\n                            // clear-race), so this loop no longer stamps them.\r\n                            // A VIEWER attach ARMS snap-above at the floor so a\r\n                            // PRE-eviction ring-roll forward Output gap (the ring\r\n                            // rolled frames out under flood with NO eviction\r\n                            // marker) SNAPS to live instead of fataling the\r\n                            // legacy reject-gap → the viewer keeps forwarding.\r\n                            // The CONTROLLER keeps strict reject-gap (exactly-once\r\n                            // resume, B2). Composes with the POST-eviction\r\n                            // ViewerEvicted skip-to-live arm below.\r\n                            // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\r\n                            if req_intent == AttachIntent::Viewer {\r\n                                brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\r\n                            } else {\r\n                                brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\r\n                            }\r\n                            attached = true;\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                            match role {\r\n                                // Confirmed controller — exactly-once at the PTY\r\n                                // (the operator-minted id rides the B5 journal). NO-ACK\r\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\r\n                                // fire-and-forward (the ack is discarded here) and can\r\n                                // flood N input frames on this one conn; an acked write\r\n                                // back would fill the return direction and deadlock the\r\n                                // broker's per-conn handler. Dedup still holds at the\r\n                                // broker (op_id applied-set).\r\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n                                Some(ServeRole::Controller) => {\r\n                                    // The op_id is operator-minted (rc attach OpMinter),\r\n                                    // so it carries the `rc` journal tag (ADR-0034).\r\n                                    brain.send_effect_no_ack(\r\n                                        MintedOp::new(Minter::Rc, op_id),\r\n                                        &bytes,\r\n                                    )?\r\n                                }\r\n                                // Outcome not yet known — buffer; flushed on a\r\n                                // controller confirmation, discarded on busy/viewer.\r\n                                None => pending_input.push((op_id, bytes)),\r\n                                // A viewer never reaches here (intent gate).\r\n                                Some(ServeRole::Viewer) => {}\r\n                            }\r\n                        }\r\n                        AttachRecord::Resize { rows, cols }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\r\n                            // broker rejects it on a viewer's stream). A viewer's\r\n                            // Resize is dropped here by the intent gate too.\r\n                            let _ = brain.resize(rows, cols);\r\n                        }\r\n                        // Input/Resize before Request, a viewer's input/resize,\r\n                        // or target-direction records echoed back: noise — ignore.\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n\r\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\r\n            BrokerEvent::Subscribed { outcome, .. } => {\r\n                use crate::msg::SubscribeOutcome as O;\r\n                match outcome {\r\n                    O::Controller | O::TookControl => {\r\n                        role = Some(ServeRole::Controller);\r\n                        // A driver attached — the wake edge (controller-only;\r\n                        // idempotent on an already-active seat).\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\r\n                        // Flush any input that raced ahead of the reply (operator-\r\n                        // minted ops carry the `rc` tag).\r\n                        for (op, b) in pending_input.drain(..) {\r\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\r\n                        }\r\n                    }\r\n                    O::Viewer => {\r\n                        role = Some(ServeRole::Viewer);\r\n                        // Wake-neutral: a viewer never wakes the endpoint, never\r\n                        // keeps it awake (Gap B). Discard any racing input.\r\n                        pending_input.clear();\r\n                    }\r\n                    O::BusyControlled { .. } => {\r\n                        // A different operator controls it — refuse (no PTY input\r\n                        // ever reached it; the buffer is discarded). The rc\r\n                        // renders the busy guidance client-side from driven_by.\r\n                        wire.net_stream_send(stream_id, &[], None, true)?;\r\n                        return Ok(AttachServeOutcome::Refused);\r\n                    }\r\n                }\r\n            }\r\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\r\n            BrokerEvent::Size { rows, cols, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            // This controller was kicked by a different operator's `--take`.\r\n            BrokerEvent::Displaced { by, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                // No dormant edge: the endpoint stays driven by the taker (whose\r\n                // own serve loop already fired the wake).\r\n                return Ok(AttachServeOutcome::Displaced);\r\n            }\r\n\r\n            // This VIEWER overflowed its broker subscription queue and was evicted\r\n            // (it forwards slower than the drain fans out under flood). SKIP TO\r\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\r\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\r\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\r\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\r\n            // spin. The next live Output carries a forward seq jump; the operator's\r\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\r\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\r\n            BrokerEvent::ViewerEvicted { session_id: sid }\r\n                if attached && Some(sid) == attached_session =>\r\n            {\r\n                if let Some(prev) = last_resubscribe {\r\n                    let since = prev.elapsed();\r\n                    if since < RESUBSCRIBE_INTERVAL {\r\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\r\n                    }\r\n                }\r\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\r\n                last_resubscribe = Some(Instant::now());\r\n            }\r\n\r\n            // The operator closed its send side: the viewport is over.\r\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\r\n                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\r\n                // info.json, carrying the current driven_by through), THEN release\r\n                // the role over IPC so the broker's driven_by=None is the LAST\r\n                // write — otherwise a concurrent resting RMW restores the stale\r\n                // marker (the clear-race the broker-single-writer model averts).\r\n                if role == Some(ServeRole::Controller) {\r\n                    // The driver detached — the endpoint rests warm (controller-\r\n                    // only; a viewer leaving is wake-neutral).\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                if let Some(sid) = attached_session {\r\n                    let _ = brain.detach_session(sid); // release the role (clears markers)\r\n                }\r\n                return Ok(AttachServeOutcome::Detached);\r\n            }\r\n\r\n            // ── Session → wire: output and exit. ────────────────────────────\r\n            BrokerEvent::Output { seq, bytes, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Output {\r\n                    seq,\r\n                    data_b64: encode_bytes(&bytes),\r\n                });\r\n                // Unjournaled by design (see module docs): `seq` rides inside\r\n                // the record, and the operator's render cursor is the dedup.\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            BrokerEvent::Exit { code, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Exit { code });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Ok(AttachServeOutcome::Exited(code));\r\n            }\r\n\r\n            BrokerEvent::Error { message } => {\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Err(io::Error::other(message));\r\n            }\r\n            // Applied/NetSent acks and unrelated events: not ours to handle.\r\n            _ => {}\r\n        }\r\n    }\r\n}\r\n\r\n/// Operator-side: open an attach stream on a dialed connection and send the\r\n/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\r\n/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\r\n/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\r\n/// durable id for the exactly-once stream open (safe to block for its ack —\r\n/// no attach traffic can be in flight before the stream exists); the Request\r\n/// send itself is unjournaled (see module docs — a replayed Request re-gates\r\n/// and re-attaches idempotently).\r\n// [impl->REQ-INST-8]\r\npub fn request_attach(\r\n…\n) -> io::Result<u64> {\n…\n}","startLine":135,"lineNumbers":[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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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,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,null,560,null,562]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\attach.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":627,"totalBytes":24662,"outputLines":419,"outputBytes":24662,"shownRange":{"start":135,"end":553},"nextOffset":554}}},"isError":false,"timestamp":1784195920127}}
{"type":"message","id":"52c1aade","parentId":"584800ba","timestamp":"2026-07-16T09:58:57.174Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying stream log overflow issue**\n\n**Analyzing request eviction in long sessions**"},{"type":"toolCall","id":"call_5jGFUZFzrGluQVAJnIHcjGuk|fc_047bb49263dfa303016a58ab5fcd6881989f35626910dca86a","name":"grep","arguments":{"i":"Tracing net stream cursor gaps","pattern":"NetStreamData|net_stream_cursors|stream_cursors|net.*gap|from_seq|cursor.*stream|KIND_NET_STREAM_DATA","path":"crates/spt-daemon/src/brain.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":7936,"output":490,"cacheRead":139776,"cacheWrite":0,"totalTokens":148202,"reasoningTokens":404,"cost":{"input":0.03968,"output":0.0147,"cacheRead":0.06988799999999999,"cacheWrite":0,"total":0.12426799999999999}},"stopReason":"toolUse","timestamp":1784195920131,"responseId":"resp_047bb49263dfa303016a58ab507bc48198bfe8122cc66b2987","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58ab53124881988d802933e78133e4","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKtf7yopc1FzIAIoEX43nUx2ktUkwWI6-_1tpxbuSnQ5aX707QBEj4RKs54oacg3MJ_mxibHQAdGlxom9_qaI-H8OkphB0KdlOWiGL_gjc2UM2JmXktnn3opkT6QfJEdvnPS9Lt_mtcVHbTHDVVCENSDm5lAk6Eis_shQkEmI5dhy3xsJj3rBW_5PQyn49_QO4_2HYF2N-vhrm0LTd6UpqfAJIK8v0y4CI0y8J9EPT6RrYEBERya1winy8aYuLyFDAL7QgvWvjyYi6ihStazXCVvsCT0gWdKzo0ChsP4qGP05pz_W76pGaQThjnS8M0PsUkk0Yecc3mNh8Qp6djBJMo-hWuHZPEe8qbdSHl98g1TFyoHdLDWPdCCIzIn4XuUucBWJW5PHr9UohdlUS0_3Q_9FVMTxraauYNeoFxT1kM0nSeyFaGjKpxhafjPuwcgbdMt9Lu8BpLKuPjPQ46ntT51jMRSVP_Wx8OZZy7eRQqxx-w-T0EDOWDRb1yULQYF6zTK-qDlDyK_fYMpanYRk36ysHYqPiT5q_IlW1ehzthQRS_yu_akFtaj5LhswzPrifTVgvpzeIEkN0swPnXEzr6tqN9BnZF1fLVkp_vOCCFPG-pKlU0ZdLWFskFPXqaPG52vCzH_W4pF5mlRP_uQ3FmSAFKz555RXnQObhLSJQy_2b-cGMq5bkV_DD2pteIRvjRon1vwubMZ_auqVKC6i7SRLM5aHow_7MybT8wvc-DXWpehZKDKu-ARRjmKylRsZtyYWAJIazg_sXQVsyxF6uvgpSkq38REIjOxrIgbYzLwYwNcK5PMW2h9RMc_r1v1sdBXHybTPhvQye09BJf1X7-7uvGG2jO-pMvFu5tAYCWsE-05mKlRzKf6zBdJwJhNCdCARYE1eMGmJKbhp-m_OASMlq03rUNAuxRO4foLZNCX_a-bEcHC0Wi8YunmkX0f2w_BPsmXrhrwhPabjUy1XdNa19Lq_4gD7ZQGyiWZolc-9ty-Fn4Ik6WHPs0qFT1mRWGOJfXL14_U8nWQpbmuOvmrLJJmOOHiXEnJdbo6lraT7_UoDBuKPFmWCsbFg-8o9SOG4jlWH6uiOL9uMcnY0wIoqWfDb7cJWNxZrdu2UlySA-l0gv_Alhb-Ju8OSa52JA8nF9zRVneaMoNbEaVZ1XMWSzQZW6k2OOma2B_IIolewR0s49y0fcj4QbS0UUuM_TAMaDG1P7cIV_T_tgPLBZ1aEHOLGuU_0E0FSkrcJ2BV2ySrCikXifHhAzgNvkm7ZWNGHVYzmIHHwP1eFEITYwaOQoEKfLjExzRb-zcaKUw5RDSeiLZ1V8cwLinscBJCGash_BFFKrimLivNrVNSwb9cwuX5r6aG6qe8qxLG0KxspKfUk_GMv7Rn7TmMBE2emFxIr75wMnc1kBzQFoyalAkyEjRxxwkQHWwbof1XdVxVUN25Uwu9McLj3TgANx--PbJeCLrrlVmMt3y4QBQkDQIvJ3udOHz05TxBbxfAmnkbRrHPlLy1t-M8OTE3V1GvPV5SMJgSI2Owe0rmS_OfOUOYMjsM4VdhMqurpRrdiXeKTZeKAYUOEfSXoDRQYdtU1k62vMNMN4YnAC4_WgciecdoeJb9tHz1wTjPj6QDVrYqg0vCQPICSSSD0qS8OHaJSxKZAvPsD_v-6zdo7E5d3O-5pgTlBf6xlzK_Ptf7FjsT-N1DUY_DWGC-Z4zcNvQwTiPECfwaMcR0tHyzc13Y12qT1pAdVJ_hlA_76VT28a1vqh2QSEoTcUADfTc26evyO88hZbjB26trxwYGc09ft7MuIQRh_Fjwmqeevfn5xG2KW4FbgE5sQeSrTZuW6QylYKkWngJ5IwSruZMEZ02EiTNendYSHhMBgPcW8L-c7DJhsg079B3YbBtXf3NWr5EcguF4tFTsZ2D4UK2uinOdZX9LTAl7vX0zg-nG2hFSJKGK1_kRsKpGVtI_7v72SZgkf64koF9NO1d59hjYYwYr4m-vskJeYWKp8zHa_l_FkVnpAQ7g9n8Mz3i8zGlN76lrO94S27uHhzXCeFgmR9BneYmSB1_hLf9yg8w7yuQDVlbsVzdKyhKaIn8ROlNr_6zuAf61D4fM0AndYExGVL_q0RVNF6NcEHd--aEmuwIuH2YE0zHsIe3p0ClFpzwgkNgaTHTos7alpUK9jl13bJ-pkEnKX0O0jq2CuCxODrsdHOnxNZ9K88nFUAwzkl6531USHRUeBuiJeWmwUYD7Y745iScBnO6BB1qBv1WbbBi6xGBN6A_7PbZNxwu3gMVKtdGdVw9K8YIFgiEnYGHiTPZoo0N9bbAwgc1FnhzOt_Jgmosjs6cCcqQNQCOf0fszVA_5gQQOnKbdB5QNkb4nqELEwq7IzKBf5qY1xMtUD4TAV2e5RihIage5oEM_F-p2TihR-_C7vFu2ZGfkcJmlZ6QI7ICcDXQ01NJ89TYD-LefdFro5o9_r76e6fv9fHRRboY5lSbgNVUgVXYxPvlG5KRmTYyhyBBNLTj3rvWygOD2umku4YEP7tKUrxzppIlW3Mb-zcHPO_VYudAanqGykyN_6ju-7tmK16CwBa1311SfAFTPHYq_IkEdm0e5PwrHlhN5VLsGCAqv8y26WDa-JiUFOkEEghZbckkChmiHUy01_w5zQjzPPkOx5B3NIp0TCBHfEpTaaO0WrztIKSPFYQeoaLorodn8y17saBwmpOcJRC--mAbBU-2aAKRKuFS0STWe3xVXCU0XsRfjVv7e5gc45ZGTnph6LeY0vgHTy727Y5eRCAoaXxfkuDDFZ02UKOWlZN3h_pHjSDsYtQyXwx2Tps33zSJTeEQ-3X5DzZAoPKs3_FIBbplmDqaJ61U5XsqQDELGWSCVja9n7Ay5YbLlnvMZaF1Wm8bsIDiliJ-Y4XW_94JKgNkcr8mrNDPT0b7_rRiu7tc0LopL_EXkWMkEKXyxXgUhzeIZmIG4tJzfo380JjnSoRBNTVwjDQEK3QPLn3l3qDsblHb_J8bbWQIWSEOzS8pLBZi-TbVdBq5bpoF6Jda5GORMBqSAx_4_mK4ynR4D-WoxKGQh81LKp781oMcrlOkJJ9ES4gw0N-e-Hrfr0AUN0MI24AcsqsMzQV8e1yuuaKAjwSqJAQjGgtTVXDIhAuFy3x4aY8P1P9RQJ0jy5wKva8dlrgqvKWH8QVhrGLFH0JqhoS3UXhZeBGrpAgr-_m90IIdUDUqXgZ4pKfyB4E-7JFagO3r2y0_nlFJqRCRii6Ll6qNBLQClRP8XQMYdjYuTNenvMOzGeXjDkTpvYYbWx6Foa0BiA7vppnZkuKglDRZdIC65YZ3-FlSnd2A_oEdD-kSA0b5eR382wM6sTvcsz9jc31eWS-ztnv7ENBhHfQ0D_CL4jQyAJqHSZ-8tinBiHuSRirDFLT8PU95YGEVMwUfKRnMObJRH4SxgMwlGY40h00Qs0KXWGKHUg7lrU1wxfKVZVHoiBCxY_7b4nB0Y4cGfs_e90qUia2_Netdg","summary":[{"type":"summary_text","text":"**Identifying stream log overflow issue**"},{"type":"summary_text","text":"**Analyzing request eviction in long sessions**"}]},{"id":"fc_047bb49263dfa303016a58ab5fcd6881989f35626910dca86a","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing net stream cursor gaps\",\"pattern\":\"NetStreamData|net_stream_cursors|stream_cursors|net.*gap|from_seq|cursor.*stream|KIND_NET_STREAM_DATA\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_5jGFUZFzrGluQVAJnIHcjGuk","name":"grep"}]},"duration":17022.03599999938,"ttft":2917.1818999992684,"contextSnapshot":{"promptTokens":147712,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_5jGFUZFzrGluQVAJnIHcjGuk|fc_047bb49263dfa303016a58ab5fcd6881989f35626910dca86a","toolName":"grep","startedAt":"2026-07-16T09:58:57.174Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Tracing net stream cursor gaps"},"id":"5c46d2a7","parentId":"52c1aade","timestamp":"2026-07-16T09:58:57.174Z"}
{"type":"message","id":"f23bac29","parentId":"5c46d2a7","timestamp":"2026-07-16T09:58:57.186Z","message":{"role":"toolResult","toolCallId":"call_5jGFUZFzrGluQVAJnIHcjGuk|fc_047bb49263dfa303016a58ab5fcd6881989f35626910dca86a","toolName":"grep","content":[{"type":"text","text":" 41|    KillReq, NetDialReq, NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent,\n*42|    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,\n 43|    NetStreamSubscribeReq, NetStreamsReply, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n 44|    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,\n 45|    ViewerEvictedEvent,\n...\n 51|    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n*52|    KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED,\n 53|    KIND_MET_MEMBER, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET,\n 54|    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,\n 55|    KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,\n...\n 159|    /// A contiguous chunk of a stream's reads (the cursor has advanced past it).\n*160|    NetStreamData {\n 161|        stream_id: u64,\n 162|        seq: u64,\n 163|        bytes: Vec<u8>,\n...\n 269|    next_seq: u64,\n*270|    /// Per-net-stream resume cursors (D4b) — the stream-read siblings of\n 271|    /// `next_seq`, set by [`Brain::net_stream_subscribe`] and advanced by\n 272|    /// [`Brain::read_event`] under the same accept/dedup/gap discipline.\n 273|    /// Durability across a handoff is the logic layer's job (a re-attaching\n...\n 301|    io_timeout: Option<Duration>,\n*302|    /// #6 (ADR-0031): a COLD attach (`from_seq == 0`, set by [`Brain::attach`] /\n 303|    /// [`Brain::attach_as`]) receives a synthesized clean-screen REPAINT as its\n 304|    /// first `KIND_OUTPUT` frame, at pseudo-seq `broker.next_seq - 1` — NOT seq 0.\n 305|    /// When `true`, the strict legacy `next_seq` path adopts that first frame's seq\n...\n 309|    /// legacy single-session path; a resume-mode brain (snap-above) ignores it. A\n*310|    /// resume-from-floor re-fetch (`from_seq > 0`) leaves this `false` → raw-ring\n 311|    /// replay + strict reject-gap, entirely unchanged.\n 312|    baseline_next_output: bool,\n 313|}\n...\n 677|                    // clean-screen repaint at pseudo-seq `broker.next_seq - 1` (a\n*678|                    // forward jump from the requested `from_seq == 0`). Adopt that seq\n 679|                    // as the baseline instead of reject-gapping it — the repaint IS\n 680|                    // the complete current screen, nothing prior is owed. Consumed\n 681|                    // once per (cold) attach; every subsequent LIVE frame then takes\n...\n 806|                }\n*807|                KIND_NET_STREAM_DATA => {\n*808|                    let ev: NetStreamData = serde_json::from_value(env.payload)\n 809|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 810|                    // Same exactly-once discipline as PTY output, per stream:\n 811|                    // accept the contiguous next seq, drop a replay duplicate,\n 812|                    // reject a forward gap (a lost chunk must not pass silently).\n*813|                    let cursor = self.net_cursors.entry(ev.stream_id).or_insert(ev.seq);\n 814|                    if ev.seq < *cursor {\n 815|                        continue; // replay duplicate — dedup\n 816|                    }\n...\n 828|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n*829|                    return Ok(BrokerEvent::NetStreamData {\n 830|                        stream_id: ev.stream_id,\n 831|                        seq: ev.seq,\n 832|                        bytes,\n...\n 894|    /// Attach this (freshly-connected) brain to an **existing** broker session's\n*895|    /// output from `from_seq`, e.g. a dedicated digest-feed subscriber that did\n 896|    /// not spawn the session itself. Sets the resume cursor + session, then\n*897|    /// subscribes (the broker replays from `from_seq`, then streams live). Unlike\n 898|    /// [`Brain::handoff`] this does not touch the generation — it is a *second*\n 899|    /// reader of a session another brain drives, not a logic handoff.\n*900|    pub fn attach(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {\n 901|        self.session_id = Some(session_id);\n*902|        self.next_seq = from_seq;\n 903|        // #6 (ADR-0031): a COLD attach receives a clean-screen repaint at pseudo-seq\n 904|        // `broker.next_seq - 1`, not seq 0 — baseline the strict cursor on it.\n*905|        self.baseline_next_output = from_seq == 0;\n 906|        // A resume-mode brain (non-empty map) routes this session's output through\n 907|        // the per-session map via `read_event`'s first-sight `or_insert` seed\n 908|        // (D4-2b); a legacy brain (empty map) uses the `next_seq` set above. The\n...\n 910|        // `subscribe_with` (P1c, shared by `attach`/`attach_as`).\n*911|        self.subscribe(session_id, from_seq)\n 912|    }\n 913|\n 914|    /// Attach under the W2.5 controller/viewer model (REQ-RCVIEW-1): send a\n...\n 925|        session_id: u64,\n*926|        from_seq: u64,\n 927|        intent: AttachIntent,\n 928|        by: Option<&str>,\n 929|    ) -> io::Result<()> {\n 930|        self.session_id = Some(session_id);\n*931|        self.next_seq = from_seq;\n*932|        // #6 (ADR-0031): a COLD attach (`from_seq == 0` — the `rc` / `endpoint run\n 933|        // --attach` case) gets a clean-screen repaint at pseudo-seq\n 934|        // `broker.next_seq - 1` as its first frame; baseline the strict cursor on it\n*935|        // (accept-then-strict). A resume-from-floor re-fetch (`from_seq > 0`) leaves\n 936|        // this false → raw-ring replay + strict reject-gap unchanged (B2).\n*937|        self.baseline_next_output = from_seq == 0;\n*938|        self.subscribe_with(session_id, from_seq, intent, by.map(str::to_string))\n 939|    }\n 940|\n 941|    /// Re-subscribe a VIEWER from the LIVE TAIL after a broker eviction —\n...\n 946|    ///\n*947|    /// `from_seq = u64::MAX` makes the broker's ring replay (`s >= from_seq`)\n 948|    /// EMPTY, so the viewer receives only the size frame + live output going\n 949|    /// forward. The next live frame carries the current ring tail's seq — a\n 950|    /// FORWARD JUMP from where this brain was evicted. The legacy `next_seq` dedup\n...\n 955|    /// advances `delivered_through` / is not authoritative). The subscribe is sent\n*956|    /// FIRST (a cold brain's empty-map skips `subscribe_with`'s `from_seq` reset),\n 957|    /// THEN the cursor is armed, so neither path stamps `MAX`.\n 958|    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n 959|    pub fn attach_skip_to_live(&mut self, session_id: u64, by: Option<&str>) -> io::Result<()> {\n...\n 980|    /// Mirrors [`Brain::attach_skip_to_live`] but seeds the snap-above cursor at\n*981|    /// the requested `from_seq` (the viewer asked from there — dedup BELOW it,\n*982|    /// snap ABOVE a forward gap) and subscribes from `from_seq`, NOT the MAX\n 983|    /// skip-replay, because this is the FIRST attach rather than a post-eviction\n 984|    /// re-subscribe. The two viewer-survival mechanisms COMPOSE: this tolerates\n 985|    /// pre-eviction ring-roll gaps WHILE subscribed; [`Brain::attach_skip_to_live`]\n...\n 989|    /// subscribe is sent FIRST (a cold brain's empty map skips `subscribe_with`'s\n*990|    /// `from_seq` reset), THEN the cursor is armed.\n 991|    // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\n 992|    pub fn attach_as_viewer_snap(\n 993|        &mut self,\n 994|        session_id: u64,\n*995|        from_seq: u64,\n 996|        by: Option<&str>,\n 997|    ) -> io::Result<()> {\n 998|        self.session_id = Some(session_id);\n*999|        self.next_seq = from_seq;\n*1000|        self.subscribe_with(session_id, from_seq, AttachIntent::Viewer, by.map(str::to_string))?;\n*1001|        self.session_cursors.insert(session_id, from_seq);\n 1002|        Ok(())\n 1003|    }\n 1004|\n...\n 1526|\n*1527|    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the\n 1528|    /// local cursor (gapless, exactly-once resume — the PTY `subscribe`\n 1529|    /// discipline on a QUIC stream, D4b).\n 1530|    // [impl->REQ-DAEMON-2]\n*1531|    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {\n*1532|        self.net_cursors.insert(stream_id, from_seq);\n 1533|        self.send(\n 1534|            KIND_NET_STREAM_SUBSCRIBE,\n 1535|            serde_json::to_value(NetStreamSubscribeReq {\n 1536|                stream_id,\n*1537|                from_seq,\n 1538|            })\n 1539|            .expect(\"NetStreamSubscribeReq serializes\"),\n 1540|        )\n...\n 1544|    /// state a handed-off brain carries to resubscribe gaplessly).\n*1545|    pub fn net_cursor(&self, stream_id: u64) -> Option<u64> {\n*1546|        self.net_cursors.get(&stream_id).copied()\n 1547|    }\n 1548|\n*1549|    /// (Re)subscribe to the broker's conn-liveness event log from `from_seq`,\n 1550|    /// setting the local cursor (D4c) — gapless, exactly-once presence resume:\n 1551|    /// events produced while no brain was attached replay from the cursor.\n 1552|    // [impl->REQ-EP-4]\n*1553|    pub fn net_presence_subscribe(&mut self, from_seq: u64) -> io::Result<()> {\n*1554|        self.presence_cursor = Some(from_seq);\n 1555|        self.send(\n 1556|            KIND_NET_PRESENCE_SUBSCRIBE,\n*1557|            serde_json::to_value(NetPresenceSubscribeReq { from_seq })\n 1558|                .expect(\"NetPresenceSubscribeReq serializes\"),\n 1559|        )\n 1560|    }\n...\n 1572|    /// re-dials every peer fresh); it cares only about outcomes of dials it submits\n*1573|    /// AFTER subscribing. `from_seq = u64::MAX` means the broker replays nothing\n 1574|    /// (no retained `seq >= MAX`), so a long-lived broker whose ring has evicted\n 1575|    /// old events can never hand this carrier a below-cursor gap that would falsely\n 1576|    /// trip the dead-broker restart. Contrast [`Self::net_presence_subscribe`]\n...\n 1583|            KIND_NET_PRESENCE_SUBSCRIBE,\n*1584|            serde_json::to_value(NetPresenceSubscribeReq { from_seq: u64::MAX })\n 1585|                .expect(\"NetPresenceSubscribeReq serializes\"),\n 1586|        )\n 1587|    }\n 1588|\n*1589|    /// Re-attach to a session's output from `from_seq` as a LOCAL CONTROLLER\n 1590|    /// (intent `Control`, identity `None`) — the legacy single-session serve\n 1591|    /// [`Brain::attach`] path.\n 1592|    ///\n...\n 1608|    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n*1609|    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {\n*1610|        self.subscribe_with(session_id, from_seq, AttachIntent::Control, None)\n 1611|    }\n 1612|\n 1613|    /// Release this connection's controller/viewer role on `session_id`\n...\n 1631|        session_id: u64,\n*1632|        from_seq: u64,\n 1633|        intent: AttachIntent,\n 1634|        by: Option<String>,\n 1635|    ) -> io::Result<()> {\n 1636|        // P1c (operator-stream boundary, kitsubito RACEDIAG): a resume-mode brain\n*1637|        // (seeded map) that subscribes from `from_seq` must RESET this session's\n*1638|        // dedup cursor to `from_seq` — the broker's replay from `from_seq` is then\n 1639|        // re-delivered (and the operator's render cursor dedups the overlap),\n 1640|        // never lost. WHY THIS, NOT just `read_event`'s `or_insert`: on a re-serve,\n 1641|        // `serve_attach` may CONSUME a handoff-replay frame at the K boundary\n...\n 1645|        // seq K arrives below the advanced cursor and is deduped → seq K never\n*1646|        // reaches the operator viewport → a forward gap. Resetting to `from_seq`\n 1647|        // makes the re-subscribe re-deliver everything from its floor. A cold-start\n 1648|        // brain (empty map — e.g. the production dispatch serve brain) keeps the\n 1649|        // legacy `next_seq` path; this only arms in resume mode.\n...\n 1651|        if !self.session_cursors.is_empty() {\n*1652|            self.session_cursors.insert(session_id, from_seq);\n 1653|        }\n 1654|        self.send(\n 1655|            KIND_SUBSCRIBE,\n...\n 1657|                session_id,\n*1658|                from_seq,\n 1659|                intent,\n 1660|                by,\n 1661|            })\n...\n 1943|        // (1) CONTROLLER attach on the strict legacy path: empty `session_cursors`,\n*1944|        // next_seq seeded to the requested from_seq=0 (subscribe_with leaves the map\n 1945|        // empty for a cold brain regardless of intent). Contiguous 0,1,2 accepted.\n 1946|        brain\n 1947|            .attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\"))\n...\n 2029|    /// JUMP output seq (>> the prior cursor, the broker's live ring-tail after the\n*2030|    /// empty `from_seq=MAX` replay) is ACCEPTED via the snap-above dedup path, NOT\n 2031|    /// rejected as an `output gap` and NOT dedup-dropped. `attach_skip_to_live` is\n 2032|    /// what flips the brain from the legacy reject-gap path (the previous test) into\n 2033|    /// the resume-mode dedup-below + snap-above path (it seeds `session_cursors` and\n...\n 2039|    ///     → `read_event` errors `output gap` instead of accepting → RED.\n*2040|    ///   - Leave `next_seq = u64::MAX` (the from_seq) instead of resetting to 0 AND\n 2041|    ///     route through the legacy path → every live seq < MAX is dedup-dropped →\n 2042|    ///     `read_event` would block (no event) → the test's bounded reader times out.\n 2043|    ///   - Snap-above mis-implemented as reject-gap (`seq > cursor` → Err) → RED.\n...\n 2051|        // session_cursors[sid]=0 (snap-above armed) and next_seq=0, and writes a\n*2052|        // subscribe(from_seq=MAX) the stub silently drains.\n 2053|        brain\n 2054|            .attach_skip_to_live(sid, Some(\"operator-node\"))\n 2055|            .expect(\"attach_skip_to_live\");\n...\n 2104|    /// `skip_to_live` (which seeds at 0 after a MAX re-subscribe), `attach_as_viewer_snap`\n*2105|    /// seeds the snap-above cursor at the requested `from_seq` (a NON-zero floor):\n*2106|    /// dedup BELOW `from_seq`, snap ABOVE a forward ring-roll gap. Without arming,\n 2107|    /// the empty `session_cursors` runs the LEGACY reject-gap path → `read_event`\n 2108|    /// FATALS the forward gap (`output gap: got 4504 want 100`) → forwarding stops →\n 2109|    /// the operator sees nothing (wedged viewer, Linux forkpty).\n...\n 2111|    /// Non-vacuous (three distinct mutations caught):\n*2112|    ///   - Drop the `session_cursors.insert(session_id, from_seq)` in\n 2113|    ///     `attach_as_viewer_snap` → the map stays empty → the forward gap hits the\n 2114|    ///     legacy reject-gap path → `read_event` errors `output gap` → RED.\n*2115|    ///   - Seed at 0 instead of `from_seq` → the dedup-below boundary is WRONG: a\n*2116|    ///     seq in [0, from_seq) that should DEDUP (already delivered before this\n 2117|    ///     attach floor) would instead be ACCEPTED below the intended floor. The\n 2118|    ///     primary assertions pin the floor at FROM=100 (not 0) and the\n 2119|    ///     accept-and-advance above it.\n...\n 2129|        // session_cursors[sid]=FROM (snap-above armed at the requested floor, NOT 0)\n*2130|        // and next_seq=FROM, and writes a subscribe(from_seq=FROM) the stub drains.\n 2131|        brain\n 2132|            .attach_as_viewer_snap(sid, FROM, Some(\"operator-node\"))\n 2133|            .expect(\"attach_as_viewer_snap\");\n...\n 2136|            Some(FROM),\n*2137|            \"attach_as_viewer_snap arms the snap-above cursor at the requested floor (from_seq), not 0\"\n 2138|        );\n 2139|\n 2140|        // The ring rolled between reads: the next Output frame carries a seq far"}],"details":{"scopePath":"crates/spt-daemon/src/brain.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":64,"fileCount":1,"files":["crates/spt-daemon/src/brain.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/brain.rs","count":64}],"truncated":false,"displayContent":"   41│    KillReq, NetDialReq, NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent,\n  *42│    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,\n   43│    NetStreamSubscribeReq, NetStreamsReply, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n   44│    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,\n   45│    ViewerEvictedEvent,\n     │...\n   51│    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n  *52│    KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED,\n   53│    KIND_MET_MEMBER, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET,\n   54│    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,\n   55│    KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,\n     │...\n  159│    /// A contiguous chunk of a stream's reads (the cursor has advanced past it).\n *160│    NetStreamData {\n  161│        stream_id: u64,\n  162│        seq: u64,\n  163│        bytes: Vec<u8>,\n     │...\n  269│    next_seq: u64,\n *270│    /// Per-net-stream resume cursors (D4b) — the stream-read siblings of\n  271│    /// `next_seq`, set by [`Brain::net_stream_subscribe`] and advanced by\n  272│    /// [`Brain::read_event`] under the same accept/dedup/gap discipline.\n  273│    /// Durability across a handoff is the logic layer's job (a re-attaching\n     │...\n  301│    io_timeout: Option<Duration>,\n *302│    /// #6 (ADR-0031): a COLD attach (`from_seq == 0`, set by [`Brain::attach`] /\n  303│    /// [`Brain::attach_as`]) receives a synthesized clean-screen REPAINT as its\n  304│    /// first `KIND_OUTPUT` frame, at pseudo-seq `broker.next_seq - 1` — NOT seq 0.\n  305│    /// When `true`, the strict legacy `next_seq` path adopts that first frame's seq\n     │...\n  309│    /// legacy single-session path; a resume-mode brain (snap-above) ignores it. A\n *310│    /// resume-from-floor re-fetch (`from_seq > 0`) leaves this `false` → raw-ring\n  311│    /// replay + strict reject-gap, entirely unchanged.\n  312│    baseline_next_output: bool,\n  313│}\n     │...\n  677│                    // clean-screen repaint at pseudo-seq `broker.next_seq - 1` (a\n *678│                    // forward jump from the requested `from_seq == 0`). Adopt that seq\n  679│                    // as the baseline instead of reject-gapping it — the repaint IS\n  680│                    // the complete current screen, nothing prior is owed. Consumed\n  681│                    // once per (cold) attach; every subsequent LIVE frame then takes\n     │...\n  806│                }\n *807│                KIND_NET_STREAM_DATA => {\n *808│                    let ev: NetStreamData = serde_json::from_value(env.payload)\n  809│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n  810│                    // Same exactly-once discipline as PTY output, per stream:\n  811│                    // accept the contiguous next seq, drop a replay duplicate,\n  812│                    // reject a forward gap (a lost chunk must not pass silently).\n *813│                    let cursor = self.net_cursors.entry(ev.stream_id).or_insert(ev.seq);\n  814│                    if ev.seq < *cursor {\n  815│                        continue; // replay duplicate — dedup\n  816│                    }\n     │...\n  828│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n *829│                    return Ok(BrokerEvent::NetStreamData {\n  830│                        stream_id: ev.stream_id,\n  831│                        seq: ev.seq,\n  832│                        bytes,\n     │...\n  894│    /// Attach this (freshly-connected) brain to an **existing** broker session's\n *895│    /// output from `from_seq`, e.g. a dedicated digest-feed subscriber that did\n  896│    /// not spawn the session itself. Sets the resume cursor + session, then\n *897│    /// subscribes (the broker replays from `from_seq`, then streams live). Unlike\n  898│    /// [`Brain::handoff`] this does not touch the generation — it is a *second*\n  899│    /// reader of a session another brain drives, not a logic handoff.\n *900│    pub fn attach(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {\n  901│        self.session_id = Some(session_id);\n *902│        self.next_seq = from_seq;\n  903│        // #6 (ADR-0031): a COLD attach receives a clean-screen repaint at pseudo-seq\n  904│        // `broker.next_seq - 1`, not seq 0 — baseline the strict cursor on it.\n *905│        self.baseline_next_output = from_seq == 0;\n  906│        // A resume-mode brain (non-empty map) routes this session's output through\n  907│        // the per-session map via `read_event`'s first-sight `or_insert` seed\n  908│        // (D4-2b); a legacy brain (empty map) uses the `next_seq` set above. The\n     │...\n  910│        // `subscribe_with` (P1c, shared by `attach`/`attach_as`).\n *911│        self.subscribe(session_id, from_seq)\n  912│    }\n  913│\n  914│    /// Attach under the W2.5 controller/viewer model (REQ-RCVIEW-1): send a\n     │...\n  925│        session_id: u64,\n *926│        from_seq: u64,\n  927│        intent: AttachIntent,\n  928│        by: Option<&str>,\n  929│    ) -> io::Result<()> {\n  930│        self.session_id = Some(session_id);\n *931│        self.next_seq = from_seq;\n *932│        // #6 (ADR-0031): a COLD attach (`from_seq == 0` — the `rc` / `endpoint run\n  933│        // --attach` case) gets a clean-screen repaint at pseudo-seq\n  934│        // `broker.next_seq - 1` as its first frame; baseline the strict cursor on it\n *935│        // (accept-then-strict). A resume-from-floor re-fetch (`from_seq > 0`) leaves\n  936│        // this false → raw-ring replay + strict reject-gap unchanged (B2).\n *937│        self.baseline_next_output = from_seq == 0;\n *938│        self.subscribe_with(session_id, from_seq, intent, by.map(str::to_string))\n  939│    }\n  940│\n  941│    /// Re-subscribe a VIEWER from the LIVE TAIL after a broker eviction —\n     │...\n  946│    ///\n *947│    /// `from_seq = u64::MAX` makes the broker's ring replay (`s >= from_seq`)\n  948│    /// EMPTY, so the viewer receives only the size frame + live output going\n  949│    /// forward. The next live frame carries the current ring tail's seq — a\n  950│    /// FORWARD JUMP from where this brain was evicted. The legacy `next_seq` dedup\n     │...\n  955│    /// advances `delivered_through` / is not authoritative). The subscribe is sent\n *956│    /// FIRST (a cold brain's empty-map skips `subscribe_with`'s `from_seq` reset),\n  957│    /// THEN the cursor is armed, so neither path stamps `MAX`.\n  958│    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n  959│    pub fn attach_skip_to_live(&mut self, session_id: u64, by: Option<&str>) -> io::Result<()> {\n     │...\n  980│    /// Mirrors [`Brain::attach_skip_to_live`] but seeds the snap-above cursor at\n *981│    /// the requested `from_seq` (the viewer asked from there — dedup BELOW it,\n *982│    /// snap ABOVE a forward gap) and subscribes from `from_seq`, NOT the MAX\n  983│    /// skip-replay, because this is the FIRST attach rather than a post-eviction\n  984│    /// re-subscribe. The two viewer-survival mechanisms COMPOSE: this tolerates\n  985│    /// pre-eviction ring-roll gaps WHILE subscribed; [`Brain::attach_skip_to_live`]\n     │...\n  989│    /// subscribe is sent FIRST (a cold brain's empty map skips `subscribe_with`'s\n *990│    /// `from_seq` reset), THEN the cursor is armed.\n  991│    // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\n  992│    pub fn attach_as_viewer_snap(\n  993│        &mut self,\n  994│        session_id: u64,\n *995│        from_seq: u64,\n  996│        by: Option<&str>,\n  997│    ) -> io::Result<()> {\n  998│        self.session_id = Some(session_id);\n *999│        self.next_seq = from_seq;\n*1000│        self.subscribe_with(session_id, from_seq, AttachIntent::Viewer, by.map(str::to_string))?;\n*1001│        self.session_cursors.insert(session_id, from_seq);\n 1002│        Ok(())\n 1003│    }\n 1004│\n     │...\n 1526│\n*1527│    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the\n 1528│    /// local cursor (gapless, exactly-once resume — the PTY `subscribe`\n 1529│    /// discipline on a QUIC stream, D4b).\n 1530│    // [impl->REQ-DAEMON-2]\n*1531│    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {\n*1532│        self.net_cursors.insert(stream_id, from_seq);\n 1533│        self.send(\n 1534│            KIND_NET_STREAM_SUBSCRIBE,\n 1535│            serde_json::to_value(NetStreamSubscribeReq {\n 1536│                stream_id,\n*1537│                from_seq,\n 1538│            })\n 1539│            .expect(\"NetStreamSubscribeReq serializes\"),\n 1540│        )\n     │...\n 1544│    /// state a handed-off brain carries to resubscribe gaplessly).\n*1545│    pub fn net_cursor(&self, stream_id: u64) -> Option<u64> {\n*1546│        self.net_cursors.get(&stream_id).copied()\n 1547│    }\n 1548│\n*1549│    /// (Re)subscribe to the broker's conn-liveness event log from `from_seq`,\n 1550│    /// setting the local cursor (D4c) — gapless, exactly-once presence resume:\n 1551│    /// events produced while no brain was attached replay from the cursor.\n 1552│    // [impl->REQ-EP-4]\n*1553│    pub fn net_presence_subscribe(&mut self, from_seq: u64) -> io::Result<()> {\n*1554│        self.presence_cursor = Some(from_seq);\n 1555│        self.send(\n 1556│            KIND_NET_PRESENCE_SUBSCRIBE,\n*1557│            serde_json::to_value(NetPresenceSubscribeReq { from_seq })\n 1558│                .expect(\"NetPresenceSubscribeReq serializes\"),\n 1559│        )\n 1560│    }\n     │...\n 1572│    /// re-dials every peer fresh); it cares only about outcomes of dials it submits\n*1573│    /// AFTER subscribing. `from_seq = u64::MAX` means the broker replays nothing\n 1574│    /// (no retained `seq >= MAX`), so a long-lived broker whose ring has evicted\n 1575│    /// old events can never hand this carrier a below-cursor gap that would falsely\n 1576│    /// trip the dead-broker restart. Contrast [`Self::net_presence_subscribe`]\n     │...\n 1583│            KIND_NET_PRESENCE_SUBSCRIBE,\n*1584│            serde_json::to_value(NetPresenceSubscribeReq { from_seq: u64::MAX })\n 1585│                .expect(\"NetPresenceSubscribeReq serializes\"),\n 1586│        )\n 1587│    }\n 1588│\n*1589│    /// Re-attach to a session's output from `from_seq` as a LOCAL CONTROLLER\n 1590│    /// (intent `Control`, identity `None`) — the legacy single-session serve\n 1591│    /// [`Brain::attach`] path.\n 1592│    ///\n     │...\n 1608│    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n*1609│    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {\n*1610│        self.subscribe_with(session_id, from_seq, AttachIntent::Control, None)\n 1611│    }\n 1612│\n 1613│    /// Release this connection's controller/viewer role on `session_id`\n     │...\n 1631│        session_id: u64,\n*1632│        from_seq: u64,\n 1633│        intent: AttachIntent,\n 1634│        by: Option<String>,\n 1635│    ) -> io::Result<()> {\n 1636│        // P1c (operator-stream boundary, kitsubito RACEDIAG): a resume-mode brain\n*1637│        // (seeded map) that subscribes from `from_seq` must RESET this session's\n*1638│        // dedup cursor to `from_seq` — the broker's replay from `from_seq` is then\n 1639│        // re-delivered (and the operator's render cursor dedups the overlap),\n 1640│        // never lost. WHY THIS, NOT just `read_event`'s `or_insert`: on a re-serve,\n 1641│        // `serve_attach` may CONSUME a handoff-replay frame at the K boundary\n     │...\n 1645│        // seq K arrives below the advanced cursor and is deduped → seq K never\n*1646│        // reaches the operator viewport → a forward gap. Resetting to `from_seq`\n 1647│        // makes the re-subscribe re-deliver everything from its floor. A cold-start\n 1648│        // brain (empty map — e.g. the production dispatch serve brain) keeps the\n 1649│        // legacy `next_seq` path; this only arms in resume mode.\n     │...\n 1651│        if !self.session_cursors.is_empty() {\n*1652│            self.session_cursors.insert(session_id, from_seq);\n 1653│        }\n 1654│        self.send(\n 1655│            KIND_SUBSCRIBE,\n     │...\n 1657│                session_id,\n*1658│                from_seq,\n 1659│                intent,\n 1660│                by,\n 1661│            })\n     │...\n 1943│        // (1) CONTROLLER attach on the strict legacy path: empty `session_cursors`,\n*1944│        // next_seq seeded to the requested from_seq=0 (subscribe_with leaves the map\n 1945│        // empty for a cold brain regardless of intent). Contiguous 0,1,2 accepted.\n 1946│        brain\n 1947│            .attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\"))\n     │...\n 2029│    /// JUMP output seq (>> the prior cursor, the broker's live ring-tail after the\n*2030│    /// empty `from_seq=MAX` replay) is ACCEPTED via the snap-above dedup path, NOT\n 2031│    /// rejected as an `output gap` and NOT dedup-dropped. `attach_skip_to_live` is\n 2032│    /// what flips the brain from the legacy reject-gap path (the previous test) into\n 2033│    /// the resume-mode dedup-below + snap-above path (it seeds `session_cursors` and\n     │...\n 2039│    ///     → `read_event` errors `output gap` instead of accepting → RED.\n*2040│    ///   - Leave `next_seq = u64::MAX` (the from_seq) instead of resetting to 0 AND\n 2041│    ///     route through the legacy path → every live seq < MAX is dedup-dropped →\n 2042│    ///     `read_event` would block (no event) → the test's bounded reader times out.\n 2043│    ///   - Snap-above mis-implemented as reject-gap (`seq > cursor` → Err) → RED.\n     │...\n 2051│        // session_cursors[sid]=0 (snap-above armed) and next_seq=0, and writes a\n*2052│        // subscribe(from_seq=MAX) the stub silently drains.\n 2053│        brain\n 2054│            .attach_skip_to_live(sid, Some(\"operator-node\"))\n 2055│            .expect(\"attach_skip_to_live\");\n     │...\n 2104│    /// `skip_to_live` (which seeds at 0 after a MAX re-subscribe), `attach_as_viewer_snap`\n*2105│    /// seeds the snap-above cursor at the requested `from_seq` (a NON-zero floor):\n*2106│    /// dedup BELOW `from_seq`, snap ABOVE a forward ring-roll gap. Without arming,\n 2107│    /// the empty `session_cursors` runs the LEGACY reject-gap path → `read_event`\n 2108│    /// FATALS the forward gap (`output gap: got 4504 want 100`) → forwarding stops →\n 2109│    /// the operator sees nothing (wedged viewer, Linux forkpty).\n     │...\n 2111│    /// Non-vacuous (three distinct mutations caught):\n*2112│    ///   - Drop the `session_cursors.insert(session_id, from_seq)` in\n 2113│    ///     `attach_as_viewer_snap` → the map stays empty → the forward gap hits the\n 2114│    ///     legacy reject-gap path → `read_event` errors `output gap` → RED.\n*2115│    ///   - Seed at 0 instead of `from_seq` → the dedup-below boundary is WRONG: a\n*2116│    ///     seq in [0, from_seq) that should DEDUP (already delivered before this\n 2117│    ///     attach floor) would instead be ACCEPTED below the intended floor. The\n 2118│    ///     primary assertions pin the floor at FROM=100 (not 0) and the\n 2119│    ///     accept-and-advance above it.\n     │...\n 2129│        // session_cursors[sid]=FROM (snap-above armed at the requested floor, NOT 0)\n*2130│        // and next_seq=FROM, and writes a subscribe(from_seq=FROM) the stub drains.\n 2131│        brain\n 2132│            .attach_as_viewer_snap(sid, FROM, Some(\"operator-node\"))\n 2133│            .expect(\"attach_as_viewer_snap\");\n     │...\n 2136│            Some(FROM),\n*2137│            \"attach_as_viewer_snap arms the snap-above cursor at the requested floor (from_seq), not 0\"\n 2138│        );\n 2139│\n 2140│        // The ring rolled between reads: the next Output frame carries a seq far"},"isError":false,"timestamp":1784195937185}}
{"type":"message","id":"246324d9","parentId":"f23bac29","timestamp":"2026-07-16T09:59:06.049Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_ffz2Bd3W3TTTUiX52AjJocWC|fc_047bb49263dfa303016a58ab67d57c8198b1291495ff0a3b7e","name":"read","arguments":{"i":"Reading net stream gap behavior","path":"crates/spt-daemon/src/brain.rs","selector":"795-842"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":7751,"output":40,"cacheRead":144896,"cacheWrite":0,"totalTokens":152687,"cost":{"input":0.038755000000000005,"output":0.0012000000000000001,"cacheRead":0.072448,"cacheWrite":0,"total":0.112403}},"stopReason":"toolUse","timestamp":1784195937191,"responseId":"resp_047bb49263dfa303016a58ab61ae2c819885915e70ee71c8c8","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_047bb49263dfa303016a58ab67d57c8198b1291495ff0a3b7e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading net stream gap behavior\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":\"795-842\"}","call_id":"call_ffz2Bd3W3TTTUiX52AjJocWC","name":"read"}]},"duration":8837.742399999872,"ttft":6623.443500000052,"contextSnapshot":{"promptTokens":152647,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ffz2Bd3W3TTTUiX52AjJocWC|fc_047bb49263dfa303016a58ab67d57c8198b1291495ff0a3b7e","toolName":"read","startedAt":"2026-07-16T09:59:06.049Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Reading net stream gap behavior"},"id":"f50119f0","parentId":"246324d9","timestamp":"2026-07-16T09:59:06.049Z"}
{"type":"message","id":"ccdaee1e","parentId":"f50119f0","timestamp":"2026-07-16T09:59:06.070Z","message":{"role":"toolResult","toolCallId":"call_ffz2Bd3W3TTTUiX52AjJocWC|fc_047bb49263dfa303016a58ab67d57c8198b1291495ff0a3b7e","toolName":"read","content":[{"type":"text","text":"                KIND_SESSIONS_REPLY => {\n                    let ev: SessionsReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Sessions(ev));\n                }\n                KIND_PAIR_JOINED => {\n                    let ev: PairJoinReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::PairJoined(ev));\n                }\n                KIND_MET_MEMBER => {\n                    let ev: MetMember = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::MetMember(ev));\n                }\n                KIND_NET_STREAM_DATA => {\n                    let ev: NetStreamData = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    // Same exactly-once discipline as PTY output, per stream:\n                    // accept the contiguous next seq, drop a replay duplicate,\n                    // reject a forward gap (a lost chunk must not pass silently).\n                    let cursor = self.net_cursors.entry(ev.stream_id).or_insert(ev.seq);\n                    if ev.seq < *cursor {\n                        continue; // replay duplicate — dedup\n                    }\n                    if ev.seq > *cursor {\n                        return Err(io::Error::new(\n                            io::ErrorKind::InvalidData,\n                            format!(\n                                \"stream {} gap: got seq {} want {}\",\n                                ev.stream_id, ev.seq, *cursor\n                            ),\n                        ));\n                    }\n                    *cursor = ev.seq + 1;\n                    let bytes = decode_bytes(&ev.data_b64)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::NetStreamData {\n                        stream_id: ev.stream_id,\n                        seq: ev.seq,\n                        bytes,\n                    });\n                }\n                KIND_NET_STREAM_EOF => {\n                    let ev: NetStreamEof = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::NetStreamEof {\n                        stream_id: ev.stream_id,\n                        next_seq: ev.next_seq,\n                    });\n                }\n                KIND_NET_PRESENCE_EVENT => {\n                    let ev: NetPresenceEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n…\n                }\n\n[Showing lines 794-845 of 2178. Use :846 to continue]"}],"details":{"truncation":{"content":"                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    return Ok(BrokerEvent::Sessions(ev));\r\n                }\r\n                KIND_PAIR_JOINED => {\r\n                    let ev: PairJoinReply = serde_json::from_value(env.payload)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    return Ok(BrokerEvent::PairJoined(ev));\r\n                }\r\n                KIND_MET_MEMBER => {\r\n                    let ev: MetMember = serde_json::from_value(env.payload)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    return Ok(BrokerEvent::MetMember(ev));\r\n                }\r\n                KIND_NET_STREAM_DATA => {\r\n                    let ev: NetStreamData = serde_json::from_value(env.payload)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    // Same exactly-once discipline as PTY output, per stream:\r\n                    // accept the contiguous next seq, drop a replay duplicate,\r\n                    // reject a forward gap (a lost chunk must not pass silently).\r\n                    let cursor = self.net_cursors.entry(ev.stream_id).or_insert(ev.seq);\r\n                    if ev.seq < *cursor {\r\n                        continue; // replay duplicate — dedup\r\n                    }\r\n                    if ev.seq > *cursor {\r\n                        return Err(io::Error::new(\r\n                            io::ErrorKind::InvalidData,\r\n                            format!(\r\n                                \"stream {} gap: got seq {} want {}\",\r\n                                ev.stream_id, ev.seq, *cursor\r\n                            ),\r\n                        ));\r\n                    }\r\n                    *cursor = ev.seq + 1;\r\n                    let bytes = decode_bytes(&ev.data_b64)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    return Ok(BrokerEvent::NetStreamData {\r\n                        stream_id: ev.stream_id,\r\n                        seq: ev.seq,\r\n                        bytes,\r\n                    });\r\n                }\r\n                KIND_NET_STREAM_EOF => {\r\n                    let ev: NetStreamEof = serde_json::from_value(env.payload)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    return Ok(BrokerEvent::NetStreamEof {\r\n                        stream_id: ev.stream_id,\r\n                        next_seq: ev.next_seq,\r\n                    });\r\n                }\r\n                KIND_NET_PRESENCE_EVENT => {\r\n                    let ev: NetPresenceEvent = serde_json::from_value(env.payload)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r","truncated":true,"truncatedBy":"lines","totalLines":1385,"totalBytes":2857,"outputLines":52,"outputBytes":2857,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"                KIND_SESSIONS_REPLY => {\n                    let ev: SessionsReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    return Ok(BrokerEvent::Sessions(ev));\r\n                }\r\n                KIND_PAIR_JOINED => {\r\n                    let ev: PairJoinReply = serde_json::from_value(env.payload)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    return Ok(BrokerEvent::PairJoined(ev));\r\n                }\r\n                KIND_MET_MEMBER => {\r\n                    let ev: MetMember = serde_json::from_value(env.payload)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    return Ok(BrokerEvent::MetMember(ev));\r\n                }\r\n                KIND_NET_STREAM_DATA => {\r\n                    let ev: NetStreamData = serde_json::from_value(env.payload)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    // Same exactly-once discipline as PTY output, per stream:\r\n                    // accept the contiguous next seq, drop a replay duplicate,\r\n                    // reject a forward gap (a lost chunk must not pass silently).\r\n                    let cursor = self.net_cursors.entry(ev.stream_id).or_insert(ev.seq);\r\n                    if ev.seq < *cursor {\r\n                        continue; // replay duplicate — dedup\r\n                    }\r\n                    if ev.seq > *cursor {\r\n                        return Err(io::Error::new(\r\n                            io::ErrorKind::InvalidData,\r\n                            format!(\r\n                                \"stream {} gap: got seq {} want {}\",\r\n                                ev.stream_id, ev.seq, *cursor\r\n                            ),\r\n                        ));\r\n                    }\r\n                    *cursor = ev.seq + 1;\r\n                    let bytes = decode_bytes(&ev.data_b64)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    return Ok(BrokerEvent::NetStreamData {\r\n                        stream_id: ev.stream_id,\r\n                        seq: ev.seq,\r\n                        bytes,\r\n                    });\r\n                }\r\n                KIND_NET_STREAM_EOF => {\r\n                    let ev: NetStreamEof = serde_json::from_value(env.payload)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    return Ok(BrokerEvent::NetStreamEof {\r\n                        stream_id: ev.stream_id,\r\n                        next_seq: ev.next_seq,\r\n                    });\r\n                }\r\n                KIND_NET_PRESENCE_EVENT => {\r\n                    let ev: NetPresenceEvent = serde_json::from_value(env.payload)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n…\n                }","startLine":792,"lineNumbers":[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,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,null,863]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2178,"totalBytes":2857,"outputLines":52,"outputBytes":2857,"shownRange":{"start":794,"end":845},"nextOffset":846}}},"isError":false,"timestamp":1784195946069}}
{"type":"message","id":"956d9b62","parentId":"ccdaee1e","timestamp":"2026-07-16T09:59:24.413Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying sequence gap causing dispatcher freeze**"},{"type":"toolCall","id":"call_Oew1xERISMw1NEiE0qxQpyBc|fc_047bb49263dfa303016a58ab70c5e081988fa3f28003196e8f","name":"irc","arguments":{"i":"Reporting exact refresh chain","op":"send","to":"Main","message":"Refute global omission: `run_brain` cursor-only `resume_session_cursors` is PTY-session bookkeeping, but after first 500ms `net_status` it calls `spawn_net_consumers`; fresh `run_dispatch_loop` has empty `claimed`, enumerates every broker-held peer stream, and `worker` does `peek_first_line(stream,0)` then fresh `serve_attach(...,0)`. Exact likely partial cause: ordinary attach `StreamLog` ring is only 4096 transport chunks and overwrites its opener. Long/active rc streams can evict seq0 Request. On refresh new `peek_first_line` subscribes at 0; first retained seq>0 makes `Brain::read_event` return `stream X gap: got seq N want 0`; worker logs DISPATCH Failed and `claimed` permanently suppresses retry. A quieter 1/5 stream still retaining Request recovers. Detach+new rc creates fresh stream with seq0 Request, so recovery is exact. Manual restart test `attach_survives_target_brain_restart_exactly_once` re-serves explicitly before ring roll and does not exercise production dispatcher reclaim. Active serve worker conn indeed owns both roles: `serve_attach` primary brain calls `net_stream_subscribe` then PTY `attach_as`, so label becomes `role=brain stream-subscriber stream=X controller session=Y`; separate `wire` Brain sends output. More detail in final.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3266,"output":483,"cacheRead":150016,"cacheWrite":0,"totalTokens":153765,"reasoningTokens":132,"cost":{"input":0.01633,"output":0.014490000000000001,"cacheRead":0.07500799999999999,"cacheWrite":0,"total":0.10582799999999999}},"stopReason":"toolUse","timestamp":1784195946089,"responseId":"resp_047bb49263dfa303016a58ab6ab6748198a6eabed6ed1d29c6","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58ab6c3e308198954381d99e5a15ba","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKtwbzVGDyK9A2NzrzJ9hT2Yw1ThuyzUFfwnx6R0ylNt7KJAh654D5cRXM-TvlHVKln7_fJ3PmPUPxsdy4ASSZDPeQegz0ncR6oA7-22NUgPjxT7YAm25Wx91Z50oF2SbrUrqx5d2Akl9HHgWgs8jsPJbaFeQp6UCNMKUJwDwVryad_PzR1BJ1C79u8z_qqATov9vYzuF6MjRMn7djkqmCR4E5zoSr13xxHfSZsULE3N7r7QxbsooXc9LFe94wgKShK32iha9Ecz54TfUuvhuOvsLKTBz0VASS6fGb6DFdEGdbTBv3_JJ37bQpTqRSJFyMrB93cwkxmpqK52Uqfkeo2b5Bl3rT3X-60JNkBW8f_MRhK4I505HXHPMfRy6Ir9QJu1gP-VXO8UAIpR76KxY_rVz5NF6YPJDdM_m6ntvbOnke7DMbbEZEkHGtDf9r348LgsN1j6Ur74vhDvlyn2WOuk9hQEGJvMGNulcDBpqV5aq8qHJVpcrGwgx0_Q_K_GPrmZ_jIhgWbUn3I-UlF5rFBlCg2w_yfhpuWG1uEoEXQmugxJvhjqJ5lQqJ9szBtAyaK5xBzCuF_mcGOZOqkbKVuLbYz089V6UqzdyO5ktDodBppehTAsHkIRGqIXOVSbSlPw2FRW2MRnR9f2sR5LkqnmvqLby2O7fDcTj-rRT7nhirciWunKZygAUxzzyqDs6xpxXOZO5e_0bNgYtL5_chghb5-ykm3r2MqbarDxnrgG7BrbvuZHRdk0wMZCUVr0M7fhf_5kYqKvSD4IQ59FsBu4eaPCsZUO5V1wFr4R48VNj6UlkIYpXEQydocFiMeZrRQBvzgOk6hXctmp4e6_aPNqIyhsa0irbBtOd19MV2cL9EaLDn6Pgna0bY8_tUEpDBe1PQAYgec7mqzwO0DHMj79eMvsNg75gdhBVIi__QP7c01_2xpvP7vWkQeiOrNwC5rHMyudHE1lCseRUuTVDjBUa0GjtLmjkly0WiIWeZkmyuN5j6Jie3e3lxcOLjQepJNFZSF6vGJFhbU4T57_wZPTipOIqnZ60qdzfzBfcOGEoS7R4r3zBmdNna205rD-6gTFY4aaWhnrsvwc-U6KrW85-9U7wGgjXWgVdZdFutd85Qz_OqB0PFcBQRbdCz9HDgmMmzAOtb6ZsYFTvHPuF1XzdFZKNAeHxy4H3ka605pw1JFFszR2XbciZ_DvrsqiQI5Q7e49MK8LKsyNRzBzljTYDVLoruKWQ2wFZuF-okrpSRuC85vbUp-nNOefXdRKrqQyt_XDUwGmc6RdDnL3AfKEQBDbkJsm1DBKn9O_QHY4kZFEEwJfOQ29kMd-blNZ79g5MqMS-Mmg9Hkw1CBW-qRYMkNDUcIT4XEiaSBnRLD2_j_HAwFUjEP9N6Gk3r4S2HZiPr5ZOUZmZQw5N5dhnW9rwHdQaC9__YXwH7VvP6QGdN9k9Lwr0x-WhG6kLBB9TUsYydXi6K2evgw83iYf40lVFjuWjBgE1aYPRhhEy8UfLlC36ycYEi7yZm8YIDhTbpCUNCvswqhyvRPi_EZOHXgXnvetqnXQKVk8ApKWu46hXjcdhcW8g8a20zF95SLQiA_KC1JnCU1jnZLUT3ysq3Rj7cfRrcmgh6bR13iRlIm8Wors4ykOP1Iikxa6rLZO_Hbi4okibT433NocOsXfut9zUifRR3iFOHJuoF29ro25fatZSZrZAZxfM3d7Lhkfqw6-ya_HGuOdn01dp_TVoDO86A==","summary":[{"type":"summary_text","text":"**Identifying sequence gap causing dispatcher freeze**"}]},{"id":"fc_047bb49263dfa303016a58ab70c5e081988fa3f28003196e8f","type":"function_call","status":"completed","arguments":"{\"i\":\"Reporting exact refresh chain\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Refute global omission: `run_brain` cursor-only `resume_session_cursors` is PTY-session bookkeeping, but after first 500ms `net_status` it calls `spawn_net_consumers`; fresh `run_dispatch_loop` has empty `claimed`, enumerates every broker-held peer stream, and `worker` does `peek_first_line(stream,0)` then fresh `serve_attach(...,0)`. Exact likely partial cause: ordinary attach `StreamLog` ring is only 4096 transport chunks and overwrites its opener. Long/active rc streams can evict seq0 Request. On refresh new `peek_first_line` subscribes at 0; first retained seq>0 makes `Brain::read_event` return `stream X gap: got seq N want 0`; worker logs DISPATCH Failed and `claimed` permanently suppresses retry. A quieter 1/5 stream still retaining Request recovers. Detach+new rc creates fresh stream with seq0 Request, so recovery is exact. Manual restart test `attach_survives_target_brain_restart_exactly_once` re-serves explicitly before ring roll and does not exercise production dispatcher reclaim. Active serve worker conn indeed owns both roles: `serve_attach` primary brain calls `net_stream_subscribe` then PTY `attach_as`, so label becomes `role=brain stream-subscriber stream=X controller session=Y`; separate `wire` Brain sends output. More detail in final.\" ,\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_Oew1xERISMw1NEiE0qxQpyBc","name":"irc"}]},"duration":18317.473899999633,"ttft":2136.696200000122,"contextSnapshot":{"promptTokens":153282,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Oew1xERISMw1NEiE0qxQpyBc|fc_047bb49263dfa303016a58ab70c5e081988fa3f28003196e8f","toolName":"irc","startedAt":"2026-07-16T09:59:24.414Z","intent":"Reporting exact refresh chain"},"id":"d4768e57","parentId":"956d9b62","timestamp":"2026-07-16T09:59:24.414Z"}
{"type":"message","id":"9f621ffa","parentId":"d4768e57","timestamp":"2026-07-16T09:59:24.414Z","message":{"role":"toolResult","toolCallId":"call_Oew1xERISMw1NEiE0qxQpyBc|fc_047bb49263dfa303016a58ab70c5e081988fa3f28003196e8f","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"PtyControl","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784195964414}}
{"type":"message","id":"d3a43e11","parentId":"9f621ffa","timestamp":"2026-07-16T10:00:07.170Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Formulating eviction and connection hypotheses**\n\n**Analyzing controller channel and stream claim issues**\n\n**Identifying stale attach controller race condition**"},{"type":"thinking","thinking":"**Tracing active worker stream takeover**\n\n**Identifying frozen stream cause and missing removals**"},{"type":"toolCall","id":"call_2LPU3FKjooR4pyOBn7kZmfoU|fc_047bb49263dfa303016a58aba6bb9081989ffb4a18119067e9","name":"grep","arguments":{"i":"Checking stream row deletion","pattern":"streams\\.(lock\\(\\)\\.unwrap\\(\\)\\.)?remove|remove\\(&.*stream|retain\\(.*stream|finished.*remove","path":"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/broker.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_lp6vr0ox84ZhXsLGMCbxuxOB|fc_047bb49263dfa303016a58aba6bba08198820d69c6defc655d","name":"grep","arguments":{"i":"Finding stale stream cleanup tests","pattern":"finished.*stream|stream.*finished|old stream|stale stream|remove.*stream|stream_infos\\(\\).*len|streams\\.len|peer rows","path":"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/tests;crates/spt/tests","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_qbAAdbcnRjgLWf5K5XknC4j5|fc_047bb49263dfa303016a58aba6bbac8198b92075f4a325cc32","name":"grep","arguments":{"i":"Locating same origin resolver","pattern":"fn resolve_subscribe|Same|same.*origin|Control.*controller_by|BusyControlled|Take","path":"crates/spt-daemon/src/broker.rs","selector":"1800-1900,3440-3508","case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_MDQ6IAY9PuMjqMtbRtoVaDjc|fc_047bb49263dfa303016a58aba6bbb881989fce47187efd6585","name":"read","arguments":{"i":"Reading resolver and unsubscribe","path":"crates/spt-daemon/src/broker.rs","selector":"1800-1885,3440-3510"}},{"type":"toolCall","id":"call_mOl036JnzKSbyEWwFDPgwpvY|fc_047bb49263dfa303016a58aba6bbc4819899d996da93843b22","name":"read","arguments":{"i":"Reading stale writer epoch replacement","path":"crates/spt-daemon/src/broker.rs","selector":"880-970,1245-1285,1500-1595"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":2746,"output":1170,"cacheRead":151040,"cacheWrite":0,"totalTokens":154956,"reasoningTokens":781,"cost":{"input":0.013730000000000001,"output":0.0351,"cacheRead":0.07551999999999999,"cacheWrite":0,"total":0.12434999999999999}},"stopReason":"toolUse","timestamp":1784195964999,"responseId":"resp_047bb49263dfa303016a58ab7d4d6c8198a783a7a877c0b2fe","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58ab82a6388198834f74662a478078","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKuZh-0QZScPdMqTgzHBRlvgHBfAHDmY1JSl_pt-JCCMT67yW9ZI-UIlH8aj56F3HXvhpHuy8Ahq6l3ViYFcGq8ZEQGCkDn_9_jXkH1wqJmDh2j2jOLznNPx1Ce0cnoAvTQMkxT7PtKBHNHuSn6OAz0J_eEqKSdtzFoeOx0uE0g7Epd3-uQec-3yzTBRQGBmKUL2mFxAfQ3Q2I1LHdPyp4EF4eCE4tzz_-U_D7IlkkDoB9E6hB60ibi5J08jttRswWGARIFn86epbRyHq3eq3q3ZzxB6Sn-K4MqXo3p60G3ZWVwm5P703fDowpUckZSdBqb1k8yt6Tu-dD3MKcVp9vpiX9QYaN-njqWUP5Bv5nIIRRChbACHq3NWzCckk_OFyeX0JfBcsdWClbz9xBia6bhKZ4OziPwH2bJIaEMen72CF-DDv8ZYavsCl1h-_FRIlPXVrADMaVoWGssx5IBFZ6JdNkJzgR9ih6eJFON5ovFbaOomARa7JmY7P4oKlRxm8KP1F5zUUDkfqR4Gp9Unf4dQt8j4ae1hTe1M2V8mqH-_7te_qjiugTt0gm7yjswLdo7AtxdMY8-dpejm0YFTSA893Fh4NAZfx0NYJOOcBqiZRaE1tVxwEbVqZu3q508fFpJrK_wDfFW_AL3Orm1ij9xOfTohlcCyQ14TcNn-CnN0417DMa3_pPTaCxEXBVQhBYFOnnbVyvop-uVQVNUmf05pKogY0YJOUSjZEtaRnQtTAirrtTSAP2vigAyunwsCCAVNW7fAuzp3ap0t53lyfXEcnUVcw7KHYu1CefEBbUV-JnlmijsMEYmosCuB_4JtARG2xfDKZdZnRjQ4Bp5McIIrKmlu7yKcX6UI7DGyyFfj7kORMa1wptivNNWKAcIluWs28fXiefdDur2FfCwWGKEE3WE4YaaT0KxTrVxWviSfdXUEbjbTBiHI_aSbOmXTS-34wrXSASzGEjA7AHkttyBxV4rzzoe_8fI99yCpl8SBPM5swpoGPpv8CMNIgwR_DcZaMz_J4OoKFfHkJhC-_uKCczFP16dKQ2sx92KjhlPkgAkBbkW0FkhxSJTlQAEgD8zfRA7FriQDS8NZCdCCFib9OK6eCivmkHkeqssN-HNnjYwOE_EjVxYE9ldNMs79ixZBoCx-6SZZGDRRRempOTNvxORs8XLWlC3cqSTopVXqDYY-kOQZkjhFdtRcfFO68pN4SgDxO-_fsrk2zKXVgcfoxliQJ9K3dTzTJl0xtsi4lihnKAmgDi2TdKq0eUREsLrDVFh_upeGvQH0zOqhKJS5thK1epLxYUJbUmTv_J11E2UjLf_7wbonC87s5PLRDM1YJ2o_QYiHEVOCXJu2QrliBdKV9WmpIDWsVO2sxsJVhXeweEceis5HYBY8KR_2D3JNl0V5CsxDn5eqkmd8aLkpAG8zFDvOZOVNPoiPYKs3U-326cxwdOCgOPm5XqyKEajuQlls_v8P7EnxoQfnjDdn6Mu5jqGhjS9qhRVMAubZ5viihBys3-BIIhP5QcILklAjybDB7L6uNGwk30WvqAbHAjhe9Iysq1GxjUGMNNk5mdTIa7gmR73HdBy7dB8fzpchu9FWOH1QqM5UBQ82ZY6KRzvQ1OZ5floqKyZTJG9qVoH0O9Z31WybT5gvWZj6fBOOi3pVGO0NLo54i-Uv_Mb65SahoYb2QpKpw-VgC8LKmEBTR9mS-WabCh6_hnVRmP-j4_ndwYcoU-gnutPZm-vX0DPIcrm9xe3vs2YZkuZz2OHUuWOII-lUQvb0rYNvbJc0EOK1gOR0lzPx6ZF1BoR4wgtp1h6UCmX1BfDAfQhE2PRJsFgD67OxBi53C0fJh8RzSkSn00sKuMqNajGEGUgxlzkBtl-ZcIrBF42XABLAcrP6WhyTpZlF8Nu_lF7UNsC1rl6so9nJsAt3xp2UoZ8k0eIrTIfUSSve0zoQR2dr6-cpKCVFCbubHHSOoVi2QGg2et-efmv7jRVlAHGbUb0QKhfiORsaQrKwa9YfNSSqEVC6V7a8ZVlsNTF3ACbRS9xiyEjw-S5icjzJLCh-BGfpoWzGka49xJ7gGkZ6yLSpQgq9mZ7fuiX50tfDIZ_skF7z7xGPGSH2cFXs3e_pOZczbY5XhzL6puIkifp3C949ryfEXCDBrL93ckKg1kkLYbNkXaOZtow2V7Vb-uA91uDKbKKjww73ubxa6olqhZr53kfE2u2R_63k71T2NJyg_MzRtxmbREVtps0mkNDgH5srx2DMHrVSjSdeOuuWkBBMFddZfl5qb8FnVViKB_1KPkDorzmQ8RrMyhJ4EnQ2oa1i3ewlNHjGYmUX5hPWbH3w-3FKdAc4ZRRXXp2R6_EH6_n_-87hzxcm1PzW50FbEM39FvJDJZKnn1WIVIynFSKFoKRmWJfi3efDEq9ukLaGgneoYkqWZ3Bw_YG9uefs7ULJ1IGVfTbwh2VeOET2V1Yg-KQUwYTGXxVHz41NcwInzF6qf9rBT3Q8RQORS1Q8CABrV5qplHe_c6jEHzzpOe8pzSGrPfAz4mRGE8vxK0s8IrEWwzHLXkgIrskWYQXzuUXKmZj-s11SEzxl-j7eLJb77bspzFPLqN0G6KG3gPNBsBSpJa8iWCZrsBmpfSKKT60ZSk603s9ynCYkR-RCWXbcP2tXe21WDBVXMgKL_4gUgy14wSAddzE9MTVHnOltc9zzds8OybDmXD6a_2AXqEgwDxBzfv4Ql51fgyD37bYUPAnGuLojUt98JzYLPM1ZKIE4Rt4WOgSJ5DO54wJnHCKT4QUDqRTa7cXVQHY9HqVlkE-5T7jPkU6F8dkmjn4diK4OrarZ9-sUT6wvS1cQduj0kUo0mZJs45fexOmD27lHZSZI-wYoAERzA_UTuRJae5-T1JR0OwIJMq1OQ4frm1EUt_Nv52WvvdEAsSI8_nqG8pU3p2c_HxmML8YkS7ASiZ01LKjptbr5zQGezrNsZYrOBlMCx-R5DEp1phxQkqCU4broYSMeatV59DGc77JYnmhEHBtznf9_4D4qCapqn5_fQeT7IMzfQ2ktWfplbpLT9aenndF9n8Y7B6sA7GFVj04Xrs1qse6ByM-5G4Aq-B2qUrtyhd_-IcHPjgcSvBThghwAbjznbZln4CUrCKQD_AiDyLzItlyAf2j-qHfyjYQ5gBGpk2RSyWsTzt67J6SgUwfstQF_RTAGIPmh95WlZweuMOL67GoqI8WoyhBxNp3V4ChoI8ir6q8gSTkyg4pPYg0Gg5CBxTKA_yc2ileYWqkjAoPcpFsjTMUzgRg5CgUkYiP9FZc510gdP5tUIkUe-5m8ZjS6ras8A2PKzgo7E_dhnrvlnSUHjkvwRjOAT_HEO2M1891ffShO5dwfXeeuecJmm3YNh2upqtELpFn2Gb4amQ1jP-wLceIfpoOFEKnDw-tjvZ9iP8e-j-poNsylDR9wqwsXIasWThr62zyNnTkKjWLgTW1lA8ffIwzdewn4VG9dyHI-VfYnoj5AI0JWZ8gIIhbZWcM-1XdGjnyJIDU4EBnXqTA-Rcz1wYDFabb4-zexLXqJRFVD1aEg31zrXzk-ZM7VXdyM4YjV0_kUml-dLULNoZVXPmLQXoW5EQOA55d2mTipS7AISLRgXI6V0D_cgln0fm5H0_eYYKofZ6lnw8AGDhR9nb6S0W-nzDYCUSu7PtDlr2NJoCFiks0ocWm3dldjquH6Te_4oKq8vRkysqX8Zum75yu87K5uixyxiczt2I8Kc7nIIn3hXBhVtFgLQ0y96YMgnheGf4OSvtlKa5vZw5GUmOhz05tRxQxearelbCm6l0Np8WU_ktHUDv8urkpTR44BtFKdHetqFkMZBzDsdDNNamv7hMTx5MFiBH5hR2uue9pCc7Z8Ki_O9H-LWJqMvU39X6WT0YlXYNI0ZeQPIdiKC5WGoV-J13mEVaFENtZ769CrQ40D2l6o2xyTgrDjfDHaGCHJpkCyf57nyHnf1JFC5fhTTTWNuUSupYD2IRIv5UAJ6Vb7AiUYXhZD6wSKPyoBDPo4_UBfcivR8r-8EiBwi-o8yk7felFd-MRncqzF1onL2HzAjIUVG4XmqLRyk0bQQtn9z2AEeCfbX7pt4jv4-yray32ifo9xo6Ws9GijNiYIGJAm988DAHCWiwkzASdVvfx6Xx2hsX0v1mjERFHycaAei1t9b5Ft6hljwMWksRAjsxS8ws4gijGXYcKgvN0Xe88QCLP2MbW1LCPO3N39Y5GbuF8jGJ5O4jR8RMBTNGCl9bkj1e2_8Di4TJULJOuT4rBY-IthilXpag==","summary":[{"type":"summary_text","text":"**Formulating eviction and connection hypotheses**"},{"type":"summary_text","text":"**Analyzing controller channel and stream claim issues**"},{"type":"summary_text","text":"**Identifying stale attach controller race condition**"}]},{"id":"rs_047bb49263dfa303016a58ab9902188198aa4d6690a1d81cd4","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKumi6ihXkuA7Rn0auAy__9EB1qKWWZnV8-SqMITuzYw92xuRWaF6zQva5rrKVxVvGdVg7uS1usy7l_VY3Clow6AYMNaJyiCJVecczN_AUv-rh1-0VsEzfbz7itoBmMxH2qjS-x7OvmVT2JwH9xWUmncBDk3BcBCorITb7QWSEnYc6YElhO4LJ1IAqA7BPY_r19sVoP9lp7-EBQdmmtns4r-0Rj-OkBOG5pFgRiInef3KL3LjBnGTyAvpdqwq-79sfV3-yR07GUHkrEI__L3znLy8O87QufjZOOc5FjXKgQDqM1dS8DAZEEwBRa2899nmU3JfXmkOXVLEP-ujz7Rnu_LiCPtJQFGSskVbpSmTUnjc1twZsNk3PXyOTX_FXqokX8rc6LIfcv6gKmNGDXRZlSht4c6WmDpKT4pAcTBzi2NtKrsXTNek1n4c5grMN5E6EL57ZR7ng80QY4J3ntnApDEmmhaOPWHIhE0Z54dp4F3sRB8cVh672fvxGf1hnQqLWJ0hpegtRuBEGh-TULxO8YOcaypbsbFFhGg96ymaEGcWtsyLEdwYVsN-hAx-svb9R-Gp9dqOiYKNsRvjuWNhBZqElswiQNuoGBgBS-RxPgHddMJN2a-KJ17n4L7tCXlwRw8NKd150tFZydHVaSrA_ANtu46PrQeQEnu4bosv3M_qnEhiyNv1TpmnZtx5mw8C8skaIKt_a3tUAIEefZ4dXFuNriEk95UH0oKqD--kxKnXnAVxATC1mrEpH1673Ow-VIBp4YRx6MbGnLv8prRO_Z6UilV917jZE5LGVedzzHOKVHs_WKEZ8ng7aOMlNG7UdSLo8thliCVxpvnXcs8sYG-bFXSBchwxvD-3QHD2vGwxtnaUm14Ej3Y_HmW7aM9x-ShzXe5YGn8VUM1E2OSfGA7Iv1zmbmQiV02tn_muIYg9LHZTCjt4bxr-zOxaSNco25bJSclFFH8K1Odxrw5nTpFSTz_b6wbcgYk9Uq_Oy9qBvAbP1RxnhXtILU2ZeH1Y8OOLBRtIgC9YmJulKIJX4Sf117j2sKv49rU_c3sW4Ykw03WYXFxKFZQNkbuxhdJu5tL4p8EzWz8T9q4hPoDC1QVoeMFENxyyvRHAsRh_xuNfKE6l3oZz7sesdpPTvHDnkLyuCkUJnnLwuZ5mh7Vp4Wrr_xFNxQs1U3ZUU2l2H2C-uqNmXQ6JfBqggM10GgQ2Q-DcKewHqrPsG14mqRbRilvgNg-vJanld3mHMOWF10-VID0vfCcMFZuK7UysBYM5Cx284defbkmv3RBPwyOn9-O0ihB_FM6i42E3GhVLMdX9PMTzglohKriFcLe4f2S8eZ2dob6HXeypqboyAHlCjVtutcV24XrbZD0_2Fz7kx7kBTmxCUM3jhzIokdfkqdSiRVi2rEXGuRuVsCFi6K2_yc6MZp-HDSDsAOxHBYEVt_k2DR7PglRa-LatdQqmP9ngroL55m1afYJ7bOu318a4X_YhJku4SvA45hzImaehaoDPBA8BPgLrB8ur9pWKXL6SaeqKuiEWehb_q4gXOWUZ0VWq6leSqUFcy5j_cuAO_5oueTEOEw3wc0uP6GSm6vD41kCl7vUhPM-5hpBed9NsBXF3EsgJA1O-GrAhSyiKpyAk5s5eNJFpaKkBLSrGQDfwl1xrJs_5SmFi2z08-fqUhyq4-nhp3D0F5Mv4lDciqwk-DcJZxuITrV0bXumNfYOhfYvhxD_FEibZJqHh-tzz-tojKw1qGxCMvUPU6m44AHirJ2XkrpfsaN7cOtTNykFHUR1uukfelL-UDsJrfKznO5X6oyKaWuKsbQ8pw3VAjCj3TDDg5y8e5exLshp-xIiVZRAZpfk6byMfKcg7l7vSdbm1Tc5c7QqXuhhtgnFz2fQfe7yfLIiDoMUq1L5J4NA9RqG8QEXE46XoEA8ahcm7HYURbHOhx4a1wzIz1ZkgEWLsAwZsRIgkGe_LXyw2MoNmgyr0qC37ldcdtO2QhfjDf9hCz67106vaT1a5Dg1YVY55N_Ix-tp9tQUnrjZ_JAdE_8Vfm_61NBuGFSJ6xvnJbMFkDMqAR5jtpTciyGQbD3LLBetJtznscJI2gfHs9CZCfw3OJtW_silCoL_IQF1wW5e3E9nBbzzuxCVJyGT1t2tIMpZ1Qi5wnKs8NHjMBXwhtQ39u-cc5tjS7gP_jvcxFM5hrFj74WRTeH32P4hb8nH58OpIFbuV5hTxtoRbqYmZV8fZ5xFA9fVexKFR6SU9HekqE7mMV5NDMdvhMqvhmTU-9DXSBnk-xSbgoof3u4GpYQHy3G0LuNceD_kl8pWRTjZMTddDHdtOXtoTo3U3UlkAmj1lB7EsgWZ7h8lBTU39UKVtkoFBlHfhgvpD5LVgXxc9drTFY4ihW7z224aKMNul5QYAvXktYS-3WJ8mYo4GQHMfBGoq0tLic1Z9KEXhZ3Ti2fL0q4l7egEjckS22Oiniu2c8nye5ECePCbALchVIHj2NLx0LkygV3PEy_jOWnmJ_DiB0ZygnfE6dxK7pT7-dWW0GlDyWRawHQ0UBTRqQa7BQEK42HRAM7b7C9dIqsXUjdl1JoPNiEmjc01b890A7jz9kT5SNpAjlgPdWTqTRpuK6Uok2bP16yagEalE2f8qxaL5K1eNyxbgGo4ThHp5G0JWdO1oE8nh3eXCQFNvLMuLwmMXWzbbRsa-HKROi604cpz2jwjWK9mIh-n_IqeB7XnVb9nRCdqbk8Sa72KO3CRVL_CnOFcYwFi8sTBE6pRg==","summary":[{"type":"summary_text","text":"**Tracing active worker stream takeover**"},{"type":"summary_text","text":"**Identifying frozen stream cause and missing removals**"}]},{"id":"fc_047bb49263dfa303016a58aba6bb9081989ffb4a18119067e9","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking stream row deletion\",\"pattern\":\"streams\\\\.(lock\\\\(\\\\)\\\\.unwrap\\\\(\\\\)\\\\.)?remove|remove\\\\(&.*stream|retain\\\\(.*stream|finished.*remove\",\"path\":\"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/broker.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_2LPU3FKjooR4pyOBn7kZmfoU","name":"grep"},{"id":"fc_047bb49263dfa303016a58aba6bba08198820d69c6defc655d","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding stale stream cleanup tests\",\"pattern\":\"finished.*stream|stream.*finished|old stream|stale stream|remove.*stream|stream_infos\\\\(\\\\).*len|streams\\\\.len|peer rows\",\"path\":\"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/tests;crates/spt/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_lp6vr0ox84ZhXsLGMCbxuxOB","name":"grep"},{"id":"fc_047bb49263dfa303016a58aba6bbac8198b92075f4a325cc32","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating same origin resolver\",\"pattern\":\"fn resolve_subscribe|Same|same.*origin|Control.*controller_by|BusyControlled|Take\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"1800-1900,3440-3508\",\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_qbAAdbcnRjgLWf5K5XknC4j5","name":"grep"},{"id":"fc_047bb49263dfa303016a58aba6bbb881989fce47187efd6585","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading resolver and unsubscribe\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"1800-1885,3440-3510\"}","call_id":"call_MDQ6IAY9PuMjqMtbRtoVaDjc","name":"read"},{"id":"fc_047bb49263dfa303016a58aba6bbc4819899d996da93843b22","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading stale writer epoch replacement\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"880-970,1245-1285,1500-1595\"}","call_id":"call_mOl036JnzKSbyEWwFDPgwpvY","name":"read"}]},"duration":42169.86439999938,"ttft":5635.029899999499,"contextSnapshot":{"promptTokens":153786,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_2LPU3FKjooR4pyOBn7kZmfoU|fc_047bb49263dfa303016a58aba6bb9081989ffb4a18119067e9","toolName":"grep","startedAt":"2026-07-16T10:00:07.171Z","args":{"path":"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/broker.rs"},"intent":"Checking stream row deletion"},"id":"553ad61e","parentId":"d3a43e11","timestamp":"2026-07-16T10:00:07.171Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_lp6vr0ox84ZhXsLGMCbxuxOB|fc_047bb49263dfa303016a58aba6bba08198820d69c6defc655d","toolName":"grep","startedAt":"2026-07-16T10:00:07.171Z","args":{"path":"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/tests;crates/spt/tests"},"intent":"Finding stale stream cleanup tests"},"id":"e7598dc9","parentId":"553ad61e","timestamp":"2026-07-16T10:00:07.171Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qbAAdbcnRjgLWf5K5XknC4j5|fc_047bb49263dfa303016a58aba6bbac8198b92075f4a325cc32","toolName":"grep","startedAt":"2026-07-16T10:00:07.171Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Locating same origin resolver"},"id":"2540691a","parentId":"e7598dc9","timestamp":"2026-07-16T10:00:07.171Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_MDQ6IAY9PuMjqMtbRtoVaDjc|fc_047bb49263dfa303016a58aba6bbb881989fce47187efd6585","toolName":"read","startedAt":"2026-07-16T10:00:07.171Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading resolver and unsubscribe"},"id":"aa49abfe","parentId":"2540691a","timestamp":"2026-07-16T10:00:07.171Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_mOl036JnzKSbyEWwFDPgwpvY|fc_047bb49263dfa303016a58aba6bbc4819899d996da93843b22","toolName":"read","startedAt":"2026-07-16T10:00:07.171Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading stale writer epoch replacement"},"id":"9f92b88a","parentId":"aa49abfe","timestamp":"2026-07-16T10:00:07.171Z"}
{"type":"message","id":"04a40ba8","parentId":"9f92b88a","timestamp":"2026-07-16T10:00:07.182Z","message":{"role":"toolResult","toolCallId":"call_qbAAdbcnRjgLWf5K5XknC4j5|fc_047bb49263dfa303016a58aba6bbac8198b92075f4a325cc32","toolName":"grep","content":[{"type":"text","text":" 1806|/// Drain the inject floor's buffered controller input to the PTY **after** an\n*1807|/// injected sequence, then release the floor. Each step takes the buffered bytes\n 1808|/// OFF the lock before ENQUEUEING it to the lone input-writer (the W1 rule: never\n 1809|/// hold a lock across a PTY write — now never block on it either); input arriving\n 1810|/// mid-flush keeps buffering, so strict order is preserved until the buffer\n...\n 1837|/// broker serving. Safe for the SHORT map ops it guards (`sessions` / `pair_holds`\n*1838|/// inserts+removes stay coherent on recovery — the same argument as the floor). The\n 1839|/// per-session `OutputLog` RING needs the extra coherence clamp of [`recover_log`].\n 1840|/// CLASS INVARIANT (KNOWN-HAZARDS): NO bare `.lock().unwrap()` on a broker-resident\n 1841|/// lock reachable from serve/dispatch — use `recover`/`recover_log` or a documented\n...\n 1871|    loop {\n*1872|        let chunk = lock_floor(floor).take_or_release();\n 1873|        match chunk {\n 1874|            Some(bytes) => {\n 1875|                input.enqueue(bytes);\n...\n 3443|    /// exactly-once resume. The `intent` + `by` select the role; the broker\n*3444|    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\n 3445|    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\n 3446|    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\n*3447|    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\n 3448|    /// self-kick guard.\n 3449|    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n 3450|        let req: SubscribeReq = serde_json::from_value(env.payload)\n...\n 3455|        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\n*3456|        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\n 3457|        // the per-session exit-waiter holds for the WHOLE life of a live child\n 3458|        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\n 3459|        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\n...\n 3506|    /// With an `op_id` the write is routed through the broker-owned\n*3507|    /// [`EffectJournal`] for exactly-once delivery: a replay of the same\n 3508|    /// `(session_id, op_id)` after a brain crash is deduped (the PTY is not"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs","searchPath":"C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/broker.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":7,"fileCount":1,"files":["crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":7}],"truncated":false,"displayContent":" 1806│/// Drain the inject floor's buffered controller input to the PTY **after** an\n*1807│/// injected sequence, then release the floor. Each step takes the buffered bytes\n 1808│/// OFF the lock before ENQUEUEING it to the lone input-writer (the W1 rule: never\n 1809│/// hold a lock across a PTY write — now never block on it either); input arriving\n 1810│/// mid-flush keeps buffering, so strict order is preserved until the buffer\n     │...\n 1837│/// broker serving. Safe for the SHORT map ops it guards (`sessions` / `pair_holds`\n*1838│/// inserts+removes stay coherent on recovery — the same argument as the floor). The\n 1839│/// per-session `OutputLog` RING needs the extra coherence clamp of [`recover_log`].\n 1840│/// CLASS INVARIANT (KNOWN-HAZARDS): NO bare `.lock().unwrap()` on a broker-resident\n 1841│/// lock reachable from serve/dispatch — use `recover`/`recover_log` or a documented\n     │...\n 1871│    loop {\n*1872│        let chunk = lock_floor(floor).take_or_release();\n 1873│        match chunk {\n 1874│            Some(bytes) => {\n 1875│                input.enqueue(bytes);\n     │...\n 3443│    /// exactly-once resume. The `intent` + `by` select the role; the broker\n*3444│    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\n 3445│    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\n 3446│    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\n*3447│    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\n 3448│    /// self-kick guard.\n 3449│    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n 3450│        let req: SubscribeReq = serde_json::from_value(env.payload)\n     │...\n 3455│        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\n*3456│        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\n 3457│        // the per-session exit-waiter holds for the WHOLE life of a live child\n 3458│        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\n 3459│        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\n     │...\n 3506│    /// With an `op_id` the write is routed through the broker-owned\n*3507│    /// [`EffectJournal`] for exactly-once delivery: a replay of the same\n 3508│    /// `(session_id, op_id)` after a brain crash is deduped (the PTY is not"},"isError":false,"timestamp":1784196007182}}
{"type":"message","id":"00d46a11","parentId":"04a40ba8","timestamp":"2026-07-16T10:00:07.250Z","message":{"role":"toolResult","toolCallId":"call_mOl036JnzKSbyEWwFDPgwpvY|fc_047bb49263dfa303016a58aba6bbc4819899d996da93843b22","toolName":"read","content":[{"type":"text","text":"    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\n    /// inline under the log lock. The writer thread advances the authoritative\n    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,\n    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\n    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\n    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\n    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\n    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\n    /// single live writer per connection and a monotonic on-wire seq stream,\n    /// [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]). Stamps `driven_by` to the new\n    /// controller's identity.\n    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {\n        // Drop the prior controller sink first (its writer's live loop ends when\n        // tx drops), and bump the generation so (a) an in-flight deadline-evict\n        // for the old controller can't unseat this one and (b) a prior writer\n        // still flushing its initial batch returns before its next frame.\n        // Attribution (F-039 legs b+d): the replaced conn's record names its\n        // successor, and the new conn accretes the controller role fact.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        if let Some(old) = self.controller.take() {\n            old.send.lifecycle_event(\n                \"controller-replaced\",\n                &format!(\"session={} new_conn={}\", self.session_id, sub.id()),\n            );\n        }\n        sub.describe(&format!(\n            \"controller session={} endpoint={} by={}\",\n            self.session_id,\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint },\n            by.as_deref().unwrap_or(\"local\")\n        ));\n        sub.lifecycle_event(\n            \"controller-attach\",\n            &format!(\"session={} from_seq={from_seq}\", self.session_id),\n        );\n        self.controller = None;\n        let epoch = self.controller_epoch.fetch_add(1, Ordering::AcqRel) + 1;\n        // #6 (ADR-0031): a COLD attach's initial batch is a single synthesized clean\n        // repaint of the CURRENT SCREEN (at pseudo-seq `next_seq - 1`) instead of the\n        // raw ring — replaying an alt-screen TUI's raw ring into a fresh terminal is\n        // exactly the corruption #6/#7/#8 report. A resume-from-floor (`from_seq > 0`)\n        // keeps the raw-ring exactly-once re-fetch untouched (B2). Either way the\n        // batch is owned by the writer thread (never pushed through the live channel —\n        // it must not block the drain); live frames stream raw + in-order afterwards.\n        let initial: Vec<(u64, Vec<u8>)> = match self.repaint_initial(from_seq) {\n            Some(repaint) => vec![repaint],\n            None => self\n                .ring\n                .iter()\n                .filter(|(s, _)| *s >= from_seq)\n                .cloned()\n                .collect(),\n        };\n        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n        let writer_send = Arc::clone(&sub);\n        let dt = Arc::clone(&self.delivered_through);\n        let epoch_gate = Arc::clone(&self.controller_epoch);\n        let sid = self.session_id;\n        // W2 decouple: the writer publishes its in-flight-write window here so a\n        // stall-evict can distinguish a wedged writer from an idle one.\n        let write_blocked_since = Arc::new(Mutex::new(None));\n        let writer_blocked = Arc::clone(&write_blocked_since);\n        let writer = thread::spawn(move || {\n            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\n        });\n        self.controller = Some(ControllerSink {\n            tx,\n            send: sub,\n            by,\n            last_ok: Instant::now(),\n            write_blocked_since,\n            _writer: writer,\n        });\n        self.stamp_driven_by();\n    }\n\n    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared\n    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per\n    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its\n    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an\n    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\n    fn clear_controller(&mut self) {\n        if self.controller.take().is_some() {\n            self.stamp_driven_by();\n        }\n    }\n\n    /// B-2 (REQ-CONTROLLER-LIVENESS-REAP): lazily drop a controller whose WRITER\n    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\n    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\n    /// writer exits when a socket write fails (a severed / keepalive-killed controller\n…\n    fn set_size_and_notify(&mut self, rows: u16, cols: u16) {\n…\n        if !evicted.is_empty() {\n            for vid in evicted {\n…\n                // (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).\n                if let Some(sink) = self.viewers.remove(&vid) {\n                    sink.evicted.store(true, Ordering::Release);\n                }\n            }\n            self.stamp_viewer_count();\n        }\n    }\n\n    /// Detach `sub` from whatever role it held: clear the controller slot iff it\n    /// is the current controller (a no-op if a newer brain took over — a late\n    /// disconnect can't unseat the live controller), and remove any viewer rows\n    /// owned by `sub`. Leaves `delivered_through` intact (D4-1). Re-stamps\n    /// `driven_by` / `viewer_count` when a role actually changed.\n    fn detach_if(&mut self, sub: &SharedSend) {\n        let is_ctrl = self\n            .controller\n            .as_ref()\n            .is_some_and(|c| Arc::ptr_eq(&c.send, sub));\n        if is_ctrl {\n            self.clear_controller(); // drops the sink (ends its writer) + unlatches\n        }\n        let before = self.viewers.len();\n        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));\n        // Bounded detach record (F-039 leg d) — only when a role actually changed.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        if is_ctrl || self.viewers.len() != before {\n            sub.lifecycle_event(\n                \"session-detach\",\n                &format!(\n                    \"session={} was_controller={is_ctrl} viewers_dropped={}\",\n                    self.session_id,\n                    before - self.viewers.len()\n                ),\n            );\n        }\n        if self.viewers.len() != before {\n            self.stamp_viewer_count();\n        }\n    }\n\n…\n/// cursor on each success), then drain live [`CtrlMsg`]s with a BLOCKING socket\n/// write each — so a backed-up controller blocks ONLY this thread, never the\n/// session drain (which used to write the controller inline under the log lock).\n/// A failed write (dead operator) ends the thread; the drain's next bounded\n/// send then sees `Disconnected` and detaches the row. `Output` advances\n/// `delivered_through`; `Control` (Displaced/…) does not.\n// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\nfn controller_writer(\n    sid: u64,\n    send: SharedSend,\n    initial: Vec<(u64, Vec<u8>)>,\n    rx: Receiver<CtrlMsg>,\n    delivered_through: Arc<AtomicU64>,\n    epoch: u64,\n    controller_epoch: Arc<AtomicU64>,\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\n) {\n    // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): mark the exact window a\n    // BLOCKING socket write is in flight, so a reader can tell a WEDGED writer (brain\n    // conn suspended) from a merely idle one and stall-evict it. Set BEFORE the write,\n    // cleared AFTER — the mutex is never held across `write_frame` (that would just\n    // move the wedge onto the mutex).\n    let mark_blocked = |on: bool| {\n        if let Ok(mut g) = write_blocked_since.lock() {\n            *g = if on { Some(Instant::now()) } else { None };\n        }\n    };\n    for (seq, bytes) in &initial {\n        let frame = output_envelope(sid, *seq, bytes);\n        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\n        // epoch past ours) must write NOTHING further — otherwise its owned\n        // initial batch races the new writer on the same socket and reorders the\n        // seq stream. The gate closure runs AFTER the conn's serialized write\n        // gate is acquired and BEFORE any byte is written — the same\n        // serialization point the old epoch-check-under-`send.lock()` had: both\n        // writers serialize on the one conn gate, and `become_controller` bumps\n        // the epoch BEFORE spawning the new writer, so whichever superseded\n        // writer next holds the gate observes the bump and bails before\n        // committing — no check-then-block-then-write window. (A frame the\n        // stale writer committed BEFORE the bump is irreducible at the broker;\n        // fix #1 removes the contradictory double-take that produced it.)\n        // Round-4: the write itself is bounded + cancelable — a non-draining\n        // consumer poisons + retires the physical conn at the deadline instead\n        // of wedging this thread in the OS write forever.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        mark_blocked(true);\n        let outcome =\n            send.write_gated(&frame, || controller_epoch.load(Ordering::Acquire) == epoch);\n        mark_blocked(false);\n        match outcome {\n            Ok(ConnWrite::Done) => {}\n            Ok(ConnWrite::Superseded) => {\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=superseded\"),\n                );\n                return;\n            }\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n                );\n                return;\n            }\n        }\n        advance_delivered(&delivered_through, *seq);\n    }\n    while let Ok(msg) = rx.recv() {\n        let (frame, seq) = match &msg {\n            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),\n            CtrlMsg::Control(frame) => (frame, None),\n        };\n        // NO epoch gate on the live path (P1c): new output only ever flows to the\n        // CURRENT controller's channel (the drain clones `self.controller.tx`), so\n        // a superseded writer's channel holds only its pre-supersede output\n        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL\n        // control frames — notably the `Displaced` kick the displaced controller\n        // MUST still receive. The loop ends naturally when `tx` drops. Gating\n        // output here would suppress that `Displaced` (the cv-matrix loud-take\n        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.\n        mark_blocked(true);\n        let res = send.write(frame);\n        mark_blocked(false);\n        match res {\n            Ok(_) => {}\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n…\n                );\n…\n            }\n        }\n…\n    }\n…\n}"}],"details":{"displayContent":{"text":"    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\n    /// inline under the log lock. The writer thread advances the authoritative\n    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,\n    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\n    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\n    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\n    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\n    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\n    /// single live writer per connection and a monotonic on-wire seq stream,\n    /// [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]). Stamps `driven_by` to the new\n    /// controller's identity.\n    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {\n        // Drop the prior controller sink first (its writer's live loop ends when\n        // tx drops), and bump the generation so (a) an in-flight deadline-evict\n        // for the old controller can't unseat this one and (b) a prior writer\n        // still flushing its initial batch returns before its next frame.\n        // Attribution (F-039 legs b+d): the replaced conn's record names its\n        // successor, and the new conn accretes the controller role fact.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        if let Some(old) = self.controller.take() {\n            old.send.lifecycle_event(\n                \"controller-replaced\",\n                &format!(\"session={} new_conn={}\", self.session_id, sub.id()),\n            );\n        }\n        sub.describe(&format!(\n            \"controller session={} endpoint={} by={}\",\n            self.session_id,\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint },\n            by.as_deref().unwrap_or(\"local\")\n        ));\n        sub.lifecycle_event(\n            \"controller-attach\",\n            &format!(\"session={} from_seq={from_seq}\", self.session_id),\n        );\n        self.controller = None;\n        let epoch = self.controller_epoch.fetch_add(1, Ordering::AcqRel) + 1;\n        // #6 (ADR-0031): a COLD attach's initial batch is a single synthesized clean\n        // repaint of the CURRENT SCREEN (at pseudo-seq `next_seq - 1`) instead of the\n        // raw ring — replaying an alt-screen TUI's raw ring into a fresh terminal is\n        // exactly the corruption #6/#7/#8 report. A resume-from-floor (`from_seq > 0`)\n        // keeps the raw-ring exactly-once re-fetch untouched (B2). Either way the\n        // batch is owned by the writer thread (never pushed through the live channel —\n        // it must not block the drain); live frames stream raw + in-order afterwards.\n        let initial: Vec<(u64, Vec<u8>)> = match self.repaint_initial(from_seq) {\n            Some(repaint) => vec![repaint],\n            None => self\n                .ring\n                .iter()\n                .filter(|(s, _)| *s >= from_seq)\n                .cloned()\n                .collect(),\n        };\n        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n        let writer_send = Arc::clone(&sub);\n        let dt = Arc::clone(&self.delivered_through);\n        let epoch_gate = Arc::clone(&self.controller_epoch);\n        let sid = self.session_id;\n        // W2 decouple: the writer publishes its in-flight-write window here so a\n        // stall-evict can distinguish a wedged writer from an idle one.\n        let write_blocked_since = Arc::new(Mutex::new(None));\n        let writer_blocked = Arc::clone(&write_blocked_since);\n        let writer = thread::spawn(move || {\n            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\n        });\n        self.controller = Some(ControllerSink {\n            tx,\n            send: sub,\n            by,\n            last_ok: Instant::now(),\n            write_blocked_since,\n            _writer: writer,\n        });\n        self.stamp_driven_by();\n    }\n\n    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared\n    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per\n    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its\n    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an\n    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\n    fn clear_controller(&mut self) {\n        if self.controller.take().is_some() {\n            self.stamp_driven_by();\n        }\n    }\n\n    /// B-2 (REQ-CONTROLLER-LIVENESS-REAP): lazily drop a controller whose WRITER\n    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\n    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\n    /// writer exits when a socket write fails (a severed / keepalive-killed controller\n…\n    fn set_size_and_notify(&mut self, rows: u16, cols: u16) {\n…\n        if !evicted.is_empty() {\n            for vid in evicted {\n…\n                // (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).\n                if let Some(sink) = self.viewers.remove(&vid) {\n                    sink.evicted.store(true, Ordering::Release);\n                }\n            }\n            self.stamp_viewer_count();\n        }\n    }\n\n    /// Detach `sub` from whatever role it held: clear the controller slot iff it\n    /// is the current controller (a no-op if a newer brain took over — a late\n    /// disconnect can't unseat the live controller), and remove any viewer rows\n    /// owned by `sub`. Leaves `delivered_through` intact (D4-1). Re-stamps\n    /// `driven_by` / `viewer_count` when a role actually changed.\n    fn detach_if(&mut self, sub: &SharedSend) {\n        let is_ctrl = self\n            .controller\n            .as_ref()\n            .is_some_and(|c| Arc::ptr_eq(&c.send, sub));\n        if is_ctrl {\n            self.clear_controller(); // drops the sink (ends its writer) + unlatches\n        }\n        let before = self.viewers.len();\n        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));\n        // Bounded detach record (F-039 leg d) — only when a role actually changed.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        if is_ctrl || self.viewers.len() != before {\n            sub.lifecycle_event(\n                \"session-detach\",\n                &format!(\n                    \"session={} was_controller={is_ctrl} viewers_dropped={}\",\n                    self.session_id,\n                    before - self.viewers.len()\n                ),\n            );\n        }\n        if self.viewers.len() != before {\n            self.stamp_viewer_count();\n        }\n    }\n\n…\n/// cursor on each success), then drain live [`CtrlMsg`]s with a BLOCKING socket\n/// write each — so a backed-up controller blocks ONLY this thread, never the\n/// session drain (which used to write the controller inline under the log lock).\n/// A failed write (dead operator) ends the thread; the drain's next bounded\n/// send then sees `Disconnected` and detaches the row. `Output` advances\n/// `delivered_through`; `Control` (Displaced/…) does not.\n// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\nfn controller_writer(\n    sid: u64,\n    send: SharedSend,\n    initial: Vec<(u64, Vec<u8>)>,\n    rx: Receiver<CtrlMsg>,\n    delivered_through: Arc<AtomicU64>,\n    epoch: u64,\n    controller_epoch: Arc<AtomicU64>,\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\n) {\n    // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): mark the exact window a\n    // BLOCKING socket write is in flight, so a reader can tell a WEDGED writer (brain\n    // conn suspended) from a merely idle one and stall-evict it. Set BEFORE the write,\n    // cleared AFTER — the mutex is never held across `write_frame` (that would just\n    // move the wedge onto the mutex).\n    let mark_blocked = |on: bool| {\n        if let Ok(mut g) = write_blocked_since.lock() {\n            *g = if on { Some(Instant::now()) } else { None };\n        }\n    };\n    for (seq, bytes) in &initial {\n        let frame = output_envelope(sid, *seq, bytes);\n        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\n        // epoch past ours) must write NOTHING further — otherwise its owned\n        // initial batch races the new writer on the same socket and reorders the\n        // seq stream. The gate closure runs AFTER the conn's serialized write\n        // gate is acquired and BEFORE any byte is written — the same\n        // serialization point the old epoch-check-under-`send.lock()` had: both\n        // writers serialize on the one conn gate, and `become_controller` bumps\n        // the epoch BEFORE spawning the new writer, so whichever superseded\n        // writer next holds the gate observes the bump and bails before\n        // committing — no check-then-block-then-write window. (A frame the\n        // stale writer committed BEFORE the bump is irreducible at the broker;\n        // fix #1 removes the contradictory double-take that produced it.)\n        // Round-4: the write itself is bounded + cancelable — a non-draining\n        // consumer poisons + retires the physical conn at the deadline instead\n        // of wedging this thread in the OS write forever.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        mark_blocked(true);\n        let outcome =\n            send.write_gated(&frame, || controller_epoch.load(Ordering::Acquire) == epoch);\n        mark_blocked(false);\n        match outcome {\n            Ok(ConnWrite::Done) => {}\n            Ok(ConnWrite::Superseded) => {\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=superseded\"),\n                );\n                return;\n            }\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n                );\n                return;\n            }\n        }\n        advance_delivered(&delivered_through, *seq);\n    }\n    while let Ok(msg) = rx.recv() {\n        let (frame, seq) = match &msg {\n            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),\n            CtrlMsg::Control(frame) => (frame, None),\n        };\n        // NO epoch gate on the live path (P1c): new output only ever flows to the\n        // CURRENT controller's channel (the drain clones `self.controller.tx`), so\n        // a superseded writer's channel holds only its pre-supersede output\n        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL\n        // control frames — notably the `Displaced` kick the displaced controller\n        // MUST still receive. The loop ends naturally when `tx` drops. Gating\n        // output here would suppress that `Displaced` (the cv-matrix loud-take\n        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.\n        mark_blocked(true);\n        let res = send.write(frame);\n        mark_blocked(false);\n        match res {\n            Ok(_) => {}\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n…\n                );\n…\n            }\n        }\n…\n    }\n…\n}","startLine":880,"lineNumbers":[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,null,1226,null,1241,1242,null,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,null,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,null,1598,null,1600,1601,null,1610,null,1617]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"}}},"isError":false,"timestamp":1784196007250}}
{"type":"message","id":"ccba15c7","parentId":"00d46a11","timestamp":"2026-07-16T10:00:07.250Z","message":{"role":"toolResult","toolCallId":"call_MDQ6IAY9PuMjqMtbRtoVaDjc|fc_047bb49263dfa303016a58aba6bbb881989fce47187efd6585","toolName":"read","content":[{"type":"text","text":"fn input_writer(session: Arc<PtySession>, rx: Receiver<InputRecord>) {\n    while let Ok(bytes) = rx.recv() {\n        let _ = session.write_input(&bytes);\n    }\n}\n\n/// Drain the inject floor's buffered controller input to the PTY **after** an\n/// injected sequence, then release the floor. Each step takes the buffered bytes\n/// OFF the lock before ENQUEUEING it to the lone input-writer (the W1 rule: never\n/// hold a lock across a PTY write — now never block on it either); input arriving\n/// mid-flush keeps buffering, so strict order is preserved until the buffer\n/// drains empty and the floor frees.\n/// Lock the per-session inject floor, RECOVERING a poisoned mutex instead of\n/// panicking (B6, REQ-HAZARD-INJECT-WORKER-POISON). The floor is SHARED by the\n/// inject worker (`run_inject_worker` open / `flush_inject_floor`) and the\n/// controller-input path (`dispatch_input` Layer C `buffer_if_held`). A panic under\n/// the lock on EITHER poisons it; a bare `.lock().unwrap()` at the next site then\n/// panics too — and a panic in the inject WORKER kills its thread WITHOUT reaping\n/// the translation child, ORPHANING a live binary while `event_tx.send` fails, so\n/// force-native delivery reports \"worker-gone\" (delivered=false) FOREVER though the\n/// pid is alive (the F-e mis-diagnosed \"translation-binary liveness decay\").\n/// Recovering the guard (the effect-journal poison-tolerance idiom, bug #16 /\n/// REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE) keeps the floor usable so ONE panic cannot\n/// cascade the whole session's delivery dead.\n// [impl->REQ-HAZARD-INJECT-WORKER-POISON]\nfn lock_floor(floor: &Mutex<InjectFloor>) -> std::sync::MutexGuard<'_, InjectFloor> {\n    floor.lock().unwrap_or_else(|p| p.into_inner())\n}\n\n/// Poison-tolerant lock for ANY broker-resident mutex reachable from serve/dispatch\n/// (B-1, REQ-HAZARD-BROKER-FLOOR-LOCK-POISON). A brain-only self-update keeps the\n/// broker process — and every one of its `Mutex`es — ALIVE (REQ-UPD-3), so a single\n/// panic while another thread held one of these locks would otherwise POISON it\n/// permanently: the next `.lock().unwrap()` panics too, its per-conn reply thread\n/// dies, and EVERY subsequent attach silently deadlines (`spt rc` → \"brain IPC read\n/// deadline elapsed\", brain.rs) while non-locked ops keep working. The `into_inner`\n/// idiom (identical to [`lock_floor`] and the effect journal, bug #16) keeps the\n/// broker serving. Safe for the SHORT map ops it guards (`sessions` / `pair_holds`\n/// inserts+removes stay coherent on recovery — the same argument as the floor). The\n/// per-session `OutputLog` RING needs the extra coherence clamp of [`recover_log`].\n/// CLASS INVARIANT (KNOWN-HAZARDS): NO bare `.lock().unwrap()` on a broker-resident\n/// lock reachable from serve/dispatch — use `recover`/`recover_log` or a documented\n/// fail-fast justification.\n// [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\nfn recover<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {\n    m.lock().unwrap_or_else(|p| p.into_inner())\n}\n\n/// [`recover`] for the per-session [`OutputLog`] RING, with a COHERENCE CLAMP on the\n/// poison-recovery path (B-1, doyle ruling). A panic mid-[`OutputLog::append`] can\n/// leave the ring torn (a bumped `next_seq` past the ring, an over-cap ring from an\n/// interrupted prune, a non-monotonic front/back), and replaying those bytes to a\n/// subscriber risks garbage. So ONLY on the recovered (poisoned) branch, run\n/// [`OutputLog::clamp_or_reset`]: cheap-check the ring invariants and RESET it empty\n/// (+ loud log) on any violation. Lost scrollback self-heals on the next PTY output +\n/// repaint — strictly better than a permanent wedge (fail-fast reintroduces the very\n/// wedge B-1 kills) or a torn serve (blind recover). The healthy `Ok` path is a pure\n/// lock — no clamp, no cost on the hot append path.\n// [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\nfn recover_log(m: &Mutex<OutputLog>) -> std::sync::MutexGuard<'_, OutputLog> {\n    match m.lock() {\n        Ok(g) => g,\n        Err(p) => {\n            let mut g = p.into_inner();\n            let _ = g.clamp_or_reset(); // bool is for the unit test; the effect is the reset\n            g\n        }\n    }\n}\n\nfn flush_inject_floor(floor: &Mutex<InjectFloor>, input: &InputWriter) {\n    loop {\n        let chunk = lock_floor(floor).take_or_release();\n        match chunk {\n            Some(bytes) => {\n                input.enqueue(bytes);\n            }\n            None => break,\n        }\n    }\n}\n\n/// The inject worker (W2 Layer C): one thread per translation-bearing session. It\n/// drives inbound `<EVENT>`s through the binary and applies the binary's keystroke\n/// commands to the PTY ATOMICALLY w.r.t. the operator's live controller.\n///\n…\n\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\n    /// self-kick guard.\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n        let req: SubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\n        // Clone the log Arc out from under the sessions lock, then resolve under\n        // the log lock — never hold both locks at once.\n        //\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\n        // the per-session exit-waiter holds for the WHOLE life of a live child\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\n        // post-removal subscribe gets a prompt \"no such session\" error — both\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\n        // the rest.\n        let log = {\n            let sessions = recover(&self.sessions);\n            let h = sessions\n                .get(&req.session_id)\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\n            Arc::clone(&h.log)\n        };\n        let outcome = recover_log(&log).resolve_subscribe(\n            Arc::clone(send),\n            req.from_seq,\n            req.intent,\n            req.by,\n        );\n        send_frame(send, &subscribed_envelope(req.session_id, outcome));\n        Ok(req.session_id)\n    }\n\n    /// Release this connection's controller/viewer role on a session\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\n    /// a no-op if a newer controller already took over, or the session is gone.\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\n        let log = {\n            let sessions = recover(&self.sessions);\n            match sessions.get(&req.session_id) {\n                Some(h) => Arc::clone(&h.log),\n                None => {\n                    eprintln!(\"DBG unsubscribe: no session {}\", req.session_id);\n                    return Ok(());\n                }\n            }\n        };\n        recover_log(&log).detach_if(send);\n        Ok(())\n    }\n\n    /// Inject input bytes into a session's PTY.\n    ///\n    /// With an `op_id` the write is routed through the broker-owned\n    /// [`EffectJournal`] for exactly-once delivery: a replay of the same\n    /// `(session_id, op_id)` after a brain crash is deduped (the PTY is not\n    /// written twice), and either outcome — fresh apply or dedup — is acked back\n    /// with an [`crate::msg::KIND_APPLIED`] frame so the brain can retire the"}],"details":{"displayContent":{"text":"fn input_writer(session: Arc<PtySession>, rx: Receiver<InputRecord>) {\n    while let Ok(bytes) = rx.recv() {\n        let _ = session.write_input(&bytes);\n    }\n}\n\n/// Drain the inject floor's buffered controller input to the PTY **after** an\n/// injected sequence, then release the floor. Each step takes the buffered bytes\n/// OFF the lock before ENQUEUEING it to the lone input-writer (the W1 rule: never\n/// hold a lock across a PTY write — now never block on it either); input arriving\n/// mid-flush keeps buffering, so strict order is preserved until the buffer\n/// drains empty and the floor frees.\n/// Lock the per-session inject floor, RECOVERING a poisoned mutex instead of\n/// panicking (B6, REQ-HAZARD-INJECT-WORKER-POISON). The floor is SHARED by the\n/// inject worker (`run_inject_worker` open / `flush_inject_floor`) and the\n/// controller-input path (`dispatch_input` Layer C `buffer_if_held`). A panic under\n/// the lock on EITHER poisons it; a bare `.lock().unwrap()` at the next site then\n/// panics too — and a panic in the inject WORKER kills its thread WITHOUT reaping\n/// the translation child, ORPHANING a live binary while `event_tx.send` fails, so\n/// force-native delivery reports \"worker-gone\" (delivered=false) FOREVER though the\n/// pid is alive (the F-e mis-diagnosed \"translation-binary liveness decay\").\n/// Recovering the guard (the effect-journal poison-tolerance idiom, bug #16 /\n/// REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE) keeps the floor usable so ONE panic cannot\n/// cascade the whole session's delivery dead.\n// [impl->REQ-HAZARD-INJECT-WORKER-POISON]\nfn lock_floor(floor: &Mutex<InjectFloor>) -> std::sync::MutexGuard<'_, InjectFloor> {\n    floor.lock().unwrap_or_else(|p| p.into_inner())\n}\n\n/// Poison-tolerant lock for ANY broker-resident mutex reachable from serve/dispatch\n/// (B-1, REQ-HAZARD-BROKER-FLOOR-LOCK-POISON). A brain-only self-update keeps the\n/// broker process — and every one of its `Mutex`es — ALIVE (REQ-UPD-3), so a single\n/// panic while another thread held one of these locks would otherwise POISON it\n/// permanently: the next `.lock().unwrap()` panics too, its per-conn reply thread\n/// dies, and EVERY subsequent attach silently deadlines (`spt rc` → \"brain IPC read\n/// deadline elapsed\", brain.rs) while non-locked ops keep working. The `into_inner`\n/// idiom (identical to [`lock_floor`] and the effect journal, bug #16) keeps the\n/// broker serving. Safe for the SHORT map ops it guards (`sessions` / `pair_holds`\n/// inserts+removes stay coherent on recovery — the same argument as the floor). The\n/// per-session `OutputLog` RING needs the extra coherence clamp of [`recover_log`].\n/// CLASS INVARIANT (KNOWN-HAZARDS): NO bare `.lock().unwrap()` on a broker-resident\n/// lock reachable from serve/dispatch — use `recover`/`recover_log` or a documented\n/// fail-fast justification.\n// [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\nfn recover<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {\n    m.lock().unwrap_or_else(|p| p.into_inner())\n}\n\n/// [`recover`] for the per-session [`OutputLog`] RING, with a COHERENCE CLAMP on the\n/// poison-recovery path (B-1, doyle ruling). A panic mid-[`OutputLog::append`] can\n/// leave the ring torn (a bumped `next_seq` past the ring, an over-cap ring from an\n/// interrupted prune, a non-monotonic front/back), and replaying those bytes to a\n/// subscriber risks garbage. So ONLY on the recovered (poisoned) branch, run\n/// [`OutputLog::clamp_or_reset`]: cheap-check the ring invariants and RESET it empty\n/// (+ loud log) on any violation. Lost scrollback self-heals on the next PTY output +\n/// repaint — strictly better than a permanent wedge (fail-fast reintroduces the very\n/// wedge B-1 kills) or a torn serve (blind recover). The healthy `Ok` path is a pure\n/// lock — no clamp, no cost on the hot append path.\n// [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\nfn recover_log(m: &Mutex<OutputLog>) -> std::sync::MutexGuard<'_, OutputLog> {\n    match m.lock() {\n        Ok(g) => g,\n        Err(p) => {\n            let mut g = p.into_inner();\n            let _ = g.clamp_or_reset(); // bool is for the unit test; the effect is the reset\n            g\n        }\n    }\n}\n\nfn flush_inject_floor(floor: &Mutex<InjectFloor>, input: &InputWriter) {\n    loop {\n        let chunk = lock_floor(floor).take_or_release();\n        match chunk {\n            Some(bytes) => {\n                input.enqueue(bytes);\n            }\n            None => break,\n        }\n    }\n}\n\n/// The inject worker (W2 Layer C): one thread per translation-bearing session. It\n/// drives inbound `<EVENT>`s through the binary and applies the binary's keystroke\n/// commands to the PTY ATOMICALLY w.r.t. the operator's live controller.\n///\n…\n\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\n    /// self-kick guard.\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n        let req: SubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\n        // Clone the log Arc out from under the sessions lock, then resolve under\n        // the log lock — never hold both locks at once.\n        //\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\n        // the per-session exit-waiter holds for the WHOLE life of a live child\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\n        // post-removal subscribe gets a prompt \"no such session\" error — both\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\n        // the rest.\n        let log = {\n            let sessions = recover(&self.sessions);\n            let h = sessions\n                .get(&req.session_id)\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\n            Arc::clone(&h.log)\n        };\n        let outcome = recover_log(&log).resolve_subscribe(\n            Arc::clone(send),\n            req.from_seq,\n            req.intent,\n            req.by,\n        );\n        send_frame(send, &subscribed_envelope(req.session_id, outcome));\n        Ok(req.session_id)\n    }\n\n    /// Release this connection's controller/viewer role on a session\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\n    /// a no-op if a newer controller already took over, or the session is gone.\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\n        let log = {\n            let sessions = recover(&self.sessions);\n            match sessions.get(&req.session_id) {\n                Some(h) => Arc::clone(&h.log),\n                None => {\n                    eprintln!(\"DBG unsubscribe: no session {}\", req.session_id);\n                    return Ok(());\n                }\n            }\n        };\n        recover_log(&log).detach_if(send);\n        Ok(())\n    }\n\n    /// Inject input bytes into a session's PTY.\n    ///\n    /// With an `op_id` the write is routed through the broker-owned\n    /// [`EffectJournal`] for exactly-once delivery: a replay of the same\n    /// `(session_id, op_id)` after a brain crash is deduped (the PTY is not\n    /// written twice), and either outcome — fresh apply or dedup — is acked back\n    /// with an [`crate::msg::KIND_APPLIED`] frame so the brain can retire the","startLine":1800,"lineNumbers":[1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,null,3440,3441,3442,3443,3444,3445,3446,3447,3448,3449,3450,3451,3452,3453,3454,3455,3456,3457,3458,3459,3460,3461,3462,3463,3464,3465,3466,3467,3468,3469,3470,3471,3472,3473,3474,3475,3476,3477,3478,3479,3480,3481,3482,3483,3484,3485,3486,3487,3488,3489,3490,3491,3492,3493,3494,3495,3496,3497,3498,3499,3500,3501,3502,3503,3504,3505,3506,3507,3508,3509,3510]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"}}},"isError":false,"timestamp":1784196007250}}
{"type":"message","id":"e44f82a9","parentId":"ccba15c7","timestamp":"2026-07-16T10:00:07.251Z","message":{"role":"toolResult","toolCallId":"call_2LPU3FKjooR4pyOBn7kZmfoU|fc_047bb49263dfa303016a58aba6bb9081989ffb4a18119067e9","toolName":"grep","content":[{"type":"text","text":"No matches found"}],"details":{"scopePath":"crates/spt-daemon/src/nethost.rs, crates/spt-daemon/src/broker.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":0,"fileCount":0,"files":[],"truncated":false},"isError":false,"useless":true,"timestamp":1784196007251}}
{"type":"message","id":"1023f612","parentId":"e44f82a9","timestamp":"2026-07-16T10:00:07.264Z","message":{"role":"toolResult","toolCallId":"call_lp6vr0ox84ZhXsLGMCbxuxOB|fc_047bb49263dfa303016a58aba6bba08198820d69c6defc655d","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n\n## src/\n### nethost.rs\n 256|    /// Attach `sub` as the subscriber, replaying every retained chunk with\n*257|    /// `seq >= from_seq` (then eof, if already finished) before live streaming.\n 258|    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n 259|        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\n 260|        // record — this is one of the two RCA-pinned shared-carrier classes.\n...\n 1442|    /// `(bytes, finished)`; `finished` ⇒ the read side ended (the poller closes).\n*1443|    /// `None` ⇒ no such stream. Empty bytes + `finished == false` ⇒ nothing pending.\n 1444|    // [impl->REQ-SHELL-4]\n 1445|    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {\n 1446|        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n...\n 1698|        let infos = host.stream_infos();\n*1699|        assert_eq!(infos.len(), 2, \"operator + peer rows\");\n 1700|        let op = infos\n 1701|            .iter()\n 1702|            .find(|s| s.stream_id == op_stream)\n\n## tests/\n### netstream.rs\n 195|    let streams = receiver.net_streams().expect(\"net-streams\").streams;\n*196|    assert_eq!(streams.len(), 1, \"the stream survived the brain restart\");\n 197|    assert_eq!(streams[0].stream_id, b_stream);\n 198|    receiver\n 199|        .net_stream_subscribe(b_stream, cursor)\n### notifsync.rs\n 74|/// a receiver that previously dialed out must exclude its own ids or it reads\n*75|/// a stale stream and hangs (the D5b/D6c lesson — wait_for_stream_except).\n 76|fn wait_for_stream_except(brain: &mut Brain, known: &[u64]) -> u64 {\n 77|    for _ in 0..400 {\n 78|        let reply = brain.net_streams().expect(\"net-streams\");"}],"details":{"scopePath":"crates/spt-daemon/src/nethost.rs, crates/spt-daemon/tests, crates/spt/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":5,"fileCount":3,"files":["crates/spt-daemon/src/nethost.rs","crates/spt-daemon/tests/netstream.rs","crates/spt-daemon/tests/notifsync.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/nethost.rs","count":3},{"path":"crates/spt-daemon/tests/netstream.rs","count":1},{"path":"crates/spt-daemon/tests/notifsync.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/\n\n## src/\n### nethost.rs\n  256│    /// Attach `sub` as the subscriber, replaying every retained chunk with\n *257│    /// `seq >= from_seq` (then eof, if already finished) before live streaming.\n  258│    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n  259│        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\n  260│        // record — this is one of the two RCA-pinned shared-carrier classes.\n     │...\n 1442│    /// `(bytes, finished)`; `finished` ⇒ the read side ended (the poller closes).\n*1443│    /// `None` ⇒ no such stream. Empty bytes + `finished == false` ⇒ nothing pending.\n 1444│    // [impl->REQ-SHELL-4]\n 1445│    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {\n 1446│        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n     │...\n 1698│        let infos = host.stream_infos();\n*1699│        assert_eq!(infos.len(), 2, \"operator + peer rows\");\n 1700│        let op = infos\n 1701│            .iter()\n 1702│            .find(|s| s.stream_id == op_stream)\n\n## tests/\n### netstream.rs\n 195│    let streams = receiver.net_streams().expect(\"net-streams\").streams;\n*196│    assert_eq!(streams.len(), 1, \"the stream survived the brain restart\");\n 197│    assert_eq!(streams[0].stream_id, b_stream);\n 198│    receiver\n 199│        .net_stream_subscribe(b_stream, cursor)\n### notifsync.rs\n 74│/// a receiver that previously dialed out must exclude its own ids or it reads\n*75│/// a stale stream and hangs (the D5b/D6c lesson — wait_for_stream_except).\n 76│fn wait_for_stream_except(brain: &mut Brain, known: &[u64]) -> u64 {\n 77│    for _ in 0..400 {\n 78│        let reply = brain.net_streams().expect(\"net-streams\");"},"isError":false,"timestamp":1784196007264}}
{"type":"message","id":"cc99cdae","parentId":"1023f612","timestamp":"2026-07-16T10:00:11.497Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Investigating resolve_subscribe lines**"},{"type":"toolCall","id":"call_8zLtVziaf9Mrj7BkC9BvhAz5|fc_047bb49263dfa303016a58abaa6ddc819885ec2a454ec2759c","name":"grep","arguments":{"i":"Locating subscribe resolver definition","pattern":"resolve_subscribe","path":"crates/spt-daemon/src/broker.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":10112,"output":78,"cacheRead":151040,"cacheWrite":0,"totalTokens":161230,"reasoningTokens":18,"cost":{"input":0.05056,"output":0.00234,"cacheRead":0.07551999999999999,"cacheWrite":0,"total":0.12841999999999998}},"stopReason":"toolUse","timestamp":1784196007311,"responseId":"resp_047bb49263dfa303016a58aba7c1ac8198b2e57c87fc37befd","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58aba990f08198808f05060abcf128","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKuqews7_C8QM78vGaArCJOFut_f1xWzCnP9irDYiFoyTR0SGYDYkxt_IVu5VIhZK_huPxDLmETLnT_xsj9Ijb1SF9L0SNvR1HT3_zmmXjlEMDph-q9VGRxRWtRO0-J7-d29Z4fYl3cLWwKImh9K6LlQ5bDNTfbokr64GiiOsT0YfuTXcBmgyeeX6EVJilCbIUZ5WNNS3LQB3buAom3wcqOQETtZIeOj2xbmR0eijb3J3cOL3LIFPhK9k2r2LZWcBwNpBU3eqKdNVImhXENKN7eUCUPUjMSDROt2inOqM5zDM3_3yZnHtCAOGAUsjkCRDQpuQYTpUH4JdLhol05WqzqeYwfcI6DscjqrvMeRWuf3nvL4RKceYJpqM5U0oio7ZzRsAj6FWxiOoSmZ5STUbc2MVv7H_e_3mJrgyKta5etGKvjcVQ4zhb4I4bbk17W8I_DuL5EPMCk7P2w-YO0MwYPbe1sWms8gNdOdldVt4LE61Btdj0GrvHVsDtgRyCsRR4htzegf90mhyAbxD8GBiBbxGH1Zg13GmKs6D1GmebSBByvrIrdaRMg6JuR11f5G3xzTysvWCel5houxwywLLQ3MbSiN-HRZOjW0gEtMaLVjUkkOyTjuvkH5ws4jqFbLr-yRMyC9qwxBonXwwxYCkgy1pzTEiuZYr3B4g_oXiBB_HbDAqnVOhQBJZkYpqByddhlHIRw-9doRNk_33_bm5NgERtAY8KuaSmTFWFjzpP94DxbYYlZqxnmrT7N1_4V_2DObJGXouPpbakBsuO0sziYf2rvPgyaXS6v7cSnf4QecXJmF76CqMrY8MpOBJeb6eaKYRko4y0cPtCgGpDkWYb5Az6xImIhSh-VT6fwJ0ghmKrLTuH23QtCeyu8C2k3cojJ3ykKDqrczJuEz6LrpMpMdaojL7ZdvBI4XRa9e59FcFccXK5cG6HsC7dCv3C-24tD3hzGVH01-QvC6Nf1RRLZGW8x8GAOw9fcUU2r2qfliHnzCtMFlqAig28fHffl_KZ8a","summary":[{"type":"summary_text","text":"**Investigating resolve_subscribe lines**"}]},{"id":"fc_047bb49263dfa303016a58abaa6ddc819885ec2a454ec2759c","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating subscribe resolver definition\",\"pattern\":\"resolve_subscribe\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_8zLtVziaf9Mrj7BkC9BvhAz5","name":"grep"}]},"duration":4186.332600000314,"ttft":2231.610400000587,"contextSnapshot":{"promptTokens":161152,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_8zLtVziaf9Mrj7BkC9BvhAz5|fc_047bb49263dfa303016a58abaa6ddc819885ec2a454ec2759c","toolName":"grep","startedAt":"2026-07-16T10:00:11.497Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Locating subscribe resolver definition"},"id":"b16adadb","parentId":"cc99cdae","timestamp":"2026-07-16T10:00:11.497Z"}
{"type":"message","id":"6065d0a1","parentId":"b16adadb","timestamp":"2026-07-16T10:00:11.499Z","message":{"role":"toolResult","toolCallId":"call_8zLtVziaf9Mrj7BkC9BvhAz5|fc_047bb49263dfa303016a58abaa6ddc819885ec2a454ec2759c","toolName":"grep","content":[{"type":"text","text":" 858|    /// or `None` when undriven. The `by`-keyed ownership logic in\n*859|    /// [`resolve_subscribe`] reads this.\n 860|    fn controller_by(&self) -> Option<&str> {\n 861|        self.controller.as_ref().and_then(|c| c.by.as_deref())\n 862|    }\n...\n 1147|    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.\n*1148|    fn resolve_subscribe(\n 1149|        &mut self,\n 1150|        sub: SharedSend,\n 1151|        from_seq: u64,\n...\n 3470|        };\n*3471|        let outcome = recover_log(&log).resolve_subscribe(\n 3472|            Arc::clone(send),\n 3473|            req.from_seq,\n 3474|            req.intent,\n...\n 5855|        let outcome =\n*5856|            log.resolve_subscribe(nsend, 0, AttachIntent::Control, Some(\"newcomer\".to_string()));\n 5857|\n 5858|        assert_eq!(\n 5859|            outcome,"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":4,"fileCount":1,"files":["crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":4}],"truncated":false,"displayContent":"  858│    /// or `None` when undriven. The `by`-keyed ownership logic in\n *859│    /// [`resolve_subscribe`] reads this.\n  860│    fn controller_by(&self) -> Option<&str> {\n  861│        self.controller.as_ref().and_then(|c| c.by.as_deref())\n  862│    }\n     │...\n 1147│    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.\n*1148│    fn resolve_subscribe(\n 1149│        &mut self,\n 1150│        sub: SharedSend,\n 1151│        from_seq: u64,\n     │...\n 3470│        };\n*3471│        let outcome = recover_log(&log).resolve_subscribe(\n 3472│            Arc::clone(send),\n 3473│            req.from_seq,\n 3474│            req.intent,\n     │...\n 5855│        let outcome =\n*5856│            log.resolve_subscribe(nsend, 0, AttachIntent::Control, Some(\"newcomer\".to_string()));\n 5857│\n 5858│        assert_eq!(\n 5859│            outcome,"},"isError":false,"timestamp":1784196011498}}
{"type":"message","id":"b38b8402","parentId":"6065d0a1","timestamp":"2026-07-16T10:00:15.164Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_zRDcqxwnafilq8eTYD87I72W|fc_047bb49263dfa303016a58abadff388198a95f15965123fdeb","name":"read","arguments":{"i":"Reading same origin takeover rules","path":"crates/spt-daemon/src/broker.rs","selector":"1128-1208"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3289,"output":42,"cacheRead":158208,"cacheWrite":0,"totalTokens":161539,"cost":{"input":0.016445,"output":0.00126,"cacheRead":0.079104,"cacheWrite":0,"total":0.09680899999999999}},"stopReason":"toolUse","timestamp":1784196011513,"responseId":"resp_047bb49263dfa303016a58abac1c788198b841f149460e7607","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_047bb49263dfa303016a58abadff388198a95f15965123fdeb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading same origin takeover rules\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"1128-1208\"}","call_id":"call_zRDcqxwnafilq8eTYD87I72W","name":"read"}]},"duration":3641.210299999453,"ttft":2460.125500000082,"contextSnapshot":{"promptTokens":161497,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_zRDcqxwnafilq8eTYD87I72W|fc_047bb49263dfa303016a58abadff388198a95f15965123fdeb","toolName":"read","startedAt":"2026-07-16T10:00:15.164Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading same origin takeover rules"},"id":"ad9b7254","parentId":"b38b8402","timestamp":"2026-07-16T10:00:15.164Z"}
{"type":"message","id":"559529f0","parentId":"ad9b7254","timestamp":"2026-07-16T10:00:15.207Z","message":{"role":"toolResult","toolCallId":"call_zRDcqxwnafilq8eTYD87I72W|fc_047bb49263dfa303016a58abadff388198a95f15965123fdeb","toolName":"read","content":[{"type":"text","text":"    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {\n…\n        let evicted = Arc::new(AtomicBool::new(false));\n        let writer_evicted = Arc::clone(&evicted);\n        let session_id = self.session_id;\n        let writer =\n            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));\n        self.viewers.insert(\n            vid,\n            ViewerSink {\n                tx,\n                send: sub,\n                evicted,\n                _writer: writer,\n            },\n        );\n    }\n\n    /// Resolve a controller/viewer subscribe under the W2.5 model\n    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs\n    /// the controller take / viewer add / loud displace as a side effect. See the\n    /// identity matrix in the body — the `by`-keyed logic is what makes a\n    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.\n    fn resolve_subscribe(\n        &mut self,\n        sub: SharedSend,\n        from_seq: u64,\n        intent: AttachIntent,\n        by: Option<String>,\n    ) -> SubscribeOutcome {\n        if intent == AttachIntent::Viewer {\n            if self.viewers.len() >= MAX_VIEWERS {\n                // Soft cap reached — refuse the viewer (bounds thread count).\n                return SubscribeOutcome::BusyControlled {\n                    by: \"viewer cap reached\".to_string(),\n                };\n            }\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        // Control or Take.\n        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller WEDGED\n        // on a suspended/black-holed brain conn past BRAIN_WRITE_DEADLINE is\n        // stall-evicted HERE — the take/reattach self-heals against broker truth\n        // without waiting for new output (the output-driven mark_controller_gone\n        // never fires on an idle session). Inline stamp: this path already writes the\n        // perch under the log lock via become_controller. After the evict the slot\n        // reads free and the normal free/take logic below runs.\n        self.stall_evict_controller(Instant::now(), StampMode::Inline);\n        if self.controller.is_none() {\n            // Free endpoint → become controller (nothing to displace).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        let same_identity = self.controller_by() == by.as_deref();\n        if same_identity {\n            // Same operator (or both local) re-serving — silent re-take. This is\n            // the gate-#7 path: a successor re-taking the slot after a brain\n            // restart NEVER fires Displaced.\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        if by.is_none() {\n            // The LOCAL brain re-attaching while a REMOTE controls — never steal;\n            // fall back to a silent viewer (gate #7 for the local-resume path).\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        if self.controller_by().is_none() {\n            // A remote takes an UNDRIVEN session (only the local spawner held the\n            // slot) — silent control, no loud displace (there is no remote driver\n            // to notify).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        // A DIFFERENT remote controls it.\n        match intent {\n            AttachIntent::Take => {\n                // The ONLY loud-displace path: notify the incumbent, then take.\n                // W1: route the Displaced notice through the OLD controller's\n                // writer thread (best-effort, off the dispatch thread) — the\n                // incumbent we're kicking may be exactly the wedged one, so an\n                // inline write here would block the new attacher's dispatch.\n                if let Some(old) = &self.controller {\n                    let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(\"\"));\n…\n                }\n…\n            }\n…\n        }\n    }\n\n[Showing lines 1127-1211 of 6009. Use :1212 to continue]"}],"details":{"truncation":{"content":"        let evicted = Arc::new(AtomicBool::new(false));\r\n        let writer_evicted = Arc::clone(&evicted);\r\n        let session_id = self.session_id;\r\n        let writer =\r\n            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));\r\n        self.viewers.insert(\r\n            vid,\r\n            ViewerSink {\r\n                tx,\r\n                send: sub,\r\n                evicted,\r\n                _writer: writer,\r\n            },\r\n        );\r\n    }\r\n\r\n    /// Resolve a controller/viewer subscribe under the W2.5 model\r\n    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs\r\n    /// the controller take / viewer add / loud displace as a side effect. See the\r\n    /// identity matrix in the body — the `by`-keyed logic is what makes a\r\n    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.\r\n    fn resolve_subscribe(\r\n        &mut self,\r\n        sub: SharedSend,\r\n        from_seq: u64,\r\n        intent: AttachIntent,\r\n        by: Option<String>,\r\n    ) -> SubscribeOutcome {\r\n        if intent == AttachIntent::Viewer {\r\n            if self.viewers.len() >= MAX_VIEWERS {\r\n                // Soft cap reached — refuse the viewer (bounds thread count).\r\n                return SubscribeOutcome::BusyControlled {\r\n                    by: \"viewer cap reached\".to_string(),\r\n                };\r\n            }\r\n            self.add_viewer(sub, from_seq);\r\n            self.stamp_viewer_count();\r\n            return SubscribeOutcome::Viewer;\r\n        }\r\n        // Control or Take.\r\n        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller WEDGED\r\n        // on a suspended/black-holed brain conn past BRAIN_WRITE_DEADLINE is\r\n        // stall-evicted HERE — the take/reattach self-heals against broker truth\r\n        // without waiting for new output (the output-driven mark_controller_gone\r\n        // never fires on an idle session). Inline stamp: this path already writes the\r\n        // perch under the log lock via become_controller. After the evict the slot\r\n        // reads free and the normal free/take logic below runs.\r\n        self.stall_evict_controller(Instant::now(), StampMode::Inline);\r\n        if self.controller.is_none() {\r\n            // Free endpoint → become controller (nothing to displace).\r\n            self.become_controller(sub, by, from_seq);\r\n            return SubscribeOutcome::Controller;\r\n        }\r\n        let same_identity = self.controller_by() == by.as_deref();\r\n        if same_identity {\r\n            // Same operator (or both local) re-serving — silent re-take. This is\r\n            // the gate-#7 path: a successor re-taking the slot after a brain\r\n            // restart NEVER fires Displaced.\r\n            self.become_controller(sub, by, from_seq);\r\n            return SubscribeOutcome::Controller;\r\n        }\r\n        if by.is_none() {\r\n            // The LOCAL brain re-attaching while a REMOTE controls — never steal;\r\n            // fall back to a silent viewer (gate #7 for the local-resume path).\r\n            self.add_viewer(sub, from_seq);\r\n            self.stamp_viewer_count();\r\n            return SubscribeOutcome::Viewer;\r\n        }\r\n        if self.controller_by().is_none() {\r\n            // A remote takes an UNDRIVEN session (only the local spawner held the\r\n            // slot) — silent control, no loud displace (there is no remote driver\r\n            // to notify).\r\n            self.become_controller(sub, by, from_seq);\r\n            return SubscribeOutcome::Controller;\r\n        }\r\n        // A DIFFERENT remote controls it.\r\n        match intent {\r\n            AttachIntent::Take => {\r\n                // The ONLY loud-displace path: notify the incumbent, then take.\r\n                // W1: route the Displaced notice through the OLD controller's\r\n                // writer thread (best-effort, off the dispatch thread) — the\r\n                // incumbent we're kicking may be exactly the wedged one, so an\r\n                // inline write here would block the new attacher's dispatch.\r\n                if let Some(old) = &self.controller {\r\n                    let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(\"\"));\r","truncated":true,"truncatedBy":"lines","totalLines":4883,"totalBytes":4242,"outputLines":85,"outputBytes":4242,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {\n…\n        let evicted = Arc::new(AtomicBool::new(false));\r\n        let writer_evicted = Arc::clone(&evicted);\r\n        let session_id = self.session_id;\r\n        let writer =\r\n            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));\r\n        self.viewers.insert(\r\n            vid,\r\n            ViewerSink {\r\n                tx,\r\n                send: sub,\r\n                evicted,\r\n                _writer: writer,\r\n            },\r\n        );\r\n    }\r\n\r\n    /// Resolve a controller/viewer subscribe under the W2.5 model\r\n    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs\r\n    /// the controller take / viewer add / loud displace as a side effect. See the\r\n    /// identity matrix in the body — the `by`-keyed logic is what makes a\r\n    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.\r\n    fn resolve_subscribe(\r\n        &mut self,\r\n        sub: SharedSend,\r\n        from_seq: u64,\r\n        intent: AttachIntent,\r\n        by: Option<String>,\r\n    ) -> SubscribeOutcome {\r\n        if intent == AttachIntent::Viewer {\r\n            if self.viewers.len() >= MAX_VIEWERS {\r\n                // Soft cap reached — refuse the viewer (bounds thread count).\r\n                return SubscribeOutcome::BusyControlled {\r\n                    by: \"viewer cap reached\".to_string(),\r\n                };\r\n            }\r\n            self.add_viewer(sub, from_seq);\r\n            self.stamp_viewer_count();\r\n            return SubscribeOutcome::Viewer;\r\n        }\r\n        // Control or Take.\r\n        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller WEDGED\r\n        // on a suspended/black-holed brain conn past BRAIN_WRITE_DEADLINE is\r\n        // stall-evicted HERE — the take/reattach self-heals against broker truth\r\n        // without waiting for new output (the output-driven mark_controller_gone\r\n        // never fires on an idle session). Inline stamp: this path already writes the\r\n        // perch under the log lock via become_controller. After the evict the slot\r\n        // reads free and the normal free/take logic below runs.\r\n        self.stall_evict_controller(Instant::now(), StampMode::Inline);\r\n        if self.controller.is_none() {\r\n            // Free endpoint → become controller (nothing to displace).\r\n            self.become_controller(sub, by, from_seq);\r\n            return SubscribeOutcome::Controller;\r\n        }\r\n        let same_identity = self.controller_by() == by.as_deref();\r\n        if same_identity {\r\n            // Same operator (or both local) re-serving — silent re-take. This is\r\n            // the gate-#7 path: a successor re-taking the slot after a brain\r\n            // restart NEVER fires Displaced.\r\n            self.become_controller(sub, by, from_seq);\r\n            return SubscribeOutcome::Controller;\r\n        }\r\n        if by.is_none() {\r\n            // The LOCAL brain re-attaching while a REMOTE controls — never steal;\r\n            // fall back to a silent viewer (gate #7 for the local-resume path).\r\n            self.add_viewer(sub, from_seq);\r\n            self.stamp_viewer_count();\r\n            return SubscribeOutcome::Viewer;\r\n        }\r\n        if self.controller_by().is_none() {\r\n            // A remote takes an UNDRIVEN session (only the local spawner held the\r\n            // slot) — silent control, no loud displace (there is no remote driver\r\n            // to notify).\r\n            self.become_controller(sub, by, from_seq);\r\n            return SubscribeOutcome::Controller;\r\n        }\r\n        // A DIFFERENT remote controls it.\r\n        match intent {\r\n            AttachIntent::Take => {\r\n                // The ONLY loud-displace path: notify the incumbent, then take.\r\n                // W1: route the Displaced notice through the OLD controller's\r\n                // writer thread (best-effort, off the dispatch thread) — the\r\n                // incumbent we're kicking may be exactly the wedged one, so an\r\n                // inline write here would block the new attacher's dispatch.\r\n                if let Some(old) = &self.controller {\r\n                    let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(\"\"));\r\n…\n                }\n…\n            }\n…\n        }\n    }","startLine":1096,"lineNumbers":[1096,null,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,null,1213,null,1216,null,1220,1221]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6009,"totalBytes":4242,"outputLines":85,"outputBytes":4242,"shownRange":{"start":1127,"end":1211},"nextOffset":1212}}},"isError":false,"timestamp":1784196015206}}
{"type":"message","id":"7de4ef8d","parentId":"559529f0","timestamp":"2026-07-16T10:00:27.507Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying stale row replay race as root cause**"},{"type":"toolCall","id":"call_XYKVLUWEYfq9FFgrLKNnEvxb|fc_047bb49263dfa303016a58abb3dd4c81988fdaf9eafb209e6e","name":"irc","arguments":{"i":"Reporting stronger stale stream root","op":"send","to":"Main","message":"Stronger exact root candidate found: `NetShared.streams` NEVER removes finished stream rows (`StreamLog::finish` only sets `finished=true`; no `streams.remove` exists). Every fresh dispatcher resets `claimed` and reclaims ALL historical peer attach streams concurrently. A finished old rc stream whose Request is retained runs `serve_attach`: replay Request -> `attach_as(Control, same origin)` -> `resolve_subscribe` same_identity silently `become_controller`, replacing the legitimate current active worker with NO Displaced; then replay EOF -> `detach_session`, clearing its newly-taken controller. The active serve loop remains open and believes `role=Controller`, but its sink was superseded, so it receives no PTY output forever. Detach+new rc creates a new stream worker after the stale replay storm and retakes. Partial 4/5 maps to endpoints having historical finished attach streams vs one without (or per-worker race ordering). Separate adjacent root remains Request eviction/gap: old active streams over 4096 inbound chunks cannot be reclassified, `peek_first_line(0)` errors gap and one-shot `claimed` prevents retry. Both are safely falsifiable in one broker/dispatcher restart harness.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3268,"output":320,"cacheRead":159232,"cacheWrite":0,"totalTokens":162820,"reasoningTokens":20,"cost":{"input":0.01634,"output":0.009600000000000001,"cacheRead":0.07961599999999999,"cacheWrite":0,"total":0.105556}},"stopReason":"toolUse","timestamp":1784196015221,"responseId":"resp_047bb49263dfa303016a58abaff9288198a31efdce091b44d1","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58abb2c7f88198a105bd02c8ba68b6","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKuzaSKsEzOpi4TaEhRnVfZr73pjFbU2WB5i6Lga1Y4mZu-OzncSdmS7mHOGZiuo918gtdw23gSCrJhb8ZuI4vgdGyY77M7GutvrQjzVU8d6xeEe-Mij4kXp-ntdf3arBdE2LKuppF2cGNrqxhDMTOF-4gtW94cScvCCHQt6CoW2WYGkDcaVw4m6woKKr7xTv2LJRKcG7BmeUKMgg77mnctLyMrSXbG3an6X0Iuv0WcQJbk0wPS90lczwhcG2Co2VcqnvKrQLTY5uvjM7hj2FG7K6pcs2vn6zqXf-8_3BB-_mMz0S2Wj0G7RuY1X3peMs7Gi6vkUD8C-DUcK2tEbUH6VZGeChJ9MHeAhtku7D1_UvSZlfD8PlZV8jxrIwN-HceOjcWUVSjdBzU-bXy0fvOP5vPzmIti-cJ8T4uL5Rpe_-ZVkRzTBC6V8JDZFW6kf2NbQx2xOygCfimbIdLi5F7OvoiJjcXgYuEYBogmDFj0cNjHkHtAe5nav_msjGoFSxBDLPpIGrwhEEkcfc19h86XCcRXbCVvcs8OLJ40ls6l61-nZbxEmRcXc5lWNVQeqB9iIqvkin116fG0yjVLglIvJCpTWTOaEin6rCnZYwyKEdCM1kfJSyM0ofeKE9WtsXXBlDRSK6HTXmgI2sJrpsLEYJvyHzDADICG2lqfloh6vCtbkGj0ai4SSNMy1KxgyxV0v8aUWiNklzmCpkm4Goz5wCwjRIq9-usHxSka3J5SJnLk7pk-MPsWn4MS0BAoCRQpDMSQADnUnTgUbtrCAfwrmb9rV8hyr2SWC-Dv5EVs0BCEtKLswVu70dsDVaZhavWmTG5HMQLx0PAie9_zXVxtrXVF1Iw-znP5kzz8W6U_K6uqwYBSrUvZRT6lGHfagyLFTh1e_V75NJREyqOHeI9zYpUfrGp5j2E5zvu81X5NCY8zCW4JyhGKO9xM0Z9_kbbtJkqCfjjzkCxQ-HuQAsAhgpgpn4T_iHVD8IqVyNVJG2LEH3UoiMICf0XBvNWhts64R2uhKTOwizuCaK7-Ei8pMow==","summary":[{"type":"summary_text","text":"**Identifying stale row replay race as root cause**"}]},{"id":"fc_047bb49263dfa303016a58abb3dd4c81988fdaf9eafb209e6e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reporting stronger stale stream root\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Stronger exact root candidate found: `NetShared.streams` NEVER removes finished stream rows (`StreamLog::finish` only sets `finished=true`; no `streams.remove` exists). Every fresh dispatcher resets `claimed` and reclaims ALL historical peer attach streams concurrently. A finished old rc stream whose Request is retained runs `serve_attach`: replay Request -> `attach_as(Control, same origin)` -> `resolve_subscribe` same_identity silently `become_controller`, replacing the legitimate current active worker with NO Displaced; then replay EOF -> `detach_session`, clearing its newly-taken controller. The active serve loop remains open and believes `role=Controller`, but its sink was superseded, so it receives no PTY output forever. Detach+new rc creates a new stream worker after the stale replay storm and retakes. Partial 4/5 maps to endpoints having historical finished attach streams vs one without (or per-worker race ordering). Separate adjacent root remains Request eviction/gap: old active streams over 4096 inbound chunks cannot be reclassified, `peek_first_line(0)` errors gap and one-shot `claimed` prevents retry. Both are safely falsifiable in one broker/dispatcher restart harness.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_XYKVLUWEYfq9FFgrLKNnEvxb","name":"irc"}]},"duration":12284.603400000371,"ttft":3538.2454000003636,"contextSnapshot":{"promptTokens":162500,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_XYKVLUWEYfq9FFgrLKNnEvxb|fc_047bb49263dfa303016a58abb3dd4c81988fdaf9eafb209e6e","toolName":"irc","startedAt":"2026-07-16T10:00:27.507Z","intent":"Reporting stronger stale stream root"},"id":"18aecb83","parentId":"7de4ef8d","timestamp":"2026-07-16T10:00:27.507Z"}
{"type":"message","id":"b2e50f33","parentId":"18aecb83","timestamp":"2026-07-16T10:00:27.508Z","message":{"role":"toolResult","toolCallId":"call_XYKVLUWEYfq9FFgrLKNnEvxb|fc_047bb49263dfa303016a58abb3dd4c81988fdaf9eafb209e6e","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"PtyControl","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784196027507}}
{"type":"message","id":"d92414b4","parentId":"b2e50f33","timestamp":"2026-07-16T10:00:55.655Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing rc stream ordering and completion**\n\n**Evaluating stale and active stream race conditions**\n\n**Testing stream handling with dispatcher stop**"},{"type":"thinking","thinking":"**Designing isolated refresh harness**\n\n**Planning deterministic broker eviction test**"},{"type":"toolCall","id":"call_no4CVE2T9uUpiek4dGs5ivKA|fc_047bb49263dfa303016a58abd63d908198be797cc826c7b6c7","name":"grep","arguments":{"i":"Finding net ring cap seam","pattern":"ring_cap|with.*cap|start.*ring|DEFAULT_STREAM_RING_CHUNKS","path":"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/tests","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":2586,"output":892,"cacheRead":160256,"cacheWrite":0,"totalTokens":163734,"reasoningTokens":809,"cost":{"input":0.01293,"output":0.02676,"cacheRead":0.08012799999999999,"cacheWrite":0,"total":0.119818}},"stopReason":"toolUse","timestamp":1784196027515,"responseId":"resp_047bb49263dfa303016a58abbbf6408198b4537143985c5432","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58abbdaa508198a9cdf45e87eb6711","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKvOmr_WXxsQ6K3P25tBhCZRQL1sepUdE4fWCN1nURh3NUqcacWPsYaksWPZwPfWAfFhHZ2dS3OIz776Nzg60q6Tj6qjlJPMMiQfIDaVUIrng8tyvnQg7aVyoJ1XDy7ACfrm6c3SxK7_4dfXpb4T0rSBTcpzHFs2OPyc-kcbd9wA3ItzKnaf59bgVmf8EcwRpGdnBPz1SDYRBenIvrkUWRMwtNY7FXRF47yA39hZXK78809otBmM5M_sRvdIwEtmq8EEmB9AzzgsbWUMui9keMGcgOgQrnDT6dIVUYsNg8AcSo4ExmuQNTYDchmGGAyC6fYArBl8t6NSo1t86qFBHN2FBekvpwgJvDWieOi76VwqY0giPC3jN607soM7-E8Cx0fhDLE_dq20_p2mLw8rM9Xoe6nmEgGBJ9hdE3J3t8-8tkwS8c_8GeL3elzC0Uv48PkmPrprwIM_5lAwBElwtZp2Y24rJCfNA7CMfTFs6X_Dk8ARziSD2TazB6KIxCM-VywvJd91nn_2JVt6SnW-JR1Jt4Y9z9usdyNfO9meE69Zj-HqHjiHyX6yUWX5BszeDqyxkas7ZtIGu7XpXbbroggDvqYSuSU13ZUKm7PJLmT0CyNWaA0trObAtX5gsID8j4u9Oim57cqsKTqnerhrM_y_gPf_4wgEILKoJ_Z5lQ_7bhDCpxwLWk9u_0ulPZ7y3Py7jZTQudxBnkCLoxwQRKDCmhlyvgiJwcmMeeHFlAsfDL4To_Xi1m73ymHZAJaBuZIWMzaJgcujaZo3BsBpZBG7kjwTSenwCcWgB1KrvyuUd0LrjLZovB-VXzW9QZZ1uQeYEP_osHTjpiY6AITKwX7FrhZLEg1jevIuuBkBEpXHnm3S9hnHtk02AmWVAzTvDcK88dW158RTTYntaEoNyXYYdA9lZbxNJL_fdVkDAFKu2gkPDbjo_wI05L6Y4qr_fSeHRL3KaZOr0PuUdXpfeKHHj6meYzdyh3uw7O9gf_gIfzkXhUBHiwPLoJLeC0zjjKZxdnnSzkyEoXdjKrTSfrJ5IFZpSegr4dEGZZZD71nIK0zkDrrgpM7llOPG-QDkH211BaaIyKOrVNF70CzNehxYADkGG1G5ndCft5pl5wk_z0xZm7vz8Dy8ky1-YuK6BWrYFX09Aso6AV_rEWzK8KgyUGdy_vCqyTcyRVDnODJOhepBbv1iLC0vHrnciCsA9-4fo2PLI6vkqIU-DgtVOyrtUuNo8oZN7VOsh973AZj3UF-JuqImScy3SkWDaCQtsfK2iCFRNnbNZI_j0SC9yg_o1m-lvC-P_fkFX3kKRLu5bqM85fpiw5GhJKx_OC7c3ZvV7D7huQlxwVC1GIqRLyv1fdco7AaNE1xe1ftbTJ8-Ec5V1wa6ZFXF8aajDMsYahTWdy10oqff-ERRHsXHtngJXxG_4sI529vQ256vFrc7rZtD6XZ2Do-6KJVA18tI-nyXXnOp6mM3DXZtyoPECTQWUKp3skGV75-gdDP5obJVEael-EfeioCexjuM25ZsxjKnKVMhlNd0_r8gVbLAxeE6BjGtjpSToCLSoTZ76H3B3ozLrvFHCNsaVdOBkZg6tRcnQutewgtPSWKchCixULZ3fltXhbAkf6mkGeGZ7WEl8BvdmJA7JebI0UvjdhQqw5DnkBVy3Tnqy7Jv89QJ_V4Zqs5CDAJT6SFWFt3MBeC5FSj7PPbLo1muU_oWglUotkG6pUuWxgVo2JMBEl2FsOcwnkOZE6SGyVIRDv2YBk0nNL2bJUjbPpoJZoHZqU60-3ng1unNOCYxS943vzUaJbNiljNMxI6UueQZHJag4uv-7pO5tRoIKmcw4JphLRqVx1l6XTMcKFYP3o-FekWnY0X0voU6IZRRtFjtpHCQio1SMhn2aiB33PNCHds2c7mKj8fYW-zBq8BTdUGkdCTqHzVfJ_dZ53DquELgYJFruwtpsbGxY6K0M4idBZt4qdPos1bkYHZAi1VBsQnFOtaZ7dnTP6d0oLdiJPqOsIcxNx6-Jc7mYmuZ28kjkF5oMAMKGxarP3S1wlO4aVtmVwO9OtYSwDfrqCVtMI2WnMrEEVab9dGqvPqVHEpJoVj-hdIXzWJV095HsnmTANvua1Resj2loal4aDISNV99PBjiTviWi4_wawPSgE9aFW6GGEuIICbAU_rHPbhNzhMrV_16wy5xt8L5mcWvG5DAsG4lRaadCajS5n0tB_e6KaCobK2cei3K0bp55Gb5FMj-s_nF2EnJzdt9X50MnKDMbnr_8fvKm0k6Y1q3ulMHqdRNDKrCmjsz5WPd-OTP7smTY-WG82HUBLdLXLQrmHSjmAB-5RxA19Z3DzukZ87i47y69ks1uclnYtYZloNJlrIwZRHaMtckpBrQR6JnD3ZE0nq4JL_ifWeuW9BFq4lnCh8dy5FAYb9EvdYcjAuVpFdJEBZxMtF45660xs6bsxT35LmLy2_yrPDxT6NdeR7ayKsU1dqNkTTGHyJ2co1-PkfBW-BadUsL6LYzQz-l1n_OxdHBw-D-LroRIKiA-TZ_tt90sx_yAd-KmzR4H0c_ujvnJz1J0nrdCa5pnQj6TMB9aF2bKzaI2B2UhEeZT49ouhEI6frKjVW9olUuotTaSoLHjv7LrT8RElJKD7jSttQpYguyCRrHzZ6iq-CYTJCs298mmf6kMy9Sq3tyrQqT4e9MpaV3ZLc1sWGYjdRpcgvwNaXQOim2zZQ1m8yG66qFmKN9G5a1Zk1-EkCPK_tctUtmuS9Ziw9Lmv_qgO5PybZDpQXHCETITdXy5Lq7nX9-4W3RY6Gv5iYApA8bbxmVbR3KdzMvdxa0Xu4fviTJl7JhTkmkNFFeEmQxoRAUWyM25_zYShAuEdjxnwjwDW9HdC-tnLD89CH27VilQjLtShg1UEEttd3GEnRblHaClvYn0Za5yv_P5qeBj_P4c2at1lNI99LdO4O_ixzA9Xvl5cYi-yJVx7yqpfUw2lwSAT7KkMjD_9D8GVOPtt6mdJc3rcMZakpE-D6KYaz5v34c0XrWpvEQ-ziG0723cN4gYiFCJTa64vzAJgrA3bfd7mHoBaQjB-5v1FApdrUCj2WD2u6o6Sskze-I2G6BEH_eXsQCi-g55W69iVF-rt1JJLrTukB9TAZkH0jpa5xFbiBgFLdwSRALHKH985jH1Js9cwk1reEkWl7KRqRxKi-2TwiPmJnc6xjMwuATfX_YQj6Be511MReVPzcGBMO_P_2bHwMe0LF29VQWrng3wa6Vj8jFGmeB3qD--EFkgBymwfUA9VcE9HFnqYOpw1TyKEBxb4rHu-tNA1m72ecvotvEBKW9u-UrPebz40nU-CEzFFklP5rDMwTan2n1pmpQmbSqK5NZWt3lLXQnYkKHR06dluP-lctH0NKydWDkSMbkLO_8LH9hgE4YZYZjwhvYcfQ8GlTfrzp5ePwydoZuFk2ZSs0TImyrSNzghxw5pCSaq0DczpOe4jMWcJ9G52NL5-zK23meutD757tK-vwxXprcNcnbE2oYXdHYUXUFhQFoH35toblraw5i5RUbFLyULxdN_wrBsmbA_RsBQK4_LODZtykxVZRFHlhaQul7UO9Jp6Cs1nGg_TUlmGafXl1dq4zxA1rXHtNuZOBQjY_0LHnTN0Eq_C7wzvP6-g2hzh3jso11qBjiyjPdNKSFNYEIzMNU8nfRYWp-w7w_Ezg7p29WeRj6CyGLitpBuL7ekDbwLa9pDJCfzzCEbMu_fJUWGNFUsjNcGsWN3aUPMgU76phQDU3d9Ksi48uL-EB1lckRF9pVDSuqOn-WB01KtSj-ZLOT0fOXNM7ZlUzchQyavcRylXlAz1M2yVj46hZbydqygvSO5v23vFjUjSasgfNfxjPAUHvGce7eN2uc620Cp5xvdQxXw7K8pK_lU-0SP-2TXoS5_2WWKLx-8drOvPJt06UACiM4KaRhvnLNNzZI2Vo0mLgLx5McIqFjOEQECptApYgru6eoU0TAU3yQIJbKYuLZ1UmnQPtCc5D3e6HhtbCjHzHf4lsZ3j08GaToZV9Db3XSZ3r5Vj1X95bF9xrMp3tlsli-fu_I4mfLTNYJUZGSUCh8eY66tZxCBrGRBP9mGwJ7Esr6M5waowMpgpEINw22i_3sOM4UHf0-v8bMTv0uYjqndxUoI3Wut8Av4d3Txxbbx6asaREJhwWRJCOTgnRk2RsYfkiwwG2bdGuZxgeB22MSX_GPcSlLeS1TPJbZoYaRzu3hwczl4XQFya2cPac5wdG5vyfoJVrB39clL5ON-zIQRkMmQsbvGQkJrJ-f05EQc3Jf9he4zM__xXQ=","summary":[{"type":"summary_text","text":"**Analyzing rc stream ordering and completion**"},{"type":"summary_text","text":"**Evaluating stale and active stream race conditions**"},{"type":"summary_text","text":"**Testing stream handling with dispatcher stop**"}]},{"id":"rs_047bb49263dfa303016a58abce30748198b59f4cf6f6e9c135","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKvWArGe76rM0gyFcEC7XHQ_WuMKIDbEQSba4dyNInnWSznlCsgYPawMv37XeH0laSEcD8QiaC3qoCQS8l7ruSDV0tF6KMsX8a-ZRHa_NR2AByLrJTxO09raLww5UgOEygeu-aURJclAmHklLHGW2OllvvYseiBqhmwnBjBuoUybLjq1qAN4eiON3E0bGgdCvsFOW_jCTfVTRYMR4_VZbmoKiXeE_jNnQ7fB12-vTQbwnsuS4rndope1vj3bJTP5Le9_cR9uE82u2qaypck8cU0tdL8zuVPOTJrj6Z0fXFM7DCw-0R22bv48pZwY8XJl9knkY3mUzte-dlBaqFgJxMqm4k6CVpISxz3SJ98ihS74o-p3T0Kx6FknfCtCGSJP48FBBvnZS6FZwDhIGc3rE-32sev-d9upesLM2X9qyB9RkQmI7KmfHyn_liHo0kUNw-5llNLEYdH3E6Tn1TqPtqS855z4FbXUccfEVKbjJ_wLNHvw8lMek8F8MTddiuj71P5bbXsgVpFTV50eSJdV6n2UaCqkv6rbmVoX0QryzcYTTFP78sl2kbtOSNR1fkI7d47YJ0hTiZKt4pUK07qpTh3CElEb0fEHwzNjrIoFAOAL6BNrNQfxTZnAM3X7Cl2wSqwGv-izjJqASXHaCvbaniStjelT8O7e674hXOm_sje_nEef3MjUi_TaCdzxgKRSLL-Ni6QbC8HOIiuu4GKv3K_2b4sQZ7BRnPNWPBE7e2p-uAquIOZlhlfBaBqPbQrpao7rSqXHkUshGAM3WSvdVcWSCOPgYr2c4lIVeLC7sSEaq5XmIMxxtV8mxgk4Mg2yPxKGzTmKF0_Ygg0KedezMveZdPQulKjBKfpuASnMky1_5EH3rAbnTWRbmbXssuFNFz6PLfH2NPeixpW7sOWOp7qhyC-lifm77tck6B2J2K685088aNEaMnO_T8J7ncF5r6LYejgSYth66PW5NdEQ2Cu8lF83jPQhXCAXo10tpJoKIUBmdhgED2d0kWMd0qL7Zs6LspcjQWpgonYLh6kykCReYzbPEYeddnxLCqsaZSYV9NYRX_wOfHtmGAOQJUKk0eNqZRDnCBfDwv-pKPb4DwCSY23sr5HeMEB4xwCFp4wJCUzAA0ZEWvIj6lMBBTzc7stteYUGmxqbuhplyM-gBle1vbyOIcqnRmeApzriIUGVnfAjn3hUHCY__X-TwzLBLvTkR1s0dl6PbnmM-FtmKtvj8oEU9TkwvX8O22JLAdzig9x_FY7xWSFquvXd59VQsRVeK1EL2q0DAeAPLkzJBp5xn4pG0twPurGPscvu3UwJkpovemGWbzEYS9mrE0aiZfvY7Xw6Th1SiNYzBIIp67mA23GQwPQ4tK4lV8dG_p38T5Albjxu8v-kJ0kk8Y5raILwWBg2BXlFUZwy3w3wn_Q655qeuWlBtbX6vj7NsPWtRiybP0FWq6zbaouW2OoCqJrvWnvqyoCBKUvaiMTGy3Ba3KZEHwVqzc2jJQRO4YK7BEh2PcEXbtN0xGLBqd8dYKS0kUaHtL3KI4Gk9VSRotw_KxDhvZl_IEoGJQPjBtSmUXfExHdS-yIR7JleBtXv3c6OKq2r6botInaV0ZkRjvJGTJhh6j2BdMH61JsSiOGjx-iWwh1JighIBWqoVLSldH3IR8XFtWNBmzmDE453CHs7mXTZ4kDlrPPM4Yr-4cyc-xxsIi9HB4yNpsOKJj2j9B_IGEVMU4R_aU75PP3_W4Z5fLDwIDMGyvImbl207bA5q9Ojc6UlRwIYOmSablDr8gx7CrOduLRH5j4Ull5X-qDGDDNr9abDmQrvtgMrgNtdnFV66z6nVcX2D2RwHGCH__cCwFY6XnSbeYe6xJ3PMkSKlulloXRr6-pX98seF542T5I2U0HPED3RfD83wqXQtk4d0x-4qY08ulO7d197V_9tW4lUV1mxIDu3jwXyJuluuG80Mwn4ST0TVqGNSoWJuHfA9rjN-7qn4Pb-LTm9ePPZUzTDPN_GFolrgVK4lMz3kN-a63PlAYCYeYVI7vJf6QhlSiDUKnWccPSlsPzT19O8FQH-SfbRMmlIM4vs-0fVMpSkzCsFkzWcx_0FwhcBEwy32sQiXTjwLvs3ubVo5teGAgBJC-gqUrB2MDNNFK8G_v1uM9OpkPlDdYLmR1eGSHsGlU6_1A67bhxlELYIWAHZrfcJdvpRbFw8vYbHz9p89YGXDlhunMzpk-qM9MqRJdXTjCtV-hr5lEdg1BFJBikh5nyfcGwVfGl-jmQGLoDW9_cd4GaBKBdMdtMX3JWY3EaLW8jmRQmtc0fC-gnOOB8-ja8GAjP4s1tfo6wfWxnZogWjN4k3dzRGfBjOhPLv6BfZqqYnLv7kpIddYJhvg8HWALXU_lGfYIuzrQZQf9CJvzQgKuqtyPOsjJabZdhzxxBj1SNJm80qnuh7vjjcot3ny24nlQ9b4NKzPx1JSFvCJGk0QA6pok9qGCD9H6CWGsIILBXl7bt-q1OKYWOVZ2XKgt29llUCeYRPpPa1WzzQ7_5jhG4Ywc-Rzil1G9kPdy1TppoJhbV_vAizgO1VI-MArVdOhmsspTVW9iFEkx3Sv0xoVK1X9ZDa0wZamxpB7UQgLdrH9_oGfVw19gLr6Oa8vvlEuYTdIf0Am0677s8SCXYJrK6D9kpLDKpT-pI1Kc8afu8uU9VWfgc2nNe1ZP1T8Xe-qxjs7vGYNm9Y6DKTrYAd6Y1sG-GKz6m8cre3-X2UxGqahLEf074EPYAJvSvVjqOD3yHLk1w9zY338Esn1j1i39vy_8lpCAL8pJeWXUQ5","summary":[{"type":"summary_text","text":"**Designing isolated refresh harness**"},{"type":"summary_text","text":"**Planning deterministic broker eviction test**"}]},{"id":"fc_047bb49263dfa303016a58abd63d908198be797cc826c7b6c7","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding net ring cap seam\",\"pattern\":\"ring_cap|with.*cap|start.*ring|DEFAULT_STREAM_RING_CHUNKS\",\"path\":\"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_no4CVE2T9uUpiek4dGs5ivKA","name":"grep"}]},"duration":28139.691500000656,"ttft":2127.299000000581,"contextSnapshot":{"promptTokens":162842,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_no4CVE2T9uUpiek4dGs5ivKA|fc_047bb49263dfa303016a58abd63d908198be797cc826c7b6c7","toolName":"grep","startedAt":"2026-07-16T10:00:55.655Z","args":{"path":"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/tests"},"intent":"Finding net ring cap seam"},"id":"b8df1a47","parentId":"d92414b4","timestamp":"2026-07-16T10:00:55.655Z"}
{"type":"message","id":"49280a37","parentId":"b8df1a47","timestamp":"2026-07-16T10:00:55.673Z","message":{"role":"toolResult","toolCallId":"call_no4CVE2T9uUpiek4dGs5ivKA|fc_047bb49263dfa303016a58abd63d908198be797cc826c7b6c7","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n\n## src/\n### nethost.rs\n 110|/// buffer without limit.\n*111|const DEFAULT_STREAM_RING_CHUNKS: usize = 4096;\n 112|\n 113|/// Max bytes pulled per stream read (one ring chunk).\n 114|const STREAM_READ_CHUNK: usize = 64 * 1024;\n...\n 200|\n*201|    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\n 202|    fn set_retentive(&mut self, cap: usize) {\n 203|        self.retentive_cap = cap;\n 204|    }\n...\n 483|    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,\n*484|    ring_cap: usize,\n 485|    /// The conn-liveness event log (D4c) — one per host, all conns interleaved.\n 486|    presence: Mutex<PresenceLog>,\n 487|}\n...\n 495|            streams: Mutex::new(HashMap::new()),\n*496|            ring_cap: DEFAULT_STREAM_RING_CHUNKS,\n*497|            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),\n 498|        }\n 499|    }\n 500|}\n...\n 646|    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);\n*647|    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));\n 648|    let room = Arc::new(tokio::sync::Notify::new());\n 649|    shared.streams.lock().unwrap().insert(\n 650|        id,\n...\n 1426|\n*1427|    /// Mark a held stream **retentive** with backpressure `cap` chunks (M11-W3,\n 1428|    /// REQ-SHELL-4): its read log stops evicting (no byte loss) and its read pump\n 1429|    /// lags the sender once the undrained ring reaches `cap` (no unbounded growth).\n 1430|    /// Used for shell-tunnel streams; ordinary streams stay bounded-overwrite.\n\n## tests/\n### netbroker.rs\n 16|//! - a net-less broker answers `net-status` with `enabled: false` and a dial\n*17|//!   with a typed error (capability probing, not a wedge).\n 18|\n 19|use std::sync::atomic::{AtomicU32, Ordering};\n 20|use std::sync::Arc;\n...\n 317|\n*318|// [int->REQ-DAEMON-2] a broker bound without a network host answers capability\n 319|// probes gracefully: `net-status` says disabled, a dial is a typed error.\n 320|#[test]\n 321|fn netless_broker_probes_gracefully() {\n### psyche_context_file_e2e.rs\n 23|//! assertions go RED. (On win32 the faithful argv path additionally BRICKS the spawn\n*24|//! at 40KB; on unix, with no cap, this empty-file revert is the structural RED.)\n 25|//!\n 26|//! The `#[test]` owns the process-global `SPT_HOME` (a fresh tempdir) — run under\n 27|//! nextest (process-per-test). No spt.exe is spawned; the only child is the mock role.\n### resume.rs\n 46|fn served(name: &str, dir: &std::path::Path, cap: usize) -> Arc<Broker> {\n*47|    let broker = Broker::bind_in_with_net_cap(name, dir.join(\"effects.log\"), None, cap)\n 48|        .expect(\"bind broker\");\n 49|    let serve = Arc::clone(&broker);\n 50|    thread::spawn(move || {\n...\n 226|// [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION] the post-eviction ring-floor clamp\n*227|// (doyle D4-2 watch-item): with a tiny output-log ring cap, the dead-window\n 228|// output overruns the ring, evicting the chunks at the resume cursor. A\n 229|// re-attaching brain whose `resume_seq` is now BELOW the surviving ring floor\n 230|// must resume forward from the floor — the resume-mode dedup-below + snap-above"}],"details":{"scopePath":"crates/spt-daemon/src/nethost.rs, crates/spt-daemon/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":12,"fileCount":4,"files":["crates/spt-daemon/src/nethost.rs","crates/spt-daemon/tests/netbroker.rs","crates/spt-daemon/tests/psyche_context_file_e2e.rs","crates/spt-daemon/tests/resume.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/nethost.rs","count":7},{"path":"crates/spt-daemon/tests/netbroker.rs","count":2},{"path":"crates/spt-daemon/tests/psyche_context_file_e2e.rs","count":1},{"path":"crates/spt-daemon/tests/resume.rs","count":2}],"truncated":false,"displayContent":"# crates/spt-daemon/\n\n## src/\n### nethost.rs\n  110│/// buffer without limit.\n *111│const DEFAULT_STREAM_RING_CHUNKS: usize = 4096;\n  112│\n  113│/// Max bytes pulled per stream read (one ring chunk).\n  114│const STREAM_READ_CHUNK: usize = 64 * 1024;\n     │...\n  200│\n *201│    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\n  202│    fn set_retentive(&mut self, cap: usize) {\n  203│        self.retentive_cap = cap;\n  204│    }\n     │...\n  483│    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,\n *484│    ring_cap: usize,\n  485│    /// The conn-liveness event log (D4c) — one per host, all conns interleaved.\n  486│    presence: Mutex<PresenceLog>,\n  487│}\n     │...\n  495│            streams: Mutex::new(HashMap::new()),\n *496│            ring_cap: DEFAULT_STREAM_RING_CHUNKS,\n *497│            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),\n  498│        }\n  499│    }\n  500│}\n     │...\n  646│    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);\n *647│    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));\n  648│    let room = Arc::new(tokio::sync::Notify::new());\n  649│    shared.streams.lock().unwrap().insert(\n  650│        id,\n     │...\n 1426│\n*1427│    /// Mark a held stream **retentive** with backpressure `cap` chunks (M11-W3,\n 1428│    /// REQ-SHELL-4): its read log stops evicting (no byte loss) and its read pump\n 1429│    /// lags the sender once the undrained ring reaches `cap` (no unbounded growth).\n 1430│    /// Used for shell-tunnel streams; ordinary streams stay bounded-overwrite.\n\n## tests/\n### netbroker.rs\n  16│//! - a net-less broker answers `net-status` with `enabled: false` and a dial\n *17│//!   with a typed error (capability probing, not a wedge).\n  18│\n  19│use std::sync::atomic::{AtomicU32, Ordering};\n  20│use std::sync::Arc;\n    │...\n 317│\n*318│// [int->REQ-DAEMON-2] a broker bound without a network host answers capability\n 319│// probes gracefully: `net-status` says disabled, a dial is a typed error.\n 320│#[test]\n 321│fn netless_broker_probes_gracefully() {\n### psyche_context_file_e2e.rs\n 23│//! assertions go RED. (On win32 the faithful argv path additionally BRICKS the spawn\n*24│//! at 40KB; on unix, with no cap, this empty-file revert is the structural RED.)\n 25│//!\n 26│//! The `#[test]` owns the process-global `SPT_HOME` (a fresh tempdir) — run under\n 27│//! nextest (process-per-test). No spt.exe is spawned; the only child is the mock role.\n### resume.rs\n  46│fn served(name: &str, dir: &std::path::Path, cap: usize) -> Arc<Broker> {\n *47│    let broker = Broker::bind_in_with_net_cap(name, dir.join(\"effects.log\"), None, cap)\n  48│        .expect(\"bind broker\");\n  49│    let serve = Arc::clone(&broker);\n  50│    thread::spawn(move || {\n    │...\n 226│// [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION] the post-eviction ring-floor clamp\n*227│// (doyle D4-2 watch-item): with a tiny output-log ring cap, the dead-window\n 228│// output overruns the ring, evicting the chunks at the resume cursor. A\n 229│// re-attaching brain whose `resume_seq` is now BELOW the surviving ring floor\n 230│// must resume forward from the floor — the resume-mode dedup-below + snap-above"},"isError":false,"timestamp":1784196055673}}
{"type":"message","id":"86260ed3","parentId":"49280a37","timestamp":"2026-07-16T10:01:06.547Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting stream info and hazards**"},{"type":"toolCall","id":"call_2DtgQm7I6WnD3FCci0o5TIV5|fc_047bb49263dfa303016a58abe2444481989d0969e570a6a5d9","name":"read","arguments":{"i":"Reading PTY attach hazards","path":"docs/KNOWN-HAZARDS.md","selector":"330-520"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4439,"output":125,"cacheRead":160256,"cacheWrite":0,"totalTokens":164820,"reasoningTokens":85,"cost":{"input":0.022195000000000003,"output":0.0037500000000000003,"cacheRead":0.08012799999999999,"cacheWrite":0,"total":0.106073}},"stopReason":"toolUse","timestamp":1784196055686,"responseId":"resp_047bb49263dfa303016a58abd8287081989d51c5c4a78fa6ff","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58abdad9e481988f3a7f9294c40679","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKvif2KLGojswVmglCBfd3uNKBMKsA8wEGEBKZEkX0LXonvVW0Z58Akw6uso2ABimDXo2pdLF-750gqAAiT10r3640_irYDwRJ9UyOZjy5TOzqgpZ2MuupTv7k6L3dJ9_AFE743_6Wkf_VGf3nib3qY2DhFsKqh0o87X8N556jdYYD-vB1s9271Wqp-U5MfKZgDh5BsBdhCLWlf5CGiZH_VB6TsLUKUepElC_dtcxcBZH1Ycsznx6lFDG6Ah0WaCa1AQnMB31voUg7hBv9nF0B4D4kdfuck0JvSE6UzKhh1COyhnng5h9I2mlTcEEWgTDi7VbjBQpikcTQasyC_-9Md8owJHjOrqpzBvR3GXDckUcxrdc0KiYu6Niv4PHm90cZd4ZJ-ghf3p8AwtS4dygpo4t6laWqI9aGNqoder4lYNll97m2wY3Z9mV0cVEqFFMOtH-Nd8smiaszjRdhOwRYYUzxk21x7-Qjy-IxroYWCDWm00Hd_5p3Te0AWETzl3q5WzMzJr650Llzyqd_rY1zKncqr7myImmk_QPifIy0ToBvi4dPQwzI0gFxHvpyA0nDGq9WldV-MOcN2vKWECLCwjzXfT-rIU9BaGu3htFccaqzvhQMIlO12X5hRzBaN7next0fLB5KeYr1Xa8bVNDu2tSkLSrg39Zv0PbtQc-FqQJSwTJ1ZsI__PBp7-qowRrRcteDrsbcUI95mglQ5ho8LqrnlwNMPLa3el8WZ4BJ-FvesNgZ55Aet942cdcD1SCm5pyalLCVefky3ScrccdxTaB4bcbw6_oN2bVEUsBOHFufL_6p9Zxd1NpYLH_yLVkm5nW0uB-HOv9FRecrkL-M5rJWn2JaeX-jPNOsqPU6AY-X1WwRiOUboKQw_WmJE1GMB5uXbCS84ymjESgNlWSl4KBc6XWyowrQmHv6pobdC0ZCVcuvymywgs-m2cPhOXz15257cppmgclBngXRzcgSyrTOnVx3ltJ8Gp8VoSGzsh4ko9AIO13NHLfgm3azdzXvZV84HeqlaW-3D6vDhJ00bUlim6UO7HssaHiWCDGA1Cug1iUA6TMMGduBm8DC8MOgfRBw8IM_2-1yNUx_Paill3UMJrFAyQtWKkiHi-9mRjezR_V4lfKazc7iSFxNWL3YAi_EICujehqNDDhhTHeIXNztdREaxZ033CfkMA8E6V4VH5H8YVX-Be9UWE8o3mdWs3es4xBRJKcQAv9v5KMndRDoXL1COj-I5touq54CHQD149FzEpPhkLh_dZ0WmcwudBSrk_b2xQxKuFvMHO580jUNHrK40b0rryUwHSjODBa78SiNGwHA_5SvihENoWWNXRIMg94vpyDXYU8GvQFGgVXfvtFvzY4n2dEz-Trq8vmdI=","summary":[{"type":"summary_text","text":"**Inspecting stream info and hazards**"}]},{"id":"fc_047bb49263dfa303016a58abe2444481989d0969e570a6a5d9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading PTY attach hazards\",\"path\":\"docs/KNOWN-HAZARDS.md\",\"selector\":\"330-520\"}","call_id":"call_2DtgQm7I6WnD3FCci0o5TIV5","name":"read"}]},"duration":10859.859199999832,"ttft":3140.871299999766,"contextSnapshot":{"promptTokens":164695,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_2DtgQm7I6WnD3FCci0o5TIV5|fc_047bb49263dfa303016a58abe2444481989d0969e570a6a5d9","toolName":"read","startedAt":"2026-07-16T10:01:06.548Z","args":{"path":"docs/KNOWN-HAZARDS.md"},"intent":"Reading PTY attach hazards"},"id":"06c93afd","parentId":"86260ed3","timestamp":"2026-07-16T10:01:06.548Z"}
{"type":"message","id":"6b0c5499","parentId":"06c93afd","timestamp":"2026-07-16T10:01:06.598Z","message":{"role":"toolResult","toolCallId":"call_2DtgQm7I6WnD3FCci0o5TIV5|fc_047bb49263dfa303016a58abe2444481989d0969e570a6a5d9","toolName":"read","content":[{"type":"text","text":"## 6. Documented regressions (non-obvious invariants)\n…\n### 6.7 Broker and brain MUST be separate processes (in-process collapse silently breaks no-endpoint-drop update) `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n- **Failure:** the daemon hosts the broker as a background *thread* in the single `spt daemon` process (`daemon.rs:165-170`, `Arc<Broker>` + `thread::spawn(serve)`) instead of a separate process. A brain restart onto a swapped binary then cannot happen without killing the broker thread — closing every PTY, orphaning every harness child, dropping every socket. So `spt update apply` degrades to an in-process `Brain::handoff` no-op: the binary swaps on disk but the running daemon keeps executing the old code until an unrelated restart/logon. The no-endpoint-drop self-update pillar (REQ-UPD-3, ADR-0004) is silently unrealized. Observed live 2026-06-09: `enlyzeam` ran 0.3.0 with 0.3.2 on disk for ~a day, still reproducing the bug the update fixed.\n…\n- **Origin:** unintended spec/impl drift from ADR-0004 (the broker *process* was specced + spiked but built in-process), discovered during the v0.3.2 fleet update verify. Full audit + decisions: `docs/BROKER-BRAIN-SPLIT-RESTORATION.md` (verified) + ADR-0018.\n<!-- [doc->REQ-HAZARD-BROKER-PROCESS-ISOLATION] -->\n- **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- **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 `apply` rename to `.old-N` and ran OLD bytes under an `applied` record (`[REQ-HAZARD-BRAIN-RESPAWN-PATH]`, 6.11), fixed in v0.4.2 (respawn from the canonical path captured at broker start + a promotion bytes-gate). Seamless update is now proven on **both OSes** — Win…\n\n### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`\n- **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- **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- **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- **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- **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 names present; an unknown extra field still deserializes), so a *non-additive* pre-ready change (renamed/removed field, or a `deny_unknown_fields`/non-tolerant shape) trips the test and forces the migration **behind ready-promotion** (or into an additive form). *…\n- **Origin:** verification amendment `[V1]` (agent `doyle`) on `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.\n- **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't-pre-fail); the tripwire is the wire it trips the day one is needed.\n<!-- [doc->REQ-HAZARD-ROLLBACK-STATE-COMPAT] -->\n\n### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output\n- **Failure:** a resume-mode brain (per-session `session_cursors` populated by `resume_sessions`) drives `Brain` over **blocking** `read_event` calls. Any command that loops `read_event` until its own reply — `spawn_session_pid` waiting for `Spawned`, `net_status`, `sessions`, etc. — calls `read_event` on *every* interleaved frame, so an OUTPUT frame for a **different** session is **cursor-processed** (its `session_cursors` entry snaps forward; the broker already counted it delivered via `delivered_through` on the live-send) and then **discarded** by the waiting loop's `_ => continue`. Cursor advanced + content dropped = that chunk is gone for the downstream consumer and the broker will **not** re-send it (resume reads from the delivered cursor, ADR-0018 D4)…\n- **Invariant:** the daemon-hosted multi-session event loop must not consume a session's OUTPUT inside another session's blocking wait. The **live-agent adapter milestone must restructure the brain event pump** so command/`spawn` is non-blocking (a single demux loop owns `read_event` and routes every frame to its session's consumer), OR a blocking wait must re-queue/route the frames it reads for other sessions rather than dropping them.\n- **spt-core mapping:** **unreachable today** — the supervised daemon brain hosts no PTY sessions and spawns none; a single-session seat (legacy, empty map) has no \"other session\" to starve. Surfaces the moment daemon-hosted sessions land (the live-agent adapter), which must rebuild the blocking `read_event` loop regardless (N interactive sessions cannot share one blocking reader). Recorded so that redesign inherits the constraint rather than rediscovering it. No machinery now (would be untested dead code, activate-don't-pre-fail).\n- **Origin:** surfaced by the D4-2b resume-harness CI flake root-cause (agents `todlando` + `doyle`, 2026-06-10); sibling of `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]` 6.7.\n\n### 6.10 Phase-significant loop timing must be a durable absolute-deadline grid, not phase-relative sleep `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n- **Failure:** a periodic loop that sleeps a flat `period` each iteration (`pulse_tick` then `sleep(pulse_period)`) is **phase-relative** — every brain restart silently re-phases the grid to the restart instant. Under the seamless-update model (the supervisor respawns the brain onto a swapped binary, ADR-0018 D3-3), a routine update would shift the cadence of every phase-significant loop, and continuity cannot ride a brain→brain frame (the outgoing brain is gone before the new one starts — the same constraint that moved session continuity to the broker in D4).\n- **Invariant:** phase-significant periodic timing lives as durable absolute-deadline state on disk (`(anchor, interval)`), rehydrated on every brain start, with fires **derived functionally** (`next_fire = anchor + interval·⌈max(0,now−anchor)/interval⌉`) and **no per-fire write**. An **Update** restart re-reads the anchor and keeps deriving (phase preserved, lands mid-grid); a **Crash**/**Cold** restart re-bases the anchor to `now` (phase reset acceptable — the loop is idempotent catch-up). The update-vs-crash decision is the D3 spawn-time `StartReason`. **One-shot** (alarm) deadlines persist their absolute `target-time` at creation and **never reset** on any restart (\"remind me at 3pm\" is a commitment) — the asymmetry vs the periodic crash-reset is the rul…\n- **spt-core mapping:** ADR-0018 Q4/V3/V4, restoration D5. Mechanism in `spt-daemon::deadline` (`DeadlineAnchor` periodic + `OneShotDeadline` rule-only pure helper); the pulse loop (`lifecycle::run_pulse_loop`) consumes it. The one-shot **machinery** (a durable in-daemon alarm scheduler) is the deferred alarm port (`docs/DEFERRED.md`) — the daemon has no one-shot consumer today, so building the timer now would ship untested dead code (activate-don't-pre-fail); D5 fixes the *rule* as a tested-unwired helper, the port builds the *scheduler*.\n- **Origin:** ADR-0018 Q4 + verification amendments `[V3]`/`[V4]` (agent `doyle`); D5 plan vet (agents `todlando` + `doyle`, 2026-06-10).\n\n### 6.11 Brain respawn must exec the APPLIED bytes, not the renamed old binary (Linux `current_exe` follows the apply-rename; readiness ≠ new-bytes) `[REQ-HAZARD-BRAIN-RESPAWN-PATH]`\n- **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**, and the daemon records `applied:N` while still running the previous version. New code does not run; the record is optimistically wrong (the enlyzeam-class record/reality divergence, now provable via `exe_hash`). **Windows** dodged it — `GetModuleFileName` returns…\n- **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 the new bytes run). If either hash is absent the gate degrades to readiness-only (N-1-safe for pre-metadata releases / a missing breadcrumb) but emits `PROMOTE_BYTES_UNVERIFIED` so a disarmed gate stays field-diagnosable.\n- **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- **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<!-- [doc->REQ-HAZARD-BRAIN-RESPAWN-PATH] -->\n\n---\n\n## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)\n\nThese were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.\n\n### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`\n- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).\n- **Invariant:** every `api` *mutation* is authenticated to an endpoint/session — per-endpoint link token or OS-credential binding. An unrelated local process cannot bind, inject, end, or spoof state.\n- **spt-core mapping:** the `api` subcommand surface (PRD R-API-*) + broker IPC.\n- **Source:** codex Stage A #13 (`docs/reviews/STAGE-A-codex-redteam.md`).\n\n### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`\n- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.\n- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.\n- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.\n- **Source:** codex Stage A #14.\n\n### 7.3 Psyche outbound capture + sanitization  `[REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]`\n- **Failure:** the Psyche's sole outbound channel is its **stdout** (`<EVENT type=\"reply|notify\">` intents — ADR-0012). Two ways to break it: (a) a **null-stdout / detached** live-Psyche driver silently **discards every reply and notify**; (b) the daemon relays a Psyche-supplied `from=`/target **unchanged**, letting a sandboxed Psyche **spoof identity** or address arbitrary endpoints.\n- **Invariant:** the live-Psyche turn driver **MUST capture stdout** (a bounded, stdin-fed, stdout-captured invocation — **never** `Stdio::null()`); the daemon **MUST strip** every Psyche-supplied `from=`/target/routing attribute, **re-stamp `from=<self_id>`**, and **constrain routing** — `reply` → the inbound message's structural sender (its `from`) only, `notify` → the agent's own user/subnet only. Body validated per 4.1.\n- **spt-core mapping:** the live-Psyche turn driver + daemon outbound relay (ADR-0012); `spt-proto::event` type taxonomy (`+reply`/`+notify`). The interim `runtime::spawn_session` `Stdio::null()` path is **not** the live-Psyche driver.\n- **Source:** grill-with-docs 2026-06-03 (uncovered by prior design passes); sister `src/live/wrapper/claude.rs` (sandbox `[\"Read\",\"Write\",\"Edit\"]` + `parse_markers`).\n\n### 7.4 Per-agent pulse/psyche/echo scheduling must not serialize across agents  `[REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]`\n- **Failure:** echo-commune (`run_bounded_stdin`) and the live-Psyche turn driver (D7.5) are **bounded LLM calls that block their calling thread** until the child answers or the timeout fires. Today each agent drives its own pulse from its **own process** (`spt/src/api/{live,startup}.rs`), so blocking is isolated. When the daemon hosts **N per-agent loops** (ADR-0004 target; the `run_pulse_loop` fan-out is currently def+test only), a **single serial driver** that calls these invocations inline lets one agent's slow/hung LLM call **stall every other agent's heartbeat, commune, and reply** — the per-call timeout bounds the worst case *only* when the work is isolated; serial makes the stalls additive.\n- **Invariant:** each agent's bounded LLM-bearing work (echo-commune summarizer, Psyche turn) runs on its **own thread / off the shared scheduler** — no single-threaded driver iterating all agents may call a blocking invocation inline. One agent's slow/timed-out call must not delay another agent's next tick beyond tolerance.\n- **spt-core mapping:** the daemon's multi-agent pulse/psyche hosting (ADR-0004, \"all Psyche/pulse loops\" consolidated); `run_pulse_loop` fan-out; echo-commune + the D7.5 Psyche driver.\n- **Source:** grill-with-docs 2026-06-03 (forward invariant — the multi-agent fan-out is not yet wired). Distinct from ADR-0002 SERIOUS #6 (crash blast-radius, not scheduling latency).\n\n### 7.5 WAN-inbound origin is transport truth, never payload  `[REQ-HAZARD-WAN-ORIGIN-AUTH]`\n<!-- [doc->REQ-HAZARD-WAN-ORIGIN-AUTH] -->\n- **Failure:** the ADR-0009 access whitelist gates **unsolicited wire inbound by origin node**. If the gate's subject is read from record bytes (an `origin_node`/`from`/`node` field a sender wrote), any sender forges any origin and the whitelist is decoration — same spoof class as 7.3's Psyche-supplied `from=`, now on the cross-node surface.\n- **Invariant:** the origin the gate (and detection/UX — \"node X is driving\") consumes is the **QUIC handshake-proven remote node id** (iroh `EndpointId` == Ed25519 node pubkey, the REQ-NET-1 identity binding) read from the **broker's conn/stream table** (`NetStreamInfo::remote_id_hex`), never from payload. Wire records carry **no origin field by design**; a forged one decodes as an ignored unknown field and influences nothing. `from` inside a record is reply-routing metadata only — never an authorization subject.\n- **spt-core mapping:** `spt_net::net::wanmsg::WanMessage` (no origin field) + `spt_daemon::wan::receive_wan` (origin parameter sourced from the stream table) + every future wire-inbound consumer (D5b attach, D5c transfer, D8 notifs).\n- **Source:** M4-D5a design (ADR-0009 consequence: the gate must bind to the transport identity).\n- **Mesh note (ADR-0017, 2026-06-08):** preserved verbatim. The mesh never relays third-party rows, so a record's author is **always** the QUIC-handshake-proven connection origin. The new **membership proof** (seed-proof) is itself channel-bound to both handshake-proven pubkeys — the authorization subject stays transport truth, never payload.\n\n### 7.6 Pump brain-IPC reads must be deadline-bounded (a blocked read wedges the whole pump)  `[REQ-HAZARD-PUMP-IPC-DEADLINE]`\n<!-- [doc->REQ-HAZARD-PUMP-IPC-DEADLINE] -->\n- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop-top) stops and the stall warning fires honestly, but `supervise_pump` cannot rescue it: the supervisor catches a panic / error / clean return, and a BLOCKED thread never returns. Observed twice — 2026-06-07, then a **2.2h wedge on hfenduleam (2026-06-11)** w…\n- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a healthy long sync is never killed — only a ≥30s silence). **Mechanism note (Windows):** the carrier is a reader-thread + channel, NOT a non-blocking socket + poll — interprocess 2.4.2 on Windows named pipes has no portable read timeout (`set_recv_timeout` → `no_tim…\n- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the `net_dial`/`open_stream`/`send_stream` handlers) — shipped in v0.8.3 as §7.8 (`REQ-HAZARD-BROKER-QUIC-DEADLINE`).\n- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.\n\n### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`\n<!-- [doc->REQ-HAZARD-VIEWER-ISOLATION] -->\n- **Failure:** the W2.5 controller/viewer model lets ANY number of read-only `--view` attachers ride one session's broker `OutputLog`. The single drain thread fans each output chunk to every attacher. If a viewer's socket is fanned out with a **blocking** write under the log lock (the controller's authoritative path), one wedged viewer (a slow terminal, a black-holed WAN peer, a hostile non-reader) stalls the drain — freezing the controller's stream and backing up the PTY child. A single watcher must never be able to degrade or hang the driver.\n- **Invariant:** the drain writes the **controller** on the authoritative blocking bounded path (it alone advances `delivered_through`), but each **viewer** gets an **isolated bounded SPSC queue + a dedicated writer thread**; the drain `try_send`s under the log lock and **evicts** any viewer whose queue is `Full` (fell behind the live stream) or `Disconnected` (its writer died on a dead socket) — the drain thread **never touches a viewer socket**, so no viewer write can backpressure it. A **soft cap** (`MAX_VIEWERS`) bounds the writer-thread count (a viewer attach beyond it is refused). Viewer eviction never perturbs the controller stream, the `delivered_through` cursor, or the child. Ring **replay-at-attach** is owned by the writer thread (not the bounded l…\n- **spt-core mapping:** `OutputLog::append` (controller blocking + `viewer_send_evicts` `try_send` fan-out), `OutputLog::add_viewer` (bounded `sync_channel(VIEWER_CHANNEL_DEPTH)` + `viewer_writer` thread), `MAX_VIEWERS` soft cap, `ViewerSink`. Unit: `viewer_overflow_or_disconnect_evicts_never_blocks`. Int: `wedged_viewer_does_not_stall_controller` (a non-reading viewer is evicted while the controller keeps receiving past a 200KB burst).\n- **Source:** M12 W2.5 controller/viewer model (doyle ruling 2026-06-14, Q1).\n\n### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`\n<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\n- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only via its OWN 30s read-deadline (the 7.6 A-half) — a 30s stall + a full pump restart EACH round, and the supervised restart re-dials the SAME dead peer and re-wedges. Root cause of the 2.2h hfenduleam wedge (2026-06-11) and its recurrence (2026-06-16).\n- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`, so the BROKER fires FIRST — the brain reconstructs `ErrorKind::Other` (its `net_dial`/`net_open_stream`/`net_stream_send` map a broker error reply to `io::Error::other`), NOT its own read-deadline `TimedOut`, so `pump::peer_outcome` takes the ordinary per-pee…\n- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIES an ordinary error within the bound + exactly-once-on-timeout: journal un-applied, conn table empty, clean redial) and `pump_survives_a_black_holing_peer_heartbeat_advances_no_restart` (the production pump against a dead peer — heartbeat monotonic-advances, `run…\n- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.\n\n### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`\n<!-- [doc->REQ-HAZARD-BROKER-SEED-WIRE-SKEW] -->\n- **Failure:** the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004's no-terminate-during-update pillar forbids auto-killing it — 6.7). A self-update that changes a daemon-state WIRE FORMAT — e.g. the v0.9.0 adapter-agnostic `Seed` (the `adapter` field dropped) — therefore lands a NEW-version CLI talking to the STILL-RESIDENT OLD broker. The old broker cannot deserialize the new `Seed` (its formerly-required `adapter` is absent), so it drops the seed-control conn without acking; the CLI's `put_seed` ack-read hits EOF and surfaces a raw `UnexpectedEof` \"failed to fill whole buffer\" — a cryptic footgun that hides the real cause (perri PREP-4 FINDING 1: v0.9.0 CLI ↔ stale 0.8.x broker).\n- **Invariant:** (a) spt-core surfaces an ACTIONABLE diagnostic on the seed-ack EOF — naming the stale-broker cause + the fix (`spt daemon stop`; the broker restarts on the next `spt api` call) — never the bare io error (scoped to `UnexpectedEof` on the seed-ack path, so it never mis-fires on an unrelated error like a refused connect). (b) A daemon-state wire change requires a DELIBERATE full broker restart; this is NOT automatic — ADR-0004 forbids auto-killing the resident broker, and a brain-only update keeps the old broker. (c) FORWARD discipline: daemon-state / `Seed` schema changes stay ADDITIVE + serde-default, so a resident OLD broker tolerates a NEW CLI across a brain-only update. (This would NOT have rescued 0.9.0 itself — the old broker's `adapter`…\n- **spt-core mapping:** `startup::seed_fail_message` (the `UnexpectedEof` → actionable-hint branch) fired from `cmd_seed`'s `put_seed` error arm; the broker's seed-control residency (`seedmap::serve_seed_control`, held across brain restarts — 6.7). Unit: `seed_fail_eof_gives_actionable_stale_broker_hint` (EOF → the `spt daemon stop` hint naming the stale broker; a non-EOF kind → the plain message, no mis-fire).\n- **Source:** perri PREP-4 FINDING 1 (v0.9.0 dogfood) → v0.9.1.\n\n### 7.10 A VIEW is independent from the endpoint — closing the launching tab must NOT reap the daemon-hosted harness  `[REQ-HAZARD-VIEWER-CLOSE-DETACH]`\n<!-- [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH] -->\n- **Failure (Windows):** `spt endpoint run` autostarts the daemon (`ensure_running` → `detached_no_inherit`), which INHERITS the launching terminal's Windows Job Object. Windows Terminal / VS Code place the shell AND every descendant in a Job with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; closing the tab drops the job's last handle → the OS reaps the daemon + its broker-spawned ConPTY harness subtree. The `rc` pump detaching should end ONLY the viewport; the harness must keep running + stay re-attachable. (ConPTY isolation itself is already correct — portable-pty builds the pseudoconsole in the daemon; the leaking lifetime binding is the Job Object, not the console.)\n- **Invariant:** both daemon spawn paths request `CREATE_BREAKAWAY_FROM_JOB` (0x0100_0000), best-effort with an in-job FALLBACK on `ERROR_ACCESS_DENIED`(5) / `ERROR_INVALID_PARAMETER`(87) so a breakaway-denying job NEVER regresses the spawn. **A real job CAN deny breakaway** (proven: the cargo/CI runner's own ancestor job ACCESS_DENIES it), so efficacy against the real terminal is UNKNOWN until measured. **int stage = OPERATOR MANUAL ACCEPTANCE, not CI** (doyle ruling 2026-06-18): the runner sits inside a breakaway-denying job and every test-created job nests inside it → a faithful \"harness survives tab-close\" test is a guaranteed FALSE-RED; the two units (escape-where-permitted self-skip + fallback-where-denied) are the CI evidence; `required_stages` stays …\n- **spt-core mapping:** `daemon.rs::detached_no_inherit` + `deelevate.rs::create_with_token` (the breakaway flag + fallback). Units: `detached_no_inherit_falls_back_under_a_breakaway_denying_job` (no-regression — the shared runner exercises the fallback directly), `breakaway_spawn_escapes_a_kill_on_close_job` (the OS escape mechanism — self-SKIPS where the ancestor job forbids breakaway). Unix: the daemon's `setsid` session-detach already keeps a closing terminal's SIGHUP off its children (guard test, no code).\n- **Source:** v0.12.0 real-harness defect (operator) → v0.12.1 L1 @5ae68f8; doyle design+int ruling 2026-06-18.\n\n### 7.11 A dead PTY child + a dropped operator pump must NOT wedge the broker for other clients  `[REQ-HAZARD-ATTACH-WEDGE]`\n- **Failure (hypothesized, v0.12.0):** a legitimately dead PTY child (crash/kill) plus an `rc` pump dropped without a clean detach (closed tab) was thought to make the broker's loopback forward `write_all` block forever on a full 64 KB duplex, park a worker in the 2-worker net runtime, saturate both, and stall every new attach / `endpoint run` (with `daemon stop` unable to join).\n- **Invariant — DISPOSITION = PROVE-DON'T-CHANGE** (doyle GATE-PASS @e883f45, 2026-06-18): the post-L0 code ALREADY prevents the wedge; NO fail-fast / worker-count code was added. (1) `serve_attach` forwards fire-and-forget (`net_stream_send` `op_id=None`) and the broker-side `send_stream` is already deadline-bounded (`bounded_block_on`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s — hazard 7.8), not forever. (2) the loopback duplex is drained broker-INTERNALLY by the operator row's OWN read pump (`nethost.rs` `RecvHalf::Loopback`), which for an ordinary attach stream (`retentive_cap == 0`) NEVER parks → `peer_w` never backs up on a dead `rc` (a dead `rc` is just a dropped IPC subscriber against a bounded, EVICTING ring). (3) `bounded_block_on` = `runtime.handle().bloc…\n- **spt-core mapping:** int `crates/spt/tests/attach_wedge_e2e.rs` (REAL detached daemon + dummy-harness fixture): serve the victim (rc sees its tick), abruptly kill rc (dropped pump) + kill the PTY child → a NEW endpoint still comes online + is served (no wedge), the dead endpoint is offlined within a tick (`LIVENESS_RECONCILE_OFFLINE`), `daemon stop` bounded. Leans on 7.8 (the QUIC-op deadline) + 7.7 (viewer isolation).\n- **Source:** v0.12.0 real-harness finding (operator) → v0.12.1 L2 @e883f45; doyle GATE-PASS 2026-06-18.\n\n### 7.12 Controller output must NOT be written inline on the drain thread — a backed-up controller wedges the session  `[REQ-HAZARD-INJECT-CONTROL-COEXIST]`\n- **Failure (v0.12.x, operator dogfooding + doyle /diagnose 2026-06-19):** `OutputLog::append` fanned each live chunk to the CONTROLLER via a SYNCHRONOUS, blocking `write_frame` held INLINE on the session's single drain thread while `Mutex<OutputLog>` was locked (viewers already had a dedicated writer thread + bounded evicting channel). A backed-up controller socket — a slow operator, or the full 64 KB `rc` loopback duplex under heavy TUI redraw — parked the drain thread WITH THE LOG LOCK HELD: output + keystroke-echo stalled and every attach / resize / `KIND_SESSIONS` that needs the log lock blocked FOREVER, while the broker process stayed alive. Fires on NORMAL interactive `rc` use under heavy output (not only message injection); REOPENED the wedge facet o…\n- **Invariant:** controller delivery runs OFF the drain thread — a dedicated controller writer thread + bounded channel (the 7.7 viewer_writer pattern) does the blocking socket write; the drain hands each chunk off with a BOUNDED, OFF-LOCK send (deadline → detach + `clear_controller`, never park forever, never silently evict a LIVE operator's authoritative view). The controller is AUTHORITATIVE (unlike a viewer): its writer advances the `delivered_through` resume cursor (atomic, only-on-success, monotonic). `clear_controller` is the ONE unlatch path (clears `driven_by`, drops the sink) — W5's reconcile self-heal reuses it.\n- **spt-core mapping:** impl `broker.rs` `OutputLog::append`/`controller_writer`/`become_controller`/`ControllerJob::deliver`/`clear_controller`; int `crates/spt-daemon/tests/inject_control_wedge.rs` (a fully-backed-up controller keeps `KIND_SESSIONS` answering = no wedge); unit the bounded-deliver + monotonic-cursor kernels. The input-side parks (write_input `write_all` on a full buffer; DSR-answer writer-mutex contention) are BENIGN on Windows ConPTY (absorbs a large inject), real only on Unix forkpty → bounded by W2's atomic-write substrate.\n- **Source:** v0.12.x operator wedge → v0.13.0 W1 (doyle GATE-PASS 2026-06-19).\n\n### 7.13 `spt rc` must SWAP Windows legacy-console Backspace/Ctrl+Backspace (`^H`↔DEL); do NOT enable VT console input  `[REQ-HAZARD-RC-INPUT-KEY-ENCODING]`\n- **Failure (operator dogfooding):** `spt rc` is a raw verbatim stdin byte pump; the legacy Windows console delivers Backspace as `^H` (0x08), and Claude Code maps `^H` → backward-kill-word, so every Backspace deletes a whole word. Confirmed bytes (operator HITL capture, real Windows Terminal): Backspace=0x08, Ctrl+Backspace=0x7f.\n- **REJECTED fix (dc07c39, reverted):** enabling `ENABLE_VIRTUAL_TERMINAL_INPUT` on the stdin console BACKFIRED — on Windows Terminal that flag yields **win32-input-mode**, not legacy xterm VT. Every key then arrives as a 6-field `ESC[Vk;Sc;Uc;Kd;Cs;Rc_` key-down/up record: (1) the ctrl-b detach broke (0x02 is wrapped, so `parse_stdin_chunk` never sees the raw `DETACH_PREFIX`), and (2) win32-input-mode was forwarded into Claude Code's own ConPTY input negotiation (garbage). The VT lever is wrong for Windows Terminal. (Lesson: verify a terminal-input fix on the REAL terminal via the operator re-capture — unit/clippy never exercise the live key path.)\n- **Invariant:** stay a raw byte pump and NARROW-normalize in the forward path — **SWAP** the two delete bytes: `0x08` → `0x7f` (VT DEL, char-delete) AND `0x7f` → `0x08` (^H, kill-word), AFTER the detach state machine consumes any `0x02`, leaving `0x02` and every other byte untouched. Because CC reads `^H`=word-delete and DEL=char-delete, swapping the *input* bytes gives Backspace=char-delete and Ctrl+Backspace=word-delete — the native Win11 convention (the operator rejected losing word-delete; the earlier one-way `0x08`→`0x7f` map dropped it). cfg(windows) only (Unix is already VT; a real `ctrl+h`/`ctrl+?` there must pass through). Accepted minor loss: a real `ctrl+h` (0x08) and a real DEL (0x7f) are swapped on Windows — both are delete keys, so the practic…\n- **spt-core mapping:** the Backspace/Ctrl+Backspace SWAP intent persists; the `normalize_key_byte` byte-swap was SUPERSEDED on Windows by **7.16** (`REQ-RC-KEY-VT-TRANSLATE`, v0.13.0 bug 2) — the agnostic key-event→xterm-VT `translate_key_event` now emits Backspace → `0x7f` and Ctrl+Backspace → `0x08` natively (carrying the relocated `[impl]`/`[unit]` evidence), so `normalize_key_byte` + its unit were removed. The live keypress is HITL operator re-capture.\n- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19; superseded-on-Windows by the bug-2 translator 2026-06-19).\n\n### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`\n- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two facets, one root: **(A) stutter** — measured fsync on `%LOCALAPPDATA%\\spt-core` median 6.5 ms / spikes 198 ms, ×2 per keystroke → choppy, worsens with volume; **(B) HARD WEDGE** — a blocking `PtyWrite` (ConPTY/forkpty input buffer not draining) held the lock indefi…\n- **Invariant:** `apply_once` RELEASES the inner lock across `effect()` — reserve the key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable, mark applied). Crash-idempotency comes from the per-key reservation + the applied-set, not from holding the lock across the effect (the brain-crash guarantee is unchanged: the broker survives the brain, so reserve→effect→finalize runs uninterrupted on the broker thread; a replay hits the applied-set and dedups). `EffectKind::PtyWrite` is **EPHEMERAL** — NO journal lines, NO fsync, in-memory dedup only (a keystroke lost to a broker crash is retyped; PTY state is never rebuilt from keystroke replay) — while durable kinds (spool/r…\n- **spt-core mapping:** impl `effect.rs` `apply_once` (reserve/release/finalize) + `EffectKind::is_durable`; unit (`effect.rs`) a barrier proving the lock is NOT held across `effect()` (a different key's `apply_once` completes while one effect blocks) + `PtyWrite` writes no journal line while a durable kind (`NetSend`) does (the no-fsync proxy); int `crates/spt-daemon/tests/inject_control_wedge.rs` (real broker+PTY, a stalled consumer + sustained blocking input effect: a concurrent `spt rc` attach stays serviceable AND actually receives PTY bytes — the assertion W1's gate lacked). Unix park-(b) folded here, gated on gravity-linux. doyle GATE-GUARD: assert structurally, NEVER on fsync wall-clock (env-dependent, flaky); the stutter is fixed as a consequence of…\n- **Source:** v0.13.0 W1b (operator dogfooding post-W1 escape; doyle /diagnose + measurement 2026-06-19).\n\n### 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- **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 already clears the `status=online` latch here but left `driven_by` untouched.\n- **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 disconnect already self-heals via `detach_if`→`clear_controller`, W1 (7.12) bounds the active-output wedge, and the residual idle-wedged-REMOTE case is the deferred `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`, which needs BROKER-SIDE liveness eviction — a brain reconcile can …\n- **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- **spt-core mapping:** impl `livehost.rs` `reconcile_hosted_liveness` Gap-B clear + the additive `SessionInfo.controller_by` observability field (`msg.rs`, populated in `broker.rs` `KIND_SESSIONS`); unit `livehost.rs` `pull_liveness…` extended (offlined sessionless perch clears `driven_by`; live/relay perches untouched); int `crates/spt-daemon/tests/driven_by_selfheal.rs` `gap_b` (real broker: reconcile offlines AND clears `driven_by`) + the A1/A2 characterization (`inject_control_wedge.rs` w5_a1/w5_a2).\n- **Cross-ref 7.29 (who MAY clear vs who MUST NOT):** the BRAIN reconcile here MUST NOT clear `driven_by` off `controller_by==None` (ambiguous with a live LOCAL controller). The BROKER stamp-convergence (7.29) MAY — it reads `has_controller()` DEFINITIVELY (its own controller slot), so it authoritatively clears a stale `driven_by` against a live session with no controller (the `has_controller()==false` leg). The invariant's owner for that leg moved to the broker; this brain-side ambiguity-safety is unchanged. The wedged-open-remote A2 residual (`has_controller()==true`) is NOT covered by either — still `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.\n- **Source:** v0.13.0 W5 (repro-first, todlando; doyle-assigned 2026-06-19). A2 (idle wedged-remote leg) deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.\n\n<!-- [doc->REQ-RC-KEY-VT-TRANSLATE] -->\n### 7.16 `spt rc` translates Windows console KEY EVENTS to standard xterm VT (arrows/Home/End/F-keys reach the harness); supersedes the W7 byte-swap  `[REQ-RC-KEY-VT-TRANSLATE]`\n- **Failure (operator dogfooding):** `spt rc` read raw stdin BYTES, but the Windows legacy console (no `ENABLE_VIRTUAL_TERMINAL_INPUT`) delivers arrows / Home / End / PgUp / PgDn / Insert / Delete / F-keys as console KEY_EVENTs, NOT stdin bytes — so the byte-pump saw nothing and those keys were DEAD (only byte-emitting keys like Backspace worked, via the 7.13 swap).\n- **Invariant:** on Windows, read crossterm KEY EVENTS (the picker already does) and translate each to STANDARD xterm VT via the pure `translate_key_event` (copy a known-correct xterm table verbatim: arrows `ESC[A..D`, Home `ESC[H` / End `ESC[F`, `~` keys `ESC[<n>~`, modified `ESC[1;<m><final>` / `ESC[<n>;<m>~` with `m = 1 + Shift + 2·Alt + 4·Ctrl`, F1–F4 `ESC OP..S` / F5–F12 `ESC[<n>~`), forwarded through the SAME rc pump — the harness receives ordinary xterm VT (AGNOSTIC; NOT win32-input-mode, the 7.13 rejected lever). Press-only (drop Repeat/Release). Detach stays the `ctrl-b d` PREFIX, event-sourced (Ctrl+B arms; armed + plain `d` ⇒ Detach; armed + Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` + translated). NON-tty stdin (piped / tests) falls back to …\n- **spt-core mapping:** impl `rc.rs` `translate_key_event` + `csi_final`/`csi_tilde`/`f_key` + `key_event_step` (event detach SM) + `spawn_stdin_reader` cfg-split (`spawn_stdin_reader_events` on a tty / `spawn_stdin_reader_bytes` non-tty + Unix); unit the EXHAUSTIVE `translate_key_event` mapping + the event-detach SM (the Backspace/Ctrl+Backspace arm carries the relocated `REQ-HAZARD-RC-INPUT-KEY-ENCODING` evidence); NO int (live console = HITL, operator re-capture — REQ-RUN-PICKER/RC-1 precedent).\n- **Source:** v0.13.0 bug 2 (operator ruling: proper agnostic translator, ship-blocker; doyle design + Option-B detach ruling 2026-06-19).\n\n<!-- [doc->REQ-RC-WIN-PASTE] -->\n### 7.18 `spt rc` paste is client-originated on Windows — read the LOCAL clipboard, inject a BRACKETED paste  `[REQ-RC-WIN-PASTE]`\n- **Failure (operator dogfooding):** in an `spt rc` session neither ctrl+V nor right-click pasted (CC explicitly supports ctrl+V). `RawGuard` did 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 translated to a 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. A multi-line paste-as-keys also became a `\\r` submit-storm.\n- **Invariant:** on Windows (cfg-split; folds into the 7.16 event path), on a RIGHT-CLICK rc reads the LOCAL clipboard itself and forwards a BRACKETED paste — `wrap_bracketed_paste` = `ESC[200~` + content + `ESC[201~`. CC has bracketed-paste mode on (its TUI sets `ESC[?2004h`), so it treats the synthesized markers as a PASTE: content lands intact, NO submit-storm, harness-AGNOSTIC (standard xterm contract). `RawGuard` also `EnableMouseCapture` (disables console QuickEdit + enables `ENABLE_MOUSE_INPUT` so a right-click surfaces as `Event::Mouse`) on an interactive console only, restored on drop → right-button-down → `read_clipboard` → bracketed paste. `read_clipboard` = the `clipboard-win` crate; empty/failed read ⇒ a clean no-op (never inject garbage, never …\n- **spt-core mapping:** impl `rc.rs` `wrap_bracketed_paste` + `mouse_is_paste` + `clipboard_paste` + `read_clipboard` + `windows_mouse_wanted` + `RawGuard` (mouse capture/restore) + the `spawn_stdin_reader_events` right-mouse arm; unit the exact bracketed framing + content-verbatim + mouse classify (right-down ⇒ paste, all else ⇒ drop) + the injected-reader paste decision (non-empty ⇒ wrapped, empty/fail ⇒ no-op); NO int (live clipboard + console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent).\n- **Source:** v0.13.0 P1 (operator HITL; doyle design 2026-06-19). Depends on P0 (7.17). AMENDED v0.13.0 P1b: scope narrowed to right-click-only (the dead ctrl+V interception removed); scroll-forward is 7.20; the input-flood non-wedge is 7.19.\n\n<!-- [doc->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] -->\n### 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- **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 → the broker→brain return direction fills (~10 frames = the IPC pipe buffer) → `send_frame` BLOCKS → the handler stops reading → the brain's writes block → mutual full-duplex DEADLOCK. (Capture: 11 frames, `write_input` 11/11 — P0 holds; `ack send` START=11/END=10…\n- **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=true`). EXACTLY-ONCE preserved: the broker dedups by `(session, op_id)` at the applied-set regardless of the ack. **N-1 caveat:** an OLD resident broker (the self-update window) ignores `ack=false` → still acks → the deadlock persists until a broker restart (inher…\n- **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 session stays live + a concurrent attach opens (real broker+brain, no mocks).\n- **Source:** v0.13.0 P1b (operator HITL re-open; doyle /diagnose on the enhanced rc+broker capture 2026-06-19). The real ship-blocker behind the ctrl+V wedge.\n\n<!-- [doc->REQ-RC-MOUSE-FORWARD] -->\n### 7.20 `spt rc` must forward the scroll wheel to the harness (our mouse capture steals WT's native scroll)  `[REQ-RC-MOUSE-FORWARD]`\n- **Failure (operator HITL):** scroll stopped working in `spt rc`. P1's `EnableMouseCapture` (for right-click paste, 7.18) makes Windows Terminal forward ALL mouse — including the wheel — to rc instead of scrolling its own buffer, but the rc mouse handler dropped everything except right-button-down → scroll DIED (and WT's native scrollback is stolen under the capture).\n- **Invariant:** on Windows, TRACK the harness's mouse-reporting mode from its OUTPUT — scan for DECSET `ESC[?1000h/1002h/1003h` (mouse on) + `ESC[?1006h` (SGR ext) and their `…l` (off) into a shared `MouseMode{enabled,sgr}` (the pump writes from the output render path, the stdin reader reads); the scan survives a sequence SPLIT across output chunks (a bounded carry buffer). The mouse handler: right-button-DOWN → bracketed clipboard paste (7.18, unchanged); `ScrollUp/Down` → an xterm SGR mouse report (`ESC[<64;col+1;row+1M` up / `ESC[<65;…M` down; 0-based crossterm → 1-based xterm), forwarded ONLY when `enabled && sgr` (else DROP — a legacy report the harness may misread is garbage); Moved/drag/left/middle DROP (scroll is the need; click-forward risks garbag…\n- **spt-core mapping:** impl `rc.rs` `MouseMode` + `MouseModeScanner`/`parse_decset_private`/`apply_mouse_mode` (carry-buffer scan, fed from the pump's output path) + `scroll_dir` + `scroll_sgr` + the `spawn_stdin_reader_events` scroll arm (threaded via `Arc<MouseMode>`); unit `scroll_dir` classify + `scroll_sgr` exact bytes + the DECSET scan (set/reset, combined, mixed, and split-across-chunks); NO int (live console mouse = HITL).\n- **Source:** v0.13.0 P1b (operator HITL; doyle design 2026-06-19). Bundled with 7.19.\n\n<!-- [doc->REQ-HAZARD-CONTROLLER-WRITER-REORDER] -->\n### 7.21 Exactly ONE `controller_writer` per brain↔broker connection — a superseded writer must write nothing further  `[REQ-HAZARD-CONTROLLER-WRITER-REORDER]`\n- **Failure (doyle instrumented RACEDIAG repro, kitsubito):** on a brain-restart re-serve the handoff brain registered as controller on the SAME session TWICE over the SAME socket — `Brain::handoff` eagerly `subscribe(prior.next_seq=1)` → `become_controller(from_seq=1)`, spawning writer-A (writes seq 1); then `serve_attach` re-handled the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, spawning writer-B (writes 0,1). `become_controller` dropped the prior `ControllerSink` (its `tx`) but did NOT stop the prior writer — writer-A kept flushing its OWNED `initial` batch, and both writers held clones of one `SharedSend` (`Arc<Mutex<socket>>`) with no inter-thread ordering. When writer-A's seq 1 beat writer-B's seq 0, the stri…\n- **Invariant:** on a single brain↔broker connection exactly ONE `controller_writer` is ever the LIVE writer; a SUPERSEDED writer writes no further frames after the epoch bump it observes; and **every `controller_writer` emits a strictly ASCENDING seq stream** (sorted initial batch + ascending live frames). The CORRECTNESS guarantee that falls out: a snap-above consumer over any interleaving of ascending writers — where the surviving writer (`serve_attach`'s `attach_as(sid,0)`) offers the COMPLETE range `[0, end]` — delivers `[K, end]` with **no skip and no dup** (the first sighting of any seq `>M` is always preceded by a sighting of `M` on that same ascending writer, so `M` is delivered before the cursor can pass it). Enforced/relied-on three ways (NB: fix …\n- **spt-core mapping:** impl `brain.rs` `Brain::handoff` (KEEP the eager `subscribe`; seed `session_cursors`) + `Brain::subscribe_with` (resume-mode dedup-cursor reset to `from_seq`, shared by `attach`/`attach_as` — the operator-stream boundary fix) + `broker.rs` `OutputLog.controller_epoch: Arc<AtomicU64>` / `become_controller` (atomic `fetch_add`, passes the new epoch + `Arc::clone` into the writer) / `controller_writer` (epoch gate read UNDER the lock on both loops) / `mark_controller_gone` + the `ControllerJob` epoch read; unit (white-box, `src/broker.rs`) the epoch-gated writer (a superseded writer flushes nothing — only the latest writer's monotonic stream reaches the wire) + `handoff` seeds `session_cursors`/resubscribes; int (keystone, `tests/broker.…\n- **Source:** v0.13.0 P1c (operator-ruled root-fix before ship; doyle root-cause via instrumented repro 2026-06-20; design corrected across two gate rounds — fix #1 reverted, then the kitsubito RACEDIAG pinned the residual to the consumer-side operator-stream boundary, fixed by the `subscribe_with` cursor reset on `attach_as` re-subscribe). The last v0.13.0 ship-blocker.\n\n### 7.22 An idle delivery with no working translation binary must SPOOL, never raw-inject a pseudo-delivery reported as delivered  `[REQ-HAZARD-IDLE-SILENT-NONDELIVERY]`\n- **Failure (F-019 post-mortem):** the v0.11.0 path raw-injected `payload+\\r` into the spt-hosted PTY whenever no working translation binary handled an inbound message — none declared, spawn-failed, FAULTED (commit-deadline miss / binary death), or its inject-worker channel dropped — AND acked `delivered=true`. But a bare `payload+\\r` does NOT submit on a modern TUI (Claude Code): the message was TYPED into the harness but never sent, a silent pseudo-delivery reported as success. That silent degrade-to-raw-inject is precisely what MASKED F-019 (`[REQ-INSTALL-11]`, above) through a multi-hour black-box hunt — every behavioral hypothesis was moot because the \"delivery\" never delivered.\n- **Invariant:** spt-hosted idle delivery is translation-binary-ONLY (ADR-0022 amendment). `dispatch_endpoint_input` with no working binary replies `endpoint_injected_envelope(ep, delivered=false)` and writes NOTHING to the PTY; the caller (`try_broker_inject` → `cmd_send`) reads `delivered=false` and falls through to `deliver::send` = SPOOL (poll-fed, never lost), reporting an honest `QUEUED`, never a confident-but-false `SENT`. The failure is LOUD (`ENDPOINT_INJECT:<ep>: no working translation binary … -> SPOOLED, not injected`). The raw-inject fallback (`input.enqueue`) is removed from the no-binary, worker-dropped, AND post-fault paths; the operator-keystroke floor-flush on FAULT is UNCHANGED (operator input is never stranded). Out of scope (follow-up): …\n- **Boundary — what this does NOT cover (the STATE-vs-transient precision):** the guarantee is the steady FAILED STATE — once a binary IS faulted/absent/worker-gone, subsequent deliveries spool (`faulted` is MONOTONIC: set once, never respawns, so the state is reached deterministically). It does NOT cover the FAULT-TRANSIENT: a delivery that lands in the worker's commit window — BEFORE `event_rx` is dropped / `faulted` is set — can be optimistically enqueue-acked (`delivered=true` the instant `event_tx.send` succeeds) then DROPPED when the worker faults+returns. This is a SEPARATE, PRE-EXISTING hazard (raw-inject removal did not touch it — the old code dropped that queued event too; v0.14.3 makes nothing worse) and is tracked for **v0.15.0 under `[REQ-MSG-DE…\n- **spt-core mapping:** impl `broker.rs` `dispatch_endpoint_input` (no-working-binary → `delivered=false` + loud log, no `input.enqueue`; the `input` writer is no longer resolved from the sessions table) + `build_translation`/`fault_translation` (`None`/fault now MEAN spool); unit `msg.rs` `endpoint_injected_envelope_carries_delivered_both_ways` (the `delivered=false` spool signal round-trips); int (real broker+PTY) `tests/broker.rs` `endpoint_keyed_inject_without_binary_spools_not_pty` + `tests/inject_control_wedge.rs` `large_endpoint_inject_to_a_no_binary_session_spools_promptly_without_wedging` + `g2_no_commit_deadline_faults_binary_and_does_not_wedge_controller_input` (post-FAULT inbound EVENTUALLY spools via bounded-retry-until-spool, marker NEVER on PT…\n- **Source:** v0.14.3 (ADR-0022 amendment @1eaeef1, operator-ruled; doyle-scoped, the F-019 follow-up). CHANGE-5 (`cli.rs` honest report) verified a NO-OP — the spool fall-through already reported `QUEUED`, never `SENT`.\n<!-- [doc->REQ-HAZARD-IDLE-SILENT-NONDELIVERY] -->\n\n---\n\n### 7.23 A message that has REACHED a node's spool must NEVER depend on an adapter hook-poll cadence to reach an spt-hosted (relay-less) endpoint — the daemon drives delivery on the events it owns  `[REQ-HAZARD-DELIVERY-STARVATION]`\n- **Failure (F-023, RCA @ `f023-f024-wan-idle-starvation`):** a message that has already landed in a node's spool — WAN-arrived, or locally spooled-while-active — stranded INDEFINITELY on an spt-hosted, relay-LESS endpoint whenever the adapter's hooks went quiet (an idle session with no user turns). An spt-hosted perch has no `api listen` relay to wake it, and NOTHING in the daemon re-offered a spooled message once the arrival moment passed — delivery silently depended on the adapter happening to call `api poll`. Presented as \"sent but never lands\" with a HEALTHY translation binary and a visibly-idle perch (F-023: verified 7 min idle, binary alive, zero injection). TWO gaps: (1) the WAN-ingress path (`receive_wan`) tried the harness-hosted TCP relay then fel…\n- **Invariant:** the daemon DRIVES delivery of an already-spooled message on the events IT owns — never on hook cadence — for a relay-less spt-hosted endpoint. TWO daemon triggers feed ONE shared spt-hosted inject leg (`spt_daemon::inject::try_spt_hosted_inject`, translation-binary-ONLY): (1) **WAN ingress** — `receive_wan` injects via the live binary BEFORE the spool fallback (`[REQ-WAN-SPT-HOSTED-DELIVERY]`, leg 1); (2) **the ACTIVE→IDLE edge** — `drain_idle_window` (fired by `api state idle`) claims the pending spool (deferred AND non-deferred) and injects each via the same leg, reusing the hook-poll take/ack so a concurrent `api poll` cannot double-deliver (`[REQ-MSG-IDLE-EDGE-DRAIN]`, leg 2). The v0.14.3 LAW holds on BOTH triggers (7.22): no working bin…\n- **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 delivery with NO hook poll.\n- **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).\n<!-- [doc->REQ-HAZARD-DELIVERY-STARVATION] -->\n\n---\n\n### 7.24 A delegated live adapter apply must NEVER report success without swapping, and the live-update seam must use ONE parent-aware adapter matcher  `[REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP]`\n…\n\n…\n\n\n[Showing lines 329-523 of 736. Use :524 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"- **Origin:** unintended spec/impl drift from ADR-0004 (the broker *process* was specced + spiked but built in-process), discovered during the v0.3.2 fleet update verify. Full audit + decisions: `docs/BROKER-BRAIN-SPLIT-RESTORATION.md` (verified) + ADR-0018.\r\n<!-- [doc->REQ-HAZARD-BROKER-PROCESS-ISOLATION] -->\r\n- **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.\r\n- **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 `apply` rename to `.old-N` and ran OLD bytes under an `applied` record (`[REQ-HAZARD-BRAIN-RESPAWN-PATH]`, 6.11), fixed in v0.4.2 (respawn from the canonical path captured at broker start + a promotion bytes-gate). Seamless update is now proven on **both OSes** — Win…\n\r\n### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`\r\n- **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).\r\n- **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).\r\n- **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.\r\n- **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.\r\n- **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 names present; an unknown extra field still deserializes), so a *non-additive* pre-ready change (renamed/removed field, or a `deny_unknown_fields`/non-tolerant shape) trips the test and forces the migration **behind ready-promotion** (or into an additive form). *…\n- **Origin:** verification amendment `[V1]` (agent `doyle`) on `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.\r\n- **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't-pre-fail); the tripwire is the wire it trips the day one is needed.\r\n<!-- [doc->REQ-HAZARD-ROLLBACK-STATE-COMPAT] -->\r\n\r\n### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output\r\n- **Failure:** a resume-mode brain (per-session `session_cursors` populated by `resume_sessions`) drives `Brain` over **blocking** `read_event` calls. Any command that loops `read_event` until its own reply — `spawn_session_pid` waiting for `Spawned`, `net_status`, `sessions`, etc. — calls `read_event` on *every* interleaved frame, so an OUTPUT frame for a **different** session is **cursor-processed** (its `session_cursors` entry snaps forward; the broker already counted it delivered via `delivered_through` on the live-send) and then **discarded** by the waiting loop's `_ => continue`. Cursor advanced + content dropped = that chunk is gone for the downstream consumer and the broker will **not** re-send it (resume reads from the delivered cursor, ADR-0018 D4)…\n- **Invariant:** the daemon-hosted multi-session event loop must not consume a session's OUTPUT inside another session's blocking wait. The **live-agent adapter milestone must restructure the brain event pump** so command/`spawn` is non-blocking (a single demux loop owns `read_event` and routes every frame to its session's consumer), OR a blocking wait must re-queue/route the frames it reads for other sessions rather than dropping them.\r\n- **spt-core mapping:** **unreachable today** — the supervised daemon brain hosts no PTY sessions and spawns none; a single-session seat (legacy, empty map) has no \"other session\" to starve. Surfaces the moment daemon-hosted sessions land (the live-agent adapter), which must rebuild the blocking `read_event` loop regardless (N interactive sessions cannot share one blocking reader). Recorded so that redesign inherits the constraint rather than rediscovering it. No machinery now (would be untested dead code, activate-don't-pre-fail).\r\n- **Origin:** surfaced by the D4-2b resume-harness CI flake root-cause (agents `todlando` + `doyle`, 2026-06-10); sibling of `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]` 6.7.\r\n\r\n### 6.10 Phase-significant loop timing must be a durable absolute-deadline grid, not phase-relative sleep `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\r\n- **Failure:** a periodic loop that sleeps a flat `period` each iteration (`pulse_tick` then `sleep(pulse_period)`) is **phase-relative** — every brain restart silently re-phases the grid to the restart instant. Under the seamless-update model (the supervisor respawns the brain onto a swapped binary, ADR-0018 D3-3), a routine update would shift the cadence of every phase-significant loop, and continuity cannot ride a brain→brain frame (the outgoing brain is gone before the new one starts — the same constraint that moved session continuity to the broker in D4).\r\n- **Invariant:** phase-significant periodic timing lives as durable absolute-deadline state on disk (`(anchor, interval)`), rehydrated on every brain start, with fires **derived functionally** (`next_fire = anchor + interval·⌈max(0,now−anchor)/interval⌉`) and **no per-fire write**. An **Update** restart re-reads the anchor and keeps deriving (phase preserved, lands mid-grid); a **Crash**/**Cold** restart re-bases the anchor to `now` (phase reset acceptable — the loop is idempotent catch-up). The update-vs-crash decision is the D3 spawn-time `StartReason`. **One-shot** (alarm) deadlines persist their absolute `target-time` at creation and **never reset** on any restart (\"remind me at 3pm\" is a commitment) — the asymmetry vs the periodic crash-reset is the rul…\n- **spt-core mapping:** ADR-0018 Q4/V3/V4, restoration D5. Mechanism in `spt-daemon::deadline` (`DeadlineAnchor` periodic + `OneShotDeadline` rule-only pure helper); the pulse loop (`lifecycle::run_pulse_loop`) consumes it. The one-shot **machinery** (a durable in-daemon alarm scheduler) is the deferred alarm port (`docs/DEFERRED.md`) — the daemon has no one-shot consumer today, so building the timer now would ship untested dead code (activate-don't-pre-fail); D5 fixes the *rule* as a tested-unwired helper, the port builds the *scheduler*.\r\n- **Origin:** ADR-0018 Q4 + verification amendments `[V3]`/`[V4]` (agent `doyle`); D5 plan vet (agents `todlando` + `doyle`, 2026-06-10).\r\n\r\n### 6.11 Brain respawn must exec the APPLIED bytes, not the renamed old binary (Linux `current_exe` follows the apply-rename; readiness ≠ new-bytes) `[REQ-HAZARD-BRAIN-RESPAWN-PATH]`\r\n- **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**, and the daemon records `applied:N` while still running the previous version. New code does not run; the record is optimistically wrong (the enlyzeam-class record/reality divergence, now provable via `exe_hash`). **Windows** dodged it — `GetModuleFileName` returns…\n- **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 the new bytes run). If either hash is absent the gate degrades to readiness-only (N-1-safe for pre-metadata releases / a missing breadcrumb) but emits `PROMOTE_BYTES_UNVERIFIED` so a disarmed gate stays field-diagnosable.\r\n- **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.\r\n- **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.\r\n<!-- [doc->REQ-HAZARD-BRAIN-RESPAWN-PATH] -->\r\n\r\n---\r\n\r\n## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)\r\n\r\nThese were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.\r\n\r\n### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`\r\n- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).\r\n- **Invariant:** every `api` *mutation* is authenticated to an endpoint/session — per-endpoint link token or OS-credential binding. An unrelated local process cannot bind, inject, end, or spoof state.\r\n- **spt-core mapping:** the `api` subcommand surface (PRD R-API-*) + broker IPC.\r\n- **Source:** codex Stage A #13 (`docs/reviews/STAGE-A-codex-redteam.md`).\r\n\r\n### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`\r\n- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.\r\n- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.\r\n- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.\r\n- **Source:** codex Stage A #14.\r\n\r\n### 7.3 Psyche outbound capture + sanitization  `[REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]`\r\n- **Failure:** the Psyche's sole outbound channel is its **stdout** (`<EVENT type=\"reply|notify\">` intents — ADR-0012). Two ways to break it: (a) a **null-stdout / detached** live-Psyche driver silently **discards every reply and notify**; (b) the daemon relays a Psyche-supplied `from=`/target **unchanged**, letting a sandboxed Psyche **spoof identity** or address arbitrary endpoints.\r\n- **Invariant:** the live-Psyche turn driver **MUST capture stdout** (a bounded, stdin-fed, stdout-captured invocation — **never** `Stdio::null()`); the daemon **MUST strip** every Psyche-supplied `from=`/target/routing attribute, **re-stamp `from=<self_id>`**, and **constrain routing** — `reply` → the inbound message's structural sender (its `from`) only, `notify` → the agent's own user/subnet only. Body validated per 4.1.\r\n- **spt-core mapping:** the live-Psyche turn driver + daemon outbound relay (ADR-0012); `spt-proto::event` type taxonomy (`+reply`/`+notify`). The interim `runtime::spawn_session` `Stdio::null()` path is **not** the live-Psyche driver.\r\n- **Source:** grill-with-docs 2026-06-03 (uncovered by prior design passes); sister `src/live/wrapper/claude.rs` (sandbox `[\"Read\",\"Write\",\"Edit\"]` + `parse_markers`).\r\n\r\n### 7.4 Per-agent pulse/psyche/echo scheduling must not serialize across agents  `[REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]`\r\n- **Failure:** echo-commune (`run_bounded_stdin`) and the live-Psyche turn driver (D7.5) are **bounded LLM calls that block their calling thread** until the child answers or the timeout fires. Today each agent drives its own pulse from its **own process** (`spt/src/api/{live,startup}.rs`), so blocking is isolated. When the daemon hosts **N per-agent loops** (ADR-0004 target; the `run_pulse_loop` fan-out is currently def+test only), a **single serial driver** that calls these invocations inline lets one agent's slow/hung LLM call **stall every other agent's heartbeat, commune, and reply** — the per-call timeout bounds the worst case *only* when the work is isolated; serial makes the stalls additive.\r\n- **Invariant:** each agent's bounded LLM-bearing work (echo-commune summarizer, Psyche turn) runs on its **own thread / off the shared scheduler** — no single-threaded driver iterating all agents may call a blocking invocation inline. One agent's slow/timed-out call must not delay another agent's next tick beyond tolerance.\r\n- **spt-core mapping:** the daemon's multi-agent pulse/psyche hosting (ADR-0004, \"all Psyche/pulse loops\" consolidated); `run_pulse_loop` fan-out; echo-commune + the D7.5 Psyche driver.\r\n- **Source:** grill-with-docs 2026-06-03 (forward invariant — the multi-agent fan-out is not yet wired). Distinct from ADR-0002 SERIOUS #6 (crash blast-radius, not scheduling latency).\r\n\r\n### 7.5 WAN-inbound origin is transport truth, never payload  `[REQ-HAZARD-WAN-ORIGIN-AUTH]`\r\n<!-- [doc->REQ-HAZARD-WAN-ORIGIN-AUTH] -->\r\n- **Failure:** the ADR-0009 access whitelist gates **unsolicited wire inbound by origin node**. If the gate's subject is read from record bytes (an `origin_node`/`from`/`node` field a sender wrote), any sender forges any origin and the whitelist is decoration — same spoof class as 7.3's Psyche-supplied `from=`, now on the cross-node surface.\r\n- **Invariant:** the origin the gate (and detection/UX — \"node X is driving\") consumes is the **QUIC handshake-proven remote node id** (iroh `EndpointId` == Ed25519 node pubkey, the REQ-NET-1 identity binding) read from the **broker's conn/stream table** (`NetStreamInfo::remote_id_hex`), never from payload. Wire records carry **no origin field by design**; a forged one decodes as an ignored unknown field and influences nothing. `from` inside a record is reply-routing metadata only — never an authorization subject.\r\n- **spt-core mapping:** `spt_net::net::wanmsg::WanMessage` (no origin field) + `spt_daemon::wan::receive_wan` (origin parameter sourced from the stream table) + every future wire-inbound consumer (D5b attach, D5c transfer, D8 notifs).\r\n- **Source:** M4-D5a design (ADR-0009 consequence: the gate must bind to the transport identity).\r\n- **Mesh note (ADR-0017, 2026-06-08):** preserved verbatim. The mesh never relays third-party rows, so a record's author is **always** the QUIC-handshake-proven connection origin. The new **membership proof** (seed-proof) is itself channel-bound to both handshake-proven pubkeys — the authorization subject stays transport truth, never payload.\r\n\r\n### 7.6 Pump brain-IPC reads must be deadline-bounded (a blocked read wedges the whole pump)  `[REQ-HAZARD-PUMP-IPC-DEADLINE]`\r\n<!-- [doc->REQ-HAZARD-PUMP-IPC-DEADLINE] -->\r\n- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop-top) stops and the stall warning fires honestly, but `supervise_pump` cannot rescue it: the supervisor catches a panic / error / clean return, and a BLOCKED thread never returns. Observed twice — 2026-06-07, then a **2.2h wedge on hfenduleam (2026-06-11)** w…\n- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a healthy long sync is never killed — only a ≥30s silence). **Mechanism note (Windows):** the carrier is a reader-thread + channel, NOT a non-blocking socket + poll — interprocess 2.4.2 on Windows named pipes has no portable read timeout (`set_recv_timeout` → `no_tim…\n- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the `net_dial`/`open_stream`/`send_stream` handlers) — shipped in v0.8.3 as §7.8 (`REQ-HAZARD-BROKER-QUIC-DEADLINE`).\r\n- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.\r\n\r\n### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`\r\n<!-- [doc->REQ-HAZARD-VIEWER-ISOLATION] -->\r\n- **Failure:** the W2.5 controller/viewer model lets ANY number of read-only `--view` attachers ride one session's broker `OutputLog`. The single drain thread fans each output chunk to every attacher. If a viewer's socket is fanned out with a **blocking** write under the log lock (the controller's authoritative path), one wedged viewer (a slow terminal, a black-holed WAN peer, a hostile non-reader) stalls the drain — freezing the controller's stream and backing up the PTY child. A single watcher must never be able to degrade or hang the driver.\r\n- **Invariant:** the drain writes the **controller** on the authoritative blocking bounded path (it alone advances `delivered_through`), but each **viewer** gets an **isolated bounded SPSC queue + a dedicated writer thread**; the drain `try_send`s under the log lock and **evicts** any viewer whose queue is `Full` (fell behind the live stream) or `Disconnected` (its writer died on a dead socket) — the drain thread **never touches a viewer socket**, so no viewer write can backpressure it. A **soft cap** (`MAX_VIEWERS`) bounds the writer-thread count (a viewer attach beyond it is refused). Viewer eviction never perturbs the controller stream, the `delivered_through` cursor, or the child. Ring **replay-at-attach** is owned by the writer thread (not the bounded l…\n- **spt-core mapping:** `OutputLog::append` (controller blocking + `viewer_send_evicts` `try_send` fan-out), `OutputLog::add_viewer` (bounded `sync_channel(VIEWER_CHANNEL_DEPTH)` + `viewer_writer` thread), `MAX_VIEWERS` soft cap, `ViewerSink`. Unit: `viewer_overflow_or_disconnect_evicts_never_blocks`. Int: `wedged_viewer_does_not_stall_controller` (a non-reading viewer is evicted while the controller keeps receiving past a 200KB burst).\r\n- **Source:** M12 W2.5 controller/viewer model (doyle ruling 2026-06-14, Q1).\r\n\r\n### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`\r\n<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\r\n- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only via its OWN 30s read-deadline (the 7.6 A-half) — a 30s stall + a full pump restart EACH round, and the supervised restart re-dials the SAME dead peer and re-wedges. Root cause of the 2.2h hfenduleam wedge (2026-06-11) and its recurrence (2026-06-16).\r\n- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`, so the BROKER fires FIRST — the brain reconstructs `ErrorKind::Other` (its `net_dial`/`net_open_stream`/`net_stream_send` map a broker error reply to `io::Error::other`), NOT its own read-deadline `TimedOut`, so `pump::peer_outcome` takes the ordinary per-pee…\n- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIES an ordinary error within the bound + exactly-once-on-timeout: journal un-applied, conn table empty, clean redial) and `pump_survives_a_black_holing_peer_heartbeat_advances_no_restart` (the production pump against a dead peer — heartbeat monotonic-advances, `run…\n- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.\r\n\r\n### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`\r\n<!-- [doc->REQ-HAZARD-BROKER-SEED-WIRE-SKEW] -->\r\n- **Failure:** the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004's no-terminate-during-update pillar forbids auto-killing it — 6.7). A self-update that changes a daemon-state WIRE FORMAT — e.g. the v0.9.0 adapter-agnostic `Seed` (the `adapter` field dropped) — therefore lands a NEW-version CLI talking to the STILL-RESIDENT OLD broker. The old broker cannot deserialize the new `Seed` (its formerly-required `adapter` is absent), so it drops the seed-control conn without acking; the CLI's `put_seed` ack-read hits EOF and surfaces a raw `UnexpectedEof` \"failed to fill whole buffer\" — a cryptic footgun that hides the real cause (perri PREP-4 FINDING 1: v0.9.0 CLI ↔ stale 0.8.x broker).\r\n- **Invariant:** (a) spt-core surfaces an ACTIONABLE diagnostic on the seed-ack EOF — naming the stale-broker cause + the fix (`spt daemon stop`; the broker restarts on the next `spt api` call) — never the bare io error (scoped to `UnexpectedEof` on the seed-ack path, so it never mis-fires on an unrelated error like a refused connect). (b) A daemon-state wire change requires a DELIBERATE full broker restart; this is NOT automatic — ADR-0004 forbids auto-killing the resident broker, and a brain-only update keeps the old broker. (c) FORWARD discipline: daemon-state / `Seed` schema changes stay ADDITIVE + serde-default, so a resident OLD broker tolerates a NEW CLI across a brain-only update. (This would NOT have rescued 0.9.0 itself — the old broker's `adapter`…\n- **spt-core mapping:** `startup::seed_fail_message` (the `UnexpectedEof` → actionable-hint branch) fired from `cmd_seed`'s `put_seed` error arm; the broker's seed-control residency (`seedmap::serve_seed_control`, held across brain restarts — 6.7). Unit: `seed_fail_eof_gives_actionable_stale_broker_hint` (EOF → the `spt daemon stop` hint naming the stale broker; a non-EOF kind → the plain message, no mis-fire).\r\n- **Source:** perri PREP-4 FINDING 1 (v0.9.0 dogfood) → v0.9.1.\r\n\r\n### 7.10 A VIEW is independent from the endpoint — closing the launching tab must NOT reap the daemon-hosted harness  `[REQ-HAZARD-VIEWER-CLOSE-DETACH]`\r\n<!-- [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH] -->\r\n- **Failure (Windows):** `spt endpoint run` autostarts the daemon (`ensure_running` → `detached_no_inherit`), which INHERITS the launching terminal's Windows Job Object. Windows Terminal / VS Code place the shell AND every descendant in a Job with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; closing the tab drops the job's last handle → the OS reaps the daemon + its broker-spawned ConPTY harness subtree. The `rc` pump detaching should end ONLY the viewport; the harness must keep running + stay re-attachable. (ConPTY isolation itself is already correct — portable-pty builds the pseudoconsole in the daemon; the leaking lifetime binding is the Job Object, not the console.)\r\n- **Invariant:** both daemon spawn paths request `CREATE_BREAKAWAY_FROM_JOB` (0x0100_0000), best-effort with an in-job FALLBACK on `ERROR_ACCESS_DENIED`(5) / `ERROR_INVALID_PARAMETER`(87) so a breakaway-denying job NEVER regresses the spawn. **A real job CAN deny breakaway** (proven: the cargo/CI runner's own ancestor job ACCESS_DENIES it), so efficacy against the real terminal is UNKNOWN until measured. **int stage = OPERATOR MANUAL ACCEPTANCE, not CI** (doyle ruling 2026-06-18): the runner sits inside a breakaway-denying job and every test-created job nests inside it → a faithful \"harness survives tab-close\" test is a guaranteed FALSE-RED; the two units (escape-where-permitted self-skip + fallback-where-denied) are the CI evidence; `required_stages` stays …\n- **spt-core mapping:** `daemon.rs::detached_no_inherit` + `deelevate.rs::create_with_token` (the breakaway flag + fallback). Units: `detached_no_inherit_falls_back_under_a_breakaway_denying_job` (no-regression — the shared runner exercises the fallback directly), `breakaway_spawn_escapes_a_kill_on_close_job` (the OS escape mechanism — self-SKIPS where the ancestor job forbids breakaway). Unix: the daemon's `setsid` session-detach already keeps a closing terminal's SIGHUP off its children (guard test, no code).\r\n- **Source:** v0.12.0 real-harness defect (operator) → v0.12.1 L1 @5ae68f8; doyle design+int ruling 2026-06-18.\r\n\r\n### 7.11 A dead PTY child + a dropped operator pump must NOT wedge the broker for other clients  `[REQ-HAZARD-ATTACH-WEDGE]`\r\n- **Failure (hypothesized, v0.12.0):** a legitimately dead PTY child (crash/kill) plus an `rc` pump dropped without a clean detach (closed tab) was thought to make the broker's loopback forward `write_all` block forever on a full 64 KB duplex, park a worker in the 2-worker net runtime, saturate both, and stall every new attach / `endpoint run` (with `daemon stop` unable to join).\r\n- **Invariant — DISPOSITION = PROVE-DON'T-CHANGE** (doyle GATE-PASS @e883f45, 2026-06-18): the post-L0 code ALREADY prevents the wedge; NO fail-fast / worker-count code was added. (1) `serve_attach` forwards fire-and-forget (`net_stream_send` `op_id=None`) and the broker-side `send_stream` is already deadline-bounded (`bounded_block_on`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s — hazard 7.8), not forever. (2) the loopback duplex is drained broker-INTERNALLY by the operator row's OWN read pump (`nethost.rs` `RecvHalf::Loopback`), which for an ordinary attach stream (`retentive_cap == 0`) NEVER parks → `peer_w` never backs up on a dead `rc` (a dead `rc` is just a dropped IPC subscriber against a bounded, EVICTING ring). (3) `bounded_block_on` = `runtime.handle().bloc…\n- **spt-core mapping:** int `crates/spt/tests/attach_wedge_e2e.rs` (REAL detached daemon + dummy-harness fixture): serve the victim (rc sees its tick), abruptly kill rc (dropped pump) + kill the PTY child → a NEW endpoint still comes online + is served (no wedge), the dead endpoint is offlined within a tick (`LIVENESS_RECONCILE_OFFLINE`), `daemon stop` bounded. Leans on 7.8 (the QUIC-op deadline) + 7.7 (viewer isolation).\r\n- **Source:** v0.12.0 real-harness finding (operator) → v0.12.1 L2 @e883f45; doyle GATE-PASS 2026-06-18.\r\n\r\n### 7.12 Controller output must NOT be written inline on the drain thread — a backed-up controller wedges the session  `[REQ-HAZARD-INJECT-CONTROL-COEXIST]`\r\n- **Failure (v0.12.x, operator dogfooding + doyle /diagnose 2026-06-19):** `OutputLog::append` fanned each live chunk to the CONTROLLER via a SYNCHRONOUS, blocking `write_frame` held INLINE on the session's single drain thread while `Mutex<OutputLog>` was locked (viewers already had a dedicated writer thread + bounded evicting channel). A backed-up controller socket — a slow operator, or the full 64 KB `rc` loopback duplex under heavy TUI redraw — parked the drain thread WITH THE LOG LOCK HELD: output + keystroke-echo stalled and every attach / resize / `KIND_SESSIONS` that needs the log lock blocked FOREVER, while the broker process stayed alive. Fires on NORMAL interactive `rc` use under heavy output (not only message injection); REOPENED the wedge facet o…\n- **Invariant:** controller delivery runs OFF the drain thread — a dedicated controller writer thread + bounded channel (the 7.7 viewer_writer pattern) does the blocking socket write; the drain hands each chunk off with a BOUNDED, OFF-LOCK send (deadline → detach + `clear_controller`, never park forever, never silently evict a LIVE operator's authoritative view). The controller is AUTHORITATIVE (unlike a viewer): its writer advances the `delivered_through` resume cursor (atomic, only-on-success, monotonic). `clear_controller` is the ONE unlatch path (clears `driven_by`, drops the sink) — W5's reconcile self-heal reuses it.\r\n- **spt-core mapping:** impl `broker.rs` `OutputLog::append`/`controller_writer`/`become_controller`/`ControllerJob::deliver`/`clear_controller`; int `crates/spt-daemon/tests/inject_control_wedge.rs` (a fully-backed-up controller keeps `KIND_SESSIONS` answering = no wedge); unit the bounded-deliver + monotonic-cursor kernels. The input-side parks (write_input `write_all` on a full buffer; DSR-answer writer-mutex contention) are BENIGN on Windows ConPTY (absorbs a large inject), real only on Unix forkpty → bounded by W2's atomic-write substrate.\r\n- **Source:** v0.12.x operator wedge → v0.13.0 W1 (doyle GATE-PASS 2026-06-19).\r\n\r\n### 7.13 `spt rc` must SWAP Windows legacy-console Backspace/Ctrl+Backspace (`^H`↔DEL); do NOT enable VT console input  `[REQ-HAZARD-RC-INPUT-KEY-ENCODING]`\r\n- **Failure (operator dogfooding):** `spt rc` is a raw verbatim stdin byte pump; the legacy Windows console delivers Backspace as `^H` (0x08), and Claude Code maps `^H` → backward-kill-word, so every Backspace deletes a whole word. Confirmed bytes (operator HITL capture, real Windows Terminal): Backspace=0x08, Ctrl+Backspace=0x7f.\r\n- **REJECTED fix (dc07c39, reverted):** enabling `ENABLE_VIRTUAL_TERMINAL_INPUT` on the stdin console BACKFIRED — on Windows Terminal that flag yields **win32-input-mode**, not legacy xterm VT. Every key then arrives as a 6-field `ESC[Vk;Sc;Uc;Kd;Cs;Rc_` key-down/up record: (1) the ctrl-b detach broke (0x02 is wrapped, so `parse_stdin_chunk` never sees the raw `DETACH_PREFIX`), and (2) win32-input-mode was forwarded into Claude Code's own ConPTY input negotiation (garbage). The VT lever is wrong for Windows Terminal. (Lesson: verify a terminal-input fix on the REAL terminal via the operator re-capture — unit/clippy never exercise the live key path.)\r\n- **Invariant:** stay a raw byte pump and NARROW-normalize in the forward path — **SWAP** the two delete bytes: `0x08` → `0x7f` (VT DEL, char-delete) AND `0x7f` → `0x08` (^H, kill-word), AFTER the detach state machine consumes any `0x02`, leaving `0x02` and every other byte untouched. Because CC reads `^H`=word-delete and DEL=char-delete, swapping the *input* bytes gives Backspace=char-delete and Ctrl+Backspace=word-delete — the native Win11 convention (the operator rejected losing word-delete; the earlier one-way `0x08`→`0x7f` map dropped it). cfg(windows) only (Unix is already VT; a real `ctrl+h`/`ctrl+?` there must pass through). Accepted minor loss: a real `ctrl+h` (0x08) and a real DEL (0x7f) are swapped on Windows — both are delete keys, so the practic…\n- **spt-core mapping:** the Backspace/Ctrl+Backspace SWAP intent persists; the `normalize_key_byte` byte-swap was SUPERSEDED on Windows by **7.16** (`REQ-RC-KEY-VT-TRANSLATE`, v0.13.0 bug 2) — the agnostic key-event→xterm-VT `translate_key_event` now emits Backspace → `0x7f` and Ctrl+Backspace → `0x08` natively (carrying the relocated `[impl]`/`[unit]` evidence), so `normalize_key_byte` + its unit were removed. The live keypress is HITL operator re-capture.\r\n- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19; superseded-on-Windows by the bug-2 translator 2026-06-19).\r\n\r\n### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`\r\n- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two facets, one root: **(A) stutter** — measured fsync on `%LOCALAPPDATA%\\spt-core` median 6.5 ms / spikes 198 ms, ×2 per keystroke → choppy, worsens with volume; **(B) HARD WEDGE** — a blocking `PtyWrite` (ConPTY/forkpty input buffer not draining) held the lock indefi…\n- **Invariant:** `apply_once` RELEASES the inner lock across `effect()` — reserve the key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable, mark applied). Crash-idempotency comes from the per-key reservation + the applied-set, not from holding the lock across the effect (the brain-crash guarantee is unchanged: the broker survives the brain, so reserve→effect→finalize runs uninterrupted on the broker thread; a replay hits the applied-set and dedups). `EffectKind::PtyWrite` is **EPHEMERAL** — NO journal lines, NO fsync, in-memory dedup only (a keystroke lost to a broker crash is retyped; PTY state is never rebuilt from keystroke replay) — while durable kinds (spool/r…\n- **spt-core mapping:** impl `effect.rs` `apply_once` (reserve/release/finalize) + `EffectKind::is_durable`; unit (`effect.rs`) a barrier proving the lock is NOT held across `effect()` (a different key's `apply_once` completes while one effect blocks) + `PtyWrite` writes no journal line while a durable kind (`NetSend`) does (the no-fsync proxy); int `crates/spt-daemon/tests/inject_control_wedge.rs` (real broker+PTY, a stalled consumer + sustained blocking input effect: a concurrent `spt rc` attach stays serviceable AND actually receives PTY bytes — the assertion W1's gate lacked). Unix park-(b) folded here, gated on gravity-linux. doyle GATE-GUARD: assert structurally, NEVER on fsync wall-clock (env-dependent, flaky); the stutter is fixed as a consequence of…\n- **Source:** v0.13.0 W1b (operator dogfooding post-W1 escape; doyle /diagnose + measurement 2026-06-19).\r\n\r\n### 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]`\r\n- **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 already clears the `status=online` latch here but left `driven_by` untouched.\r\n- **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 disconnect already self-heals via `detach_if`→`clear_controller`, W1 (7.12) bounds the active-output wedge, and the residual idle-wedged-REMOTE case is the deferred `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`, which needs BROKER-SIDE liveness eviction — a brain reconcile can …\n- **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).\r\n- **spt-core mapping:** impl `livehost.rs` `reconcile_hosted_liveness` Gap-B clear + the additive `SessionInfo.controller_by` observability field (`msg.rs`, populated in `broker.rs` `KIND_SESSIONS`); unit `livehost.rs` `pull_liveness…` extended (offlined sessionless perch clears `driven_by`; live/relay perches untouched); int `crates/spt-daemon/tests/driven_by_selfheal.rs` `gap_b` (real broker: reconcile offlines AND clears `driven_by`) + the A1/A2 characterization (`inject_control_wedge.rs` w5_a1/w5_a2).\r\n- **Cross-ref 7.29 (who MAY clear vs who MUST NOT):** the BRAIN reconcile here MUST NOT clear `driven_by` off `controller_by==None` (ambiguous with a live LOCAL controller). The BROKER stamp-convergence (7.29) MAY — it reads `has_controller()` DEFINITIVELY (its own controller slot), so it authoritatively clears a stale `driven_by` against a live session with no controller (the `has_controller()==false` leg). The invariant's owner for that leg moved to the broker; this brain-side ambiguity-safety is unchanged. The wedged-open-remote A2 residual (`has_controller()==true`) is NOT covered by either — still `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.\r\n- **Source:** v0.13.0 W5 (repro-first, todlando; doyle-assigned 2026-06-19). A2 (idle wedged-remote leg) deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.\r\n\r\n<!-- [doc->REQ-RC-KEY-VT-TRANSLATE] -->\r\n### 7.16 `spt rc` translates Windows console KEY EVENTS to standard xterm VT (arrows/Home/End/F-keys reach the harness); supersedes the W7 byte-swap  `[REQ-RC-KEY-VT-TRANSLATE]`\r\n- **Failure (operator dogfooding):** `spt rc` read raw stdin BYTES, but the Windows legacy console (no `ENABLE_VIRTUAL_TERMINAL_INPUT`) delivers arrows / Home / End / PgUp / PgDn / Insert / Delete / F-keys as console KEY_EVENTs, NOT stdin bytes — so the byte-pump saw nothing and those keys were DEAD (only byte-emitting keys like Backspace worked, via the 7.13 swap).\r\n- **Invariant:** on Windows, read crossterm KEY EVENTS (the picker already does) and translate each to STANDARD xterm VT via the pure `translate_key_event` (copy a known-correct xterm table verbatim: arrows `ESC[A..D`, Home `ESC[H` / End `ESC[F`, `~` keys `ESC[<n>~`, modified `ESC[1;<m><final>` / `ESC[<n>;<m>~` with `m = 1 + Shift + 2·Alt + 4·Ctrl`, F1–F4 `ESC OP..S` / F5–F12 `ESC[<n>~`), forwarded through the SAME rc pump — the harness receives ordinary xterm VT (AGNOSTIC; NOT win32-input-mode, the 7.13 rejected lever). Press-only (drop Repeat/Release). Detach stays the `ctrl-b d` PREFIX, event-sourced (Ctrl+B arms; armed + plain `d` ⇒ Detach; armed + Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` + translated). NON-tty stdin (piped / tests) falls back to …\n- **spt-core mapping:** impl `rc.rs` `translate_key_event` + `csi_final`/`csi_tilde`/`f_key` + `key_event_step` (event detach SM) + `spawn_stdin_reader` cfg-split (`spawn_stdin_reader_events` on a tty / `spawn_stdin_reader_bytes` non-tty + Unix); unit the EXHAUSTIVE `translate_key_event` mapping + the event-detach SM (the Backspace/Ctrl+Backspace arm carries the relocated `REQ-HAZARD-RC-INPUT-KEY-ENCODING` evidence); NO int (live console = HITL, operator re-capture — REQ-RUN-PICKER/RC-1 precedent).\r\n- **Source:** v0.13.0 bug 2 (operator ruling: proper agnostic translator, ship-blocker; doyle design + Option-B detach ruling 2026-06-19).\r\n\r\n<!-- [doc->REQ-RC-WIN-PASTE] -->\r\n### 7.18 `spt rc` paste is client-originated on Windows — read the LOCAL clipboard, inject a BRACKETED paste  `[REQ-RC-WIN-PASTE]`\r\n- **Failure (operator dogfooding):** in an `spt rc` session neither ctrl+V nor right-click pasted (CC explicitly supports ctrl+V). `RawGuard` did 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 translated to a 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. A multi-line paste-as-keys also became a `\\r` submit-storm.\r\n- **Invariant:** on Windows (cfg-split; folds into the 7.16 event path), on a RIGHT-CLICK rc reads the LOCAL clipboard itself and forwards a BRACKETED paste — `wrap_bracketed_paste` = `ESC[200~` + content + `ESC[201~`. CC has bracketed-paste mode on (its TUI sets `ESC[?2004h`), so it treats the synthesized markers as a PASTE: content lands intact, NO submit-storm, harness-AGNOSTIC (standard xterm contract). `RawGuard` also `EnableMouseCapture` (disables console QuickEdit + enables `ENABLE_MOUSE_INPUT` so a right-click surfaces as `Event::Mouse`) on an interactive console only, restored on drop → right-button-down → `read_clipboard` → bracketed paste. `read_clipboard` = the `clipboard-win` crate; empty/failed read ⇒ a clean no-op (never inject garbage, never …\n- **spt-core mapping:** impl `rc.rs` `wrap_bracketed_paste` + `mouse_is_paste` + `clipboard_paste` + `read_clipboard` + `windows_mouse_wanted` + `RawGuard` (mouse capture/restore) + the `spawn_stdin_reader_events` right-mouse arm; unit the exact bracketed framing + content-verbatim + mouse classify (right-down ⇒ paste, all else ⇒ drop) + the injected-reader paste decision (non-empty ⇒ wrapped, empty/fail ⇒ no-op); NO int (live clipboard + console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent).\r\n- **Source:** v0.13.0 P1 (operator HITL; doyle design 2026-06-19). Depends on P0 (7.17). AMENDED v0.13.0 P1b: scope narrowed to right-click-only (the dead ctrl+V interception removed); scroll-forward is 7.20; the input-flood non-wedge is 7.19.\r\n\r\n<!-- [doc->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] -->\r\n### 7.19 An operator input FLOOD must not deadlock the broker via the applied-ack on the same conn  `[REQ-HAZARD-INPUT-ACK-BACKPRESSURE]`\r\n- **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 → the broker→brain return direction fills (~10 frames = the IPC pipe buffer) → `send_frame` BLOCKS → the handler stops reading → the brain's writes block → mutual full-duplex DEADLOCK. (Capture: 11 frames, `write_input` 11/11 — P0 holds; `ack send` START=11/END=10…\n- **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=true`). EXACTLY-ONCE preserved: the broker dedups by `(session, op_id)` at the applied-set regardless of the ack. **N-1 caveat:** an OLD resident broker (the self-update window) ignores `ack=false` → still acks → the deadlock persists until a broker restart (inher…\n- **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 session stays live + a concurrent attach opens (real broker+brain, no mocks).\r\n- **Source:** v0.13.0 P1b (operator HITL re-open; doyle /diagnose on the enhanced rc+broker capture 2026-06-19). The real ship-blocker behind the ctrl+V wedge.\r\n\r\n<!-- [doc->REQ-RC-MOUSE-FORWARD] -->\r\n### 7.20 `spt rc` must forward the scroll wheel to the harness (our mouse capture steals WT's native scroll)  `[REQ-RC-MOUSE-FORWARD]`\r\n- **Failure (operator HITL):** scroll stopped working in `spt rc`. P1's `EnableMouseCapture` (for right-click paste, 7.18) makes Windows Terminal forward ALL mouse — including the wheel — to rc instead of scrolling its own buffer, but the rc mouse handler dropped everything except right-button-down → scroll DIED (and WT's native scrollback is stolen under the capture).\r\n- **Invariant:** on Windows, TRACK the harness's mouse-reporting mode from its OUTPUT — scan for DECSET `ESC[?1000h/1002h/1003h` (mouse on) + `ESC[?1006h` (SGR ext) and their `…l` (off) into a shared `MouseMode{enabled,sgr}` (the pump writes from the output render path, the stdin reader reads); the scan survives a sequence SPLIT across output chunks (a bounded carry buffer). The mouse handler: right-button-DOWN → bracketed clipboard paste (7.18, unchanged); `ScrollUp/Down` → an xterm SGR mouse report (`ESC[<64;col+1;row+1M` up / `ESC[<65;…M` down; 0-based crossterm → 1-based xterm), forwarded ONLY when `enabled && sgr` (else DROP — a legacy report the harness may misread is garbage); Moved/drag/left/middle DROP (scroll is the need; click-forward risks garbag…\n- **spt-core mapping:** impl `rc.rs` `MouseMode` + `MouseModeScanner`/`parse_decset_private`/`apply_mouse_mode` (carry-buffer scan, fed from the pump's output path) + `scroll_dir` + `scroll_sgr` + the `spawn_stdin_reader_events` scroll arm (threaded via `Arc<MouseMode>`); unit `scroll_dir` classify + `scroll_sgr` exact bytes + the DECSET scan (set/reset, combined, mixed, and split-across-chunks); NO int (live console mouse = HITL).\r\n- **Source:** v0.13.0 P1b (operator HITL; doyle design 2026-06-19). Bundled with 7.19.\r\n\r\n<!-- [doc->REQ-HAZARD-CONTROLLER-WRITER-REORDER] -->\r\n### 7.21 Exactly ONE `controller_writer` per brain↔broker connection — a superseded writer must write nothing further  `[REQ-HAZARD-CONTROLLER-WRITER-REORDER]`\r\n- **Failure (doyle instrumented RACEDIAG repro, kitsubito):** on a brain-restart re-serve the handoff brain registered as controller on the SAME session TWICE over the SAME socket — `Brain::handoff` eagerly `subscribe(prior.next_seq=1)` → `become_controller(from_seq=1)`, spawning writer-A (writes seq 1); then `serve_attach` re-handled the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, spawning writer-B (writes 0,1). `become_controller` dropped the prior `ControllerSink` (its `tx`) but did NOT stop the prior writer — writer-A kept flushing its OWNED `initial` batch, and both writers held clones of one `SharedSend` (`Arc<Mutex<socket>>`) with no inter-thread ordering. When writer-A's seq 1 beat writer-B's seq 0, the stri…\n- **Invariant:** on a single brain↔broker connection exactly ONE `controller_writer` is ever the LIVE writer; a SUPERSEDED writer writes no further frames after the epoch bump it observes; and **every `controller_writer` emits a strictly ASCENDING seq stream** (sorted initial batch + ascending live frames). The CORRECTNESS guarantee that falls out: a snap-above consumer over any interleaving of ascending writers — where the surviving writer (`serve_attach`'s `attach_as(sid,0)`) offers the COMPLETE range `[0, end]` — delivers `[K, end]` with **no skip and no dup** (the first sighting of any seq `>M` is always preceded by a sighting of `M` on that same ascending writer, so `M` is delivered before the cursor can pass it). Enforced/relied-on three ways (NB: fix …\n- **spt-core mapping:** impl `brain.rs` `Brain::handoff` (KEEP the eager `subscribe`; seed `session_cursors`) + `Brain::subscribe_with` (resume-mode dedup-cursor reset to `from_seq`, shared by `attach`/`attach_as` — the operator-stream boundary fix) + `broker.rs` `OutputLog.controller_epoch: Arc<AtomicU64>` / `become_controller` (atomic `fetch_add`, passes the new epoch + `Arc::clone` into the writer) / `controller_writer` (epoch gate read UNDER the lock on both loops) / `mark_controller_gone` + the `ControllerJob` epoch read; unit (white-box, `src/broker.rs`) the epoch-gated writer (a superseded writer flushes nothing — only the latest writer's monotonic stream reaches the wire) + `handoff` seeds `session_cursors`/resubscribes; int (keystone, `tests/broker.…\n- **Source:** v0.13.0 P1c (operator-ruled root-fix before ship; doyle root-cause via instrumented repro 2026-06-20; design corrected across two gate rounds — fix #1 reverted, then the kitsubito RACEDIAG pinned the residual to the consumer-side operator-stream boundary, fixed by the `subscribe_with` cursor reset on `attach_as` re-subscribe). The last v0.13.0 ship-blocker.\r\n\r\n### 7.22 An idle delivery with no working translation binary must SPOOL, never raw-inject a pseudo-delivery reported as delivered  `[REQ-HAZARD-IDLE-SILENT-NONDELIVERY]`\r\n- **Failure (F-019 post-mortem):** the v0.11.0 path raw-injected `payload+\\r` into the spt-hosted PTY whenever no working translation binary handled an inbound message — none declared, spawn-failed, FAULTED (commit-deadline miss / binary death), or its inject-worker channel dropped — AND acked `delivered=true`. But a bare `payload+\\r` does NOT submit on a modern TUI (Claude Code): the message was TYPED into the harness but never sent, a silent pseudo-delivery reported as success. That silent degrade-to-raw-inject is precisely what MASKED F-019 (`[REQ-INSTALL-11]`, above) through a multi-hour black-box hunt — every behavioral hypothesis was moot because the \"delivery\" never delivered.\r\n- **Invariant:** spt-hosted idle delivery is translation-binary-ONLY (ADR-0022 amendment). `dispatch_endpoint_input` with no working binary replies `endpoint_injected_envelope(ep, delivered=false)` and writes NOTHING to the PTY; the caller (`try_broker_inject` → `cmd_send`) reads `delivered=false` and falls through to `deliver::send` = SPOOL (poll-fed, never lost), reporting an honest `QUEUED`, never a confident-but-false `SENT`. The failure is LOUD (`ENDPOINT_INJECT:<ep>: no working translation binary … -> SPOOLED, not injected`). The raw-inject fallback (`input.enqueue`) is removed from the no-binary, worker-dropped, AND post-fault paths; the operator-keystroke floor-flush on FAULT is UNCHANGED (operator input is never stranded). Out of scope (follow-up): …\n- **Boundary — what this does NOT cover (the STATE-vs-transient precision):** the guarantee is the steady FAILED STATE — once a binary IS faulted/absent/worker-gone, subsequent deliveries spool (`faulted` is MONOTONIC: set once, never respawns, so the state is reached deterministically). It does NOT cover the FAULT-TRANSIENT: a delivery that lands in the worker's commit window — BEFORE `event_rx` is dropped / `faulted` is set — can be optimistically enqueue-acked (`delivered=true` the instant `event_tx.send` succeeds) then DROPPED when the worker faults+returns. This is a SEPARATE, PRE-EXISTING hazard (raw-inject removal did not touch it — the old code dropped that queued event too; v0.14.3 makes nothing worse) and is tracked for **v0.15.0 under `[REQ-MSG-DE…\n- **spt-core mapping:** impl `broker.rs` `dispatch_endpoint_input` (no-working-binary → `delivered=false` + loud log, no `input.enqueue`; the `input` writer is no longer resolved from the sessions table) + `build_translation`/`fault_translation` (`None`/fault now MEAN spool); unit `msg.rs` `endpoint_injected_envelope_carries_delivered_both_ways` (the `delivered=false` spool signal round-trips); int (real broker+PTY) `tests/broker.rs` `endpoint_keyed_inject_without_binary_spools_not_pty` + `tests/inject_control_wedge.rs` `large_endpoint_inject_to_a_no_binary_session_spools_promptly_without_wedging` + `g2_no_commit_deadline_faults_binary_and_does_not_wedge_controller_input` (post-FAULT inbound EVENTUALLY spools via bounded-retry-until-spool, marker NEVER on PT…\n- **Source:** v0.14.3 (ADR-0022 amendment @1eaeef1, operator-ruled; doyle-scoped, the F-019 follow-up). CHANGE-5 (`cli.rs` honest report) verified a NO-OP — the spool fall-through already reported `QUEUED`, never `SENT`.\r\n<!-- [doc->REQ-HAZARD-IDLE-SILENT-NONDELIVERY] -->\r\n\r\n---\r\n\r\n### 7.23 A message that has REACHED a node's spool must NEVER depend on an adapter hook-poll cadence to reach an spt-hosted (relay-less) endpoint — the daemon drives delivery on the events it owns  `[REQ-HAZARD-DELIVERY-STARVATION]`\r\n- **Failure (F-023, RCA @ `f023-f024-wan-idle-starvation`):** a message that has already landed in a node's spool — WAN-arrived, or locally spooled-while-active — stranded INDEFINITELY on an spt-hosted, relay-LESS endpoint whenever the adapter's hooks went quiet (an idle session with no user turns). An spt-hosted perch has no `api listen` relay to wake it, and NOTHING in the daemon re-offered a spooled message once the arrival moment passed — delivery silently depended on the adapter happening to call `api poll`. Presented as \"sent but never lands\" with a HEALTHY translation binary and a visibly-idle perch (F-023: verified 7 min idle, binary alive, zero injection). TWO gaps: (1) the WAN-ingress path (`receive_wan`) tried the harness-hosted TCP relay then fel…\n- **Invariant:** the daemon DRIVES delivery of an already-spooled message on the events IT owns — never on hook cadence — for a relay-less spt-hosted endpoint. TWO daemon triggers feed ONE shared spt-hosted inject leg (`spt_daemon::inject::try_spt_hosted_inject`, translation-binary-ONLY): (1) **WAN ingress** — `receive_wan` injects via the live binary BEFORE the spool fallback (`[REQ-WAN-SPT-HOSTED-DELIVERY]`, leg 1); (2) **the ACTIVE→IDLE edge** — `drain_idle_window` (fired by `api state idle`) claims the pending spool (deferred AND non-deferred) and injects each via the same leg, reusing the hook-poll take/ack so a concurrent `api poll` cannot double-deliver (`[REQ-MSG-IDLE-EDGE-DRAIN]`, leg 2). The v0.14.3 LAW holds on BOTH triggers (7.22): no working bin…\n- **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 delivery with NO hook poll.\r\n- **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).\r\n<!-- [doc->REQ-HAZARD-DELIVERY-STARVATION] -->\r\n\r\n---\r\n\r\n### 7.24 A delegated live adapter apply must NEVER report success without swapping, and the live-update seam must use ONE parent-aware adapter matcher  `[REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP]`\r","truncated":true,"truncatedBy":"lines","totalLines":408,"totalBytes":71115,"outputLines":195,"outputBytes":71115,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"## 6. Documented regressions (non-obvious invariants)\n…\n### 6.7 Broker and brain MUST be separate processes (in-process collapse silently breaks no-endpoint-drop update) `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n- **Failure:** the daemon hosts the broker as a background *thread* in the single `spt daemon` process (`daemon.rs:165-170`, `Arc<Broker>` + `thread::spawn(serve)`) instead of a separate process. A brain restart onto a swapped binary then cannot happen without killing the broker thread — closing every PTY, orphaning every harness child, dropping every socket. So `spt update apply` degrades to an in-process `Brain::handoff` no-op: the binary swaps on disk but the running daemon keeps executing the old code until an unrelated restart/logon. The no-endpoint-drop self-update pillar (REQ-UPD-3, ADR-0004) is silently unrealized. Observed live 2026-06-09: `enlyzeam` ran 0.3.0 with 0.3.2 on disk for ~a day, still reproducing the bug the update fixed.\n…\n- **Origin:** unintended spec/impl drift from ADR-0004 (the broker *process* was specced + spiked but built in-process), discovered during the v0.3.2 fleet update verify. Full audit + decisions: `docs/BROKER-BRAIN-SPLIT-RESTORATION.md` (verified) + ADR-0018.\r\n<!-- [doc->REQ-HAZARD-BROKER-PROCESS-ISOLATION] -->\r\n- **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.\r\n- **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 `apply` rename to `.old-N` and ran OLD bytes under an `applied` record (`[REQ-HAZARD-BRAIN-RESPAWN-PATH]`, 6.11), fixed in v0.4.2 (respawn from the canonical path captured at broker start + a promotion bytes-gate). Seamless update is now proven on **both OSes** — Win…\n\r\n### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`\r\n- **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).\r\n- **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).\r\n- **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.\r\n- **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.\r\n- **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 names present; an unknown extra field still deserializes), so a *non-additive* pre-ready change (renamed/removed field, or a `deny_unknown_fields`/non-tolerant shape) trips the test and forces the migration **behind ready-promotion** (or into an additive form). *…\n- **Origin:** verification amendment `[V1]` (agent `doyle`) on `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.\r\n- **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't-pre-fail); the tripwire is the wire it trips the day one is needed.\r\n<!-- [doc->REQ-HAZARD-ROLLBACK-STATE-COMPAT] -->\r\n\r\n### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output\r\n- **Failure:** a resume-mode brain (per-session `session_cursors` populated by `resume_sessions`) drives `Brain` over **blocking** `read_event` calls. Any command that loops `read_event` until its own reply — `spawn_session_pid` waiting for `Spawned`, `net_status`, `sessions`, etc. — calls `read_event` on *every* interleaved frame, so an OUTPUT frame for a **different** session is **cursor-processed** (its `session_cursors` entry snaps forward; the broker already counted it delivered via `delivered_through` on the live-send) and then **discarded** by the waiting loop's `_ => continue`. Cursor advanced + content dropped = that chunk is gone for the downstream consumer and the broker will **not** re-send it (resume reads from the delivered cursor, ADR-0018 D4)…\n- **Invariant:** the daemon-hosted multi-session event loop must not consume a session's OUTPUT inside another session's blocking wait. The **live-agent adapter milestone must restructure the brain event pump** so command/`spawn` is non-blocking (a single demux loop owns `read_event` and routes every frame to its session's consumer), OR a blocking wait must re-queue/route the frames it reads for other sessions rather than dropping them.\r\n- **spt-core mapping:** **unreachable today** — the supervised daemon brain hosts no PTY sessions and spawns none; a single-session seat (legacy, empty map) has no \"other session\" to starve. Surfaces the moment daemon-hosted sessions land (the live-agent adapter), which must rebuild the blocking `read_event` loop regardless (N interactive sessions cannot share one blocking reader). Recorded so that redesign inherits the constraint rather than rediscovering it. No machinery now (would be untested dead code, activate-don't-pre-fail).\r\n- **Origin:** surfaced by the D4-2b resume-harness CI flake root-cause (agents `todlando` + `doyle`, 2026-06-10); sibling of `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]` 6.7.\r\n\r\n### 6.10 Phase-significant loop timing must be a durable absolute-deadline grid, not phase-relative sleep `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\r\n- **Failure:** a periodic loop that sleeps a flat `period` each iteration (`pulse_tick` then `sleep(pulse_period)`) is **phase-relative** — every brain restart silently re-phases the grid to the restart instant. Under the seamless-update model (the supervisor respawns the brain onto a swapped binary, ADR-0018 D3-3), a routine update would shift the cadence of every phase-significant loop, and continuity cannot ride a brain→brain frame (the outgoing brain is gone before the new one starts — the same constraint that moved session continuity to the broker in D4).\r\n- **Invariant:** phase-significant periodic timing lives as durable absolute-deadline state on disk (`(anchor, interval)`), rehydrated on every brain start, with fires **derived functionally** (`next_fire = anchor + interval·⌈max(0,now−anchor)/interval⌉`) and **no per-fire write**. An **Update** restart re-reads the anchor and keeps deriving (phase preserved, lands mid-grid); a **Crash**/**Cold** restart re-bases the anchor to `now` (phase reset acceptable — the loop is idempotent catch-up). The update-vs-crash decision is the D3 spawn-time `StartReason`. **One-shot** (alarm) deadlines persist their absolute `target-time` at creation and **never reset** on any restart (\"remind me at 3pm\" is a commitment) — the asymmetry vs the periodic crash-reset is the rul…\n- **spt-core mapping:** ADR-0018 Q4/V3/V4, restoration D5. Mechanism in `spt-daemon::deadline` (`DeadlineAnchor` periodic + `OneShotDeadline` rule-only pure helper); the pulse loop (`lifecycle::run_pulse_loop`) consumes it. The one-shot **machinery** (a durable in-daemon alarm scheduler) is the deferred alarm port (`docs/DEFERRED.md`) — the daemon has no one-shot consumer today, so building the timer now would ship untested dead code (activate-don't-pre-fail); D5 fixes the *rule* as a tested-unwired helper, the port builds the *scheduler*.\r\n- **Origin:** ADR-0018 Q4 + verification amendments `[V3]`/`[V4]` (agent `doyle`); D5 plan vet (agents `todlando` + `doyle`, 2026-06-10).\r\n\r\n### 6.11 Brain respawn must exec the APPLIED bytes, not the renamed old binary (Linux `current_exe` follows the apply-rename; readiness ≠ new-bytes) `[REQ-HAZARD-BRAIN-RESPAWN-PATH]`\r\n- **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**, and the daemon records `applied:N` while still running the previous version. New code does not run; the record is optimistically wrong (the enlyzeam-class record/reality divergence, now provable via `exe_hash`). **Windows** dodged it — `GetModuleFileName` returns…\n- **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 the new bytes run). If either hash is absent the gate degrades to readiness-only (N-1-safe for pre-metadata releases / a missing breadcrumb) but emits `PROMOTE_BYTES_UNVERIFIED` so a disarmed gate stays field-diagnosable.\r\n- **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.\r\n- **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.\r\n<!-- [doc->REQ-HAZARD-BRAIN-RESPAWN-PATH] -->\r\n\r\n---\r\n\r\n## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)\r\n\r\nThese were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.\r\n\r\n### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`\r\n- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).\r\n- **Invariant:** every `api` *mutation* is authenticated to an endpoint/session — per-endpoint link token or OS-credential binding. An unrelated local process cannot bind, inject, end, or spoof state.\r\n- **spt-core mapping:** the `api` subcommand surface (PRD R-API-*) + broker IPC.\r\n- **Source:** codex Stage A #13 (`docs/reviews/STAGE-A-codex-redteam.md`).\r\n\r\n### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`\r\n- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.\r\n- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.\r\n- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.\r\n- **Source:** codex Stage A #14.\r\n\r\n### 7.3 Psyche outbound capture + sanitization  `[REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]`\r\n- **Failure:** the Psyche's sole outbound channel is its **stdout** (`<EVENT type=\"reply|notify\">` intents — ADR-0012). Two ways to break it: (a) a **null-stdout / detached** live-Psyche driver silently **discards every reply and notify**; (b) the daemon relays a Psyche-supplied `from=`/target **unchanged**, letting a sandboxed Psyche **spoof identity** or address arbitrary endpoints.\r\n- **Invariant:** the live-Psyche turn driver **MUST capture stdout** (a bounded, stdin-fed, stdout-captured invocation — **never** `Stdio::null()`); the daemon **MUST strip** every Psyche-supplied `from=`/target/routing attribute, **re-stamp `from=<self_id>`**, and **constrain routing** — `reply` → the inbound message's structural sender (its `from`) only, `notify` → the agent's own user/subnet only. Body validated per 4.1.\r\n- **spt-core mapping:** the live-Psyche turn driver + daemon outbound relay (ADR-0012); `spt-proto::event` type taxonomy (`+reply`/`+notify`). The interim `runtime::spawn_session` `Stdio::null()` path is **not** the live-Psyche driver.\r\n- **Source:** grill-with-docs 2026-06-03 (uncovered by prior design passes); sister `src/live/wrapper/claude.rs` (sandbox `[\"Read\",\"Write\",\"Edit\"]` + `parse_markers`).\r\n\r\n### 7.4 Per-agent pulse/psyche/echo scheduling must not serialize across agents  `[REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]`\r\n- **Failure:** echo-commune (`run_bounded_stdin`) and the live-Psyche turn driver (D7.5) are **bounded LLM calls that block their calling thread** until the child answers or the timeout fires. Today each agent drives its own pulse from its **own process** (`spt/src/api/{live,startup}.rs`), so blocking is isolated. When the daemon hosts **N per-agent loops** (ADR-0004 target; the `run_pulse_loop` fan-out is currently def+test only), a **single serial driver** that calls these invocations inline lets one agent's slow/hung LLM call **stall every other agent's heartbeat, commune, and reply** — the per-call timeout bounds the worst case *only* when the work is isolated; serial makes the stalls additive.\r\n- **Invariant:** each agent's bounded LLM-bearing work (echo-commune summarizer, Psyche turn) runs on its **own thread / off the shared scheduler** — no single-threaded driver iterating all agents may call a blocking invocation inline. One agent's slow/timed-out call must not delay another agent's next tick beyond tolerance.\r\n- **spt-core mapping:** the daemon's multi-agent pulse/psyche hosting (ADR-0004, \"all Psyche/pulse loops\" consolidated); `run_pulse_loop` fan-out; echo-commune + the D7.5 Psyche driver.\r\n- **Source:** grill-with-docs 2026-06-03 (forward invariant — the multi-agent fan-out is not yet wired). Distinct from ADR-0002 SERIOUS #6 (crash blast-radius, not scheduling latency).\r\n\r\n### 7.5 WAN-inbound origin is transport truth, never payload  `[REQ-HAZARD-WAN-ORIGIN-AUTH]`\r\n<!-- [doc->REQ-HAZARD-WAN-ORIGIN-AUTH] -->\r\n- **Failure:** the ADR-0009 access whitelist gates **unsolicited wire inbound by origin node**. If the gate's subject is read from record bytes (an `origin_node`/`from`/`node` field a sender wrote), any sender forges any origin and the whitelist is decoration — same spoof class as 7.3's Psyche-supplied `from=`, now on the cross-node surface.\r\n- **Invariant:** the origin the gate (and detection/UX — \"node X is driving\") consumes is the **QUIC handshake-proven remote node id** (iroh `EndpointId` == Ed25519 node pubkey, the REQ-NET-1 identity binding) read from the **broker's conn/stream table** (`NetStreamInfo::remote_id_hex`), never from payload. Wire records carry **no origin field by design**; a forged one decodes as an ignored unknown field and influences nothing. `from` inside a record is reply-routing metadata only — never an authorization subject.\r\n- **spt-core mapping:** `spt_net::net::wanmsg::WanMessage` (no origin field) + `spt_daemon::wan::receive_wan` (origin parameter sourced from the stream table) + every future wire-inbound consumer (D5b attach, D5c transfer, D8 notifs).\r\n- **Source:** M4-D5a design (ADR-0009 consequence: the gate must bind to the transport identity).\r\n- **Mesh note (ADR-0017, 2026-06-08):** preserved verbatim. The mesh never relays third-party rows, so a record's author is **always** the QUIC-handshake-proven connection origin. The new **membership proof** (seed-proof) is itself channel-bound to both handshake-proven pubkeys — the authorization subject stays transport truth, never payload.\r\n\r\n### 7.6 Pump brain-IPC reads must be deadline-bounded (a blocked read wedges the whole pump)  `[REQ-HAZARD-PUMP-IPC-DEADLINE]`\r\n<!-- [doc->REQ-HAZARD-PUMP-IPC-DEADLINE] -->\r\n- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop-top) stops and the stall warning fires honestly, but `supervise_pump` cannot rescue it: the supervisor catches a panic / error / clean return, and a BLOCKED thread never returns. Observed twice — 2026-06-07, then a **2.2h wedge on hfenduleam (2026-06-11)** w…\n- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a healthy long sync is never killed — only a ≥30s silence). **Mechanism note (Windows):** the carrier is a reader-thread + channel, NOT a non-blocking socket + poll — interprocess 2.4.2 on Windows named pipes has no portable read timeout (`set_recv_timeout` → `no_tim…\n- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the `net_dial`/`open_stream`/`send_stream` handlers) — shipped in v0.8.3 as §7.8 (`REQ-HAZARD-BROKER-QUIC-DEADLINE`).\r\n- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.\r\n\r\n### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`\r\n<!-- [doc->REQ-HAZARD-VIEWER-ISOLATION] -->\r\n- **Failure:** the W2.5 controller/viewer model lets ANY number of read-only `--view` attachers ride one session's broker `OutputLog`. The single drain thread fans each output chunk to every attacher. If a viewer's socket is fanned out with a **blocking** write under the log lock (the controller's authoritative path), one wedged viewer (a slow terminal, a black-holed WAN peer, a hostile non-reader) stalls the drain — freezing the controller's stream and backing up the PTY child. A single watcher must never be able to degrade or hang the driver.\r\n- **Invariant:** the drain writes the **controller** on the authoritative blocking bounded path (it alone advances `delivered_through`), but each **viewer** gets an **isolated bounded SPSC queue + a dedicated writer thread**; the drain `try_send`s under the log lock and **evicts** any viewer whose queue is `Full` (fell behind the live stream) or `Disconnected` (its writer died on a dead socket) — the drain thread **never touches a viewer socket**, so no viewer write can backpressure it. A **soft cap** (`MAX_VIEWERS`) bounds the writer-thread count (a viewer attach beyond it is refused). Viewer eviction never perturbs the controller stream, the `delivered_through` cursor, or the child. Ring **replay-at-attach** is owned by the writer thread (not the bounded l…\n- **spt-core mapping:** `OutputLog::append` (controller blocking + `viewer_send_evicts` `try_send` fan-out), `OutputLog::add_viewer` (bounded `sync_channel(VIEWER_CHANNEL_DEPTH)` + `viewer_writer` thread), `MAX_VIEWERS` soft cap, `ViewerSink`. Unit: `viewer_overflow_or_disconnect_evicts_never_blocks`. Int: `wedged_viewer_does_not_stall_controller` (a non-reading viewer is evicted while the controller keeps receiving past a 200KB burst).\r\n- **Source:** M12 W2.5 controller/viewer model (doyle ruling 2026-06-14, Q1).\r\n\r\n### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`\r\n<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\r\n- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only via its OWN 30s read-deadline (the 7.6 A-half) — a 30s stall + a full pump restart EACH round, and the supervised restart re-dials the SAME dead peer and re-wedges. Root cause of the 2.2h hfenduleam wedge (2026-06-11) and its recurrence (2026-06-16).\r\n- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`, so the BROKER fires FIRST — the brain reconstructs `ErrorKind::Other` (its `net_dial`/`net_open_stream`/`net_stream_send` map a broker error reply to `io::Error::other`), NOT its own read-deadline `TimedOut`, so `pump::peer_outcome` takes the ordinary per-pee…\n- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIES an ordinary error within the bound + exactly-once-on-timeout: journal un-applied, conn table empty, clean redial) and `pump_survives_a_black_holing_peer_heartbeat_advances_no_restart` (the production pump against a dead peer — heartbeat monotonic-advances, `run…\n- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.\r\n\r\n### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`\r\n<!-- [doc->REQ-HAZARD-BROKER-SEED-WIRE-SKEW] -->\r\n- **Failure:** the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004's no-terminate-during-update pillar forbids auto-killing it — 6.7). A self-update that changes a daemon-state WIRE FORMAT — e.g. the v0.9.0 adapter-agnostic `Seed` (the `adapter` field dropped) — therefore lands a NEW-version CLI talking to the STILL-RESIDENT OLD broker. The old broker cannot deserialize the new `Seed` (its formerly-required `adapter` is absent), so it drops the seed-control conn without acking; the CLI's `put_seed` ack-read hits EOF and surfaces a raw `UnexpectedEof` \"failed to fill whole buffer\" — a cryptic footgun that hides the real cause (perri PREP-4 FINDING 1: v0.9.0 CLI ↔ stale 0.8.x broker).\r\n- **Invariant:** (a) spt-core surfaces an ACTIONABLE diagnostic on the seed-ack EOF — naming the stale-broker cause + the fix (`spt daemon stop`; the broker restarts on the next `spt api` call) — never the bare io error (scoped to `UnexpectedEof` on the seed-ack path, so it never mis-fires on an unrelated error like a refused connect). (b) A daemon-state wire change requires a DELIBERATE full broker restart; this is NOT automatic — ADR-0004 forbids auto-killing the resident broker, and a brain-only update keeps the old broker. (c) FORWARD discipline: daemon-state / `Seed` schema changes stay ADDITIVE + serde-default, so a resident OLD broker tolerates a NEW CLI across a brain-only update. (This would NOT have rescued 0.9.0 itself — the old broker's `adapter`…\n- **spt-core mapping:** `startup::seed_fail_message` (the `UnexpectedEof` → actionable-hint branch) fired from `cmd_seed`'s `put_seed` error arm; the broker's seed-control residency (`seedmap::serve_seed_control`, held across brain restarts — 6.7). Unit: `seed_fail_eof_gives_actionable_stale_broker_hint` (EOF → the `spt daemon stop` hint naming the stale broker; a non-EOF kind → the plain message, no mis-fire).\r\n- **Source:** perri PREP-4 FINDING 1 (v0.9.0 dogfood) → v0.9.1.\r\n\r\n### 7.10 A VIEW is independent from the endpoint — closing the launching tab must NOT reap the daemon-hosted harness  `[REQ-HAZARD-VIEWER-CLOSE-DETACH]`\r\n<!-- [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH] -->\r\n- **Failure (Windows):** `spt endpoint run` autostarts the daemon (`ensure_running` → `detached_no_inherit`), which INHERITS the launching terminal's Windows Job Object. Windows Terminal / VS Code place the shell AND every descendant in a Job with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; closing the tab drops the job's last handle → the OS reaps the daemon + its broker-spawned ConPTY harness subtree. The `rc` pump detaching should end ONLY the viewport; the harness must keep running + stay re-attachable. (ConPTY isolation itself is already correct — portable-pty builds the pseudoconsole in the daemon; the leaking lifetime binding is the Job Object, not the console.)\r\n- **Invariant:** both daemon spawn paths request `CREATE_BREAKAWAY_FROM_JOB` (0x0100_0000), best-effort with an in-job FALLBACK on `ERROR_ACCESS_DENIED`(5) / `ERROR_INVALID_PARAMETER`(87) so a breakaway-denying job NEVER regresses the spawn. **A real job CAN deny breakaway** (proven: the cargo/CI runner's own ancestor job ACCESS_DENIES it), so efficacy against the real terminal is UNKNOWN until measured. **int stage = OPERATOR MANUAL ACCEPTANCE, not CI** (doyle ruling 2026-06-18): the runner sits inside a breakaway-denying job and every test-created job nests inside it → a faithful \"harness survives tab-close\" test is a guaranteed FALSE-RED; the two units (escape-where-permitted self-skip + fallback-where-denied) are the CI evidence; `required_stages` stays …\n- **spt-core mapping:** `daemon.rs::detached_no_inherit` + `deelevate.rs::create_with_token` (the breakaway flag + fallback). Units: `detached_no_inherit_falls_back_under_a_breakaway_denying_job` (no-regression — the shared runner exercises the fallback directly), `breakaway_spawn_escapes_a_kill_on_close_job` (the OS escape mechanism — self-SKIPS where the ancestor job forbids breakaway). Unix: the daemon's `setsid` session-detach already keeps a closing terminal's SIGHUP off its children (guard test, no code).\r\n- **Source:** v0.12.0 real-harness defect (operator) → v0.12.1 L1 @5ae68f8; doyle design+int ruling 2026-06-18.\r\n\r\n### 7.11 A dead PTY child + a dropped operator pump must NOT wedge the broker for other clients  `[REQ-HAZARD-ATTACH-WEDGE]`\r\n- **Failure (hypothesized, v0.12.0):** a legitimately dead PTY child (crash/kill) plus an `rc` pump dropped without a clean detach (closed tab) was thought to make the broker's loopback forward `write_all` block forever on a full 64 KB duplex, park a worker in the 2-worker net runtime, saturate both, and stall every new attach / `endpoint run` (with `daemon stop` unable to join).\r\n- **Invariant — DISPOSITION = PROVE-DON'T-CHANGE** (doyle GATE-PASS @e883f45, 2026-06-18): the post-L0 code ALREADY prevents the wedge; NO fail-fast / worker-count code was added. (1) `serve_attach` forwards fire-and-forget (`net_stream_send` `op_id=None`) and the broker-side `send_stream` is already deadline-bounded (`bounded_block_on`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s — hazard 7.8), not forever. (2) the loopback duplex is drained broker-INTERNALLY by the operator row's OWN read pump (`nethost.rs` `RecvHalf::Loopback`), which for an ordinary attach stream (`retentive_cap == 0`) NEVER parks → `peer_w` never backs up on a dead `rc` (a dead `rc` is just a dropped IPC subscriber against a bounded, EVICTING ring). (3) `bounded_block_on` = `runtime.handle().bloc…\n- **spt-core mapping:** int `crates/spt/tests/attach_wedge_e2e.rs` (REAL detached daemon + dummy-harness fixture): serve the victim (rc sees its tick), abruptly kill rc (dropped pump) + kill the PTY child → a NEW endpoint still comes online + is served (no wedge), the dead endpoint is offlined within a tick (`LIVENESS_RECONCILE_OFFLINE`), `daemon stop` bounded. Leans on 7.8 (the QUIC-op deadline) + 7.7 (viewer isolation).\r\n- **Source:** v0.12.0 real-harness finding (operator) → v0.12.1 L2 @e883f45; doyle GATE-PASS 2026-06-18.\r\n\r\n### 7.12 Controller output must NOT be written inline on the drain thread — a backed-up controller wedges the session  `[REQ-HAZARD-INJECT-CONTROL-COEXIST]`\r\n- **Failure (v0.12.x, operator dogfooding + doyle /diagnose 2026-06-19):** `OutputLog::append` fanned each live chunk to the CONTROLLER via a SYNCHRONOUS, blocking `write_frame` held INLINE on the session's single drain thread while `Mutex<OutputLog>` was locked (viewers already had a dedicated writer thread + bounded evicting channel). A backed-up controller socket — a slow operator, or the full 64 KB `rc` loopback duplex under heavy TUI redraw — parked the drain thread WITH THE LOG LOCK HELD: output + keystroke-echo stalled and every attach / resize / `KIND_SESSIONS` that needs the log lock blocked FOREVER, while the broker process stayed alive. Fires on NORMAL interactive `rc` use under heavy output (not only message injection); REOPENED the wedge facet o…\n- **Invariant:** controller delivery runs OFF the drain thread — a dedicated controller writer thread + bounded channel (the 7.7 viewer_writer pattern) does the blocking socket write; the drain hands each chunk off with a BOUNDED, OFF-LOCK send (deadline → detach + `clear_controller`, never park forever, never silently evict a LIVE operator's authoritative view). The controller is AUTHORITATIVE (unlike a viewer): its writer advances the `delivered_through` resume cursor (atomic, only-on-success, monotonic). `clear_controller` is the ONE unlatch path (clears `driven_by`, drops the sink) — W5's reconcile self-heal reuses it.\r\n- **spt-core mapping:** impl `broker.rs` `OutputLog::append`/`controller_writer`/`become_controller`/`ControllerJob::deliver`/`clear_controller`; int `crates/spt-daemon/tests/inject_control_wedge.rs` (a fully-backed-up controller keeps `KIND_SESSIONS` answering = no wedge); unit the bounded-deliver + monotonic-cursor kernels. The input-side parks (write_input `write_all` on a full buffer; DSR-answer writer-mutex contention) are BENIGN on Windows ConPTY (absorbs a large inject), real only on Unix forkpty → bounded by W2's atomic-write substrate.\r\n- **Source:** v0.12.x operator wedge → v0.13.0 W1 (doyle GATE-PASS 2026-06-19).\r\n\r\n### 7.13 `spt rc` must SWAP Windows legacy-console Backspace/Ctrl+Backspace (`^H`↔DEL); do NOT enable VT console input  `[REQ-HAZARD-RC-INPUT-KEY-ENCODING]`\r\n- **Failure (operator dogfooding):** `spt rc` is a raw verbatim stdin byte pump; the legacy Windows console delivers Backspace as `^H` (0x08), and Claude Code maps `^H` → backward-kill-word, so every Backspace deletes a whole word. Confirmed bytes (operator HITL capture, real Windows Terminal): Backspace=0x08, Ctrl+Backspace=0x7f.\r\n- **REJECTED fix (dc07c39, reverted):** enabling `ENABLE_VIRTUAL_TERMINAL_INPUT` on the stdin console BACKFIRED — on Windows Terminal that flag yields **win32-input-mode**, not legacy xterm VT. Every key then arrives as a 6-field `ESC[Vk;Sc;Uc;Kd;Cs;Rc_` key-down/up record: (1) the ctrl-b detach broke (0x02 is wrapped, so `parse_stdin_chunk` never sees the raw `DETACH_PREFIX`), and (2) win32-input-mode was forwarded into Claude Code's own ConPTY input negotiation (garbage). The VT lever is wrong for Windows Terminal. (Lesson: verify a terminal-input fix on the REAL terminal via the operator re-capture — unit/clippy never exercise the live key path.)\r\n- **Invariant:** stay a raw byte pump and NARROW-normalize in the forward path — **SWAP** the two delete bytes: `0x08` → `0x7f` (VT DEL, char-delete) AND `0x7f` → `0x08` (^H, kill-word), AFTER the detach state machine consumes any `0x02`, leaving `0x02` and every other byte untouched. Because CC reads `^H`=word-delete and DEL=char-delete, swapping the *input* bytes gives Backspace=char-delete and Ctrl+Backspace=word-delete — the native Win11 convention (the operator rejected losing word-delete; the earlier one-way `0x08`→`0x7f` map dropped it). cfg(windows) only (Unix is already VT; a real `ctrl+h`/`ctrl+?` there must pass through). Accepted minor loss: a real `ctrl+h` (0x08) and a real DEL (0x7f) are swapped on Windows — both are delete keys, so the practic…\n- **spt-core mapping:** the Backspace/Ctrl+Backspace SWAP intent persists; the `normalize_key_byte` byte-swap was SUPERSEDED on Windows by **7.16** (`REQ-RC-KEY-VT-TRANSLATE`, v0.13.0 bug 2) — the agnostic key-event→xterm-VT `translate_key_event` now emits Backspace → `0x7f` and Ctrl+Backspace → `0x08` natively (carrying the relocated `[impl]`/`[unit]` evidence), so `normalize_key_byte` + its unit were removed. The live keypress is HITL operator re-capture.\r\n- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19; superseded-on-Windows by the bug-2 translator 2026-06-19).\r\n\r\n### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`\r\n- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two facets, one root: **(A) stutter** — measured fsync on `%LOCALAPPDATA%\\spt-core` median 6.5 ms / spikes 198 ms, ×2 per keystroke → choppy, worsens with volume; **(B) HARD WEDGE** — a blocking `PtyWrite` (ConPTY/forkpty input buffer not draining) held the lock indefi…\n- **Invariant:** `apply_once` RELEASES the inner lock across `effect()` — reserve the key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable, mark applied). Crash-idempotency comes from the per-key reservation + the applied-set, not from holding the lock across the effect (the brain-crash guarantee is unchanged: the broker survives the brain, so reserve→effect→finalize runs uninterrupted on the broker thread; a replay hits the applied-set and dedups). `EffectKind::PtyWrite` is **EPHEMERAL** — NO journal lines, NO fsync, in-memory dedup only (a keystroke lost to a broker crash is retyped; PTY state is never rebuilt from keystroke replay) — while durable kinds (spool/r…\n- **spt-core mapping:** impl `effect.rs` `apply_once` (reserve/release/finalize) + `EffectKind::is_durable`; unit (`effect.rs`) a barrier proving the lock is NOT held across `effect()` (a different key's `apply_once` completes while one effect blocks) + `PtyWrite` writes no journal line while a durable kind (`NetSend`) does (the no-fsync proxy); int `crates/spt-daemon/tests/inject_control_wedge.rs` (real broker+PTY, a stalled consumer + sustained blocking input effect: a concurrent `spt rc` attach stays serviceable AND actually receives PTY bytes — the assertion W1's gate lacked). Unix park-(b) folded here, gated on gravity-linux. doyle GATE-GUARD: assert structurally, NEVER on fsync wall-clock (env-dependent, flaky); the stutter is fixed as a consequence of…\n- **Source:** v0.13.0 W1b (operator dogfooding post-W1 escape; doyle /diagnose + measurement 2026-06-19).\r\n\r\n### 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]`\r\n- **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 already clears the `status=online` latch here but left `driven_by` untouched.\r\n- **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 disconnect already self-heals via `detach_if`→`clear_controller`, W1 (7.12) bounds the active-output wedge, and the residual idle-wedged-REMOTE case is the deferred `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`, which needs BROKER-SIDE liveness eviction — a brain reconcile can …\n- **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).\r\n- **spt-core mapping:** impl `livehost.rs` `reconcile_hosted_liveness` Gap-B clear + the additive `SessionInfo.controller_by` observability field (`msg.rs`, populated in `broker.rs` `KIND_SESSIONS`); unit `livehost.rs` `pull_liveness…` extended (offlined sessionless perch clears `driven_by`; live/relay perches untouched); int `crates/spt-daemon/tests/driven_by_selfheal.rs` `gap_b` (real broker: reconcile offlines AND clears `driven_by`) + the A1/A2 characterization (`inject_control_wedge.rs` w5_a1/w5_a2).\r\n- **Cross-ref 7.29 (who MAY clear vs who MUST NOT):** the BRAIN reconcile here MUST NOT clear `driven_by` off `controller_by==None` (ambiguous with a live LOCAL controller). The BROKER stamp-convergence (7.29) MAY — it reads `has_controller()` DEFINITIVELY (its own controller slot), so it authoritatively clears a stale `driven_by` against a live session with no controller (the `has_controller()==false` leg). The invariant's owner for that leg moved to the broker; this brain-side ambiguity-safety is unchanged. The wedged-open-remote A2 residual (`has_controller()==true`) is NOT covered by either — still `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.\r\n- **Source:** v0.13.0 W5 (repro-first, todlando; doyle-assigned 2026-06-19). A2 (idle wedged-remote leg) deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.\r\n\r\n<!-- [doc->REQ-RC-KEY-VT-TRANSLATE] -->\r\n### 7.16 `spt rc` translates Windows console KEY EVENTS to standard xterm VT (arrows/Home/End/F-keys reach the harness); supersedes the W7 byte-swap  `[REQ-RC-KEY-VT-TRANSLATE]`\r\n- **Failure (operator dogfooding):** `spt rc` read raw stdin BYTES, but the Windows legacy console (no `ENABLE_VIRTUAL_TERMINAL_INPUT`) delivers arrows / Home / End / PgUp / PgDn / Insert / Delete / F-keys as console KEY_EVENTs, NOT stdin bytes — so the byte-pump saw nothing and those keys were DEAD (only byte-emitting keys like Backspace worked, via the 7.13 swap).\r\n- **Invariant:** on Windows, read crossterm KEY EVENTS (the picker already does) and translate each to STANDARD xterm VT via the pure `translate_key_event` (copy a known-correct xterm table verbatim: arrows `ESC[A..D`, Home `ESC[H` / End `ESC[F`, `~` keys `ESC[<n>~`, modified `ESC[1;<m><final>` / `ESC[<n>;<m>~` with `m = 1 + Shift + 2·Alt + 4·Ctrl`, F1–F4 `ESC OP..S` / F5–F12 `ESC[<n>~`), forwarded through the SAME rc pump — the harness receives ordinary xterm VT (AGNOSTIC; NOT win32-input-mode, the 7.13 rejected lever). Press-only (drop Repeat/Release). Detach stays the `ctrl-b d` PREFIX, event-sourced (Ctrl+B arms; armed + plain `d` ⇒ Detach; armed + Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` + translated). NON-tty stdin (piped / tests) falls back to …\n- **spt-core mapping:** impl `rc.rs` `translate_key_event` + `csi_final`/`csi_tilde`/`f_key` + `key_event_step` (event detach SM) + `spawn_stdin_reader` cfg-split (`spawn_stdin_reader_events` on a tty / `spawn_stdin_reader_bytes` non-tty + Unix); unit the EXHAUSTIVE `translate_key_event` mapping + the event-detach SM (the Backspace/Ctrl+Backspace arm carries the relocated `REQ-HAZARD-RC-INPUT-KEY-ENCODING` evidence); NO int (live console = HITL, operator re-capture — REQ-RUN-PICKER/RC-1 precedent).\r\n- **Source:** v0.13.0 bug 2 (operator ruling: proper agnostic translator, ship-blocker; doyle design + Option-B detach ruling 2026-06-19).\r\n\r\n<!-- [doc->REQ-RC-WIN-PASTE] -->\r\n### 7.18 `spt rc` paste is client-originated on Windows — read the LOCAL clipboard, inject a BRACKETED paste  `[REQ-RC-WIN-PASTE]`\r\n- **Failure (operator dogfooding):** in an `spt rc` session neither ctrl+V nor right-click pasted (CC explicitly supports ctrl+V). `RawGuard` did 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 translated to a 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. A multi-line paste-as-keys also became a `\\r` submit-storm.\r\n- **Invariant:** on Windows (cfg-split; folds into the 7.16 event path), on a RIGHT-CLICK rc reads the LOCAL clipboard itself and forwards a BRACKETED paste — `wrap_bracketed_paste` = `ESC[200~` + content + `ESC[201~`. CC has bracketed-paste mode on (its TUI sets `ESC[?2004h`), so it treats the synthesized markers as a PASTE: content lands intact, NO submit-storm, harness-AGNOSTIC (standard xterm contract). `RawGuard` also `EnableMouseCapture` (disables console QuickEdit + enables `ENABLE_MOUSE_INPUT` so a right-click surfaces as `Event::Mouse`) on an interactive console only, restored on drop → right-button-down → `read_clipboard` → bracketed paste. `read_clipboard` = the `clipboard-win` crate; empty/failed read ⇒ a clean no-op (never inject garbage, never …\n- **spt-core mapping:** impl `rc.rs` `wrap_bracketed_paste` + `mouse_is_paste` + `clipboard_paste` + `read_clipboard` + `windows_mouse_wanted` + `RawGuard` (mouse capture/restore) + the `spawn_stdin_reader_events` right-mouse arm; unit the exact bracketed framing + content-verbatim + mouse classify (right-down ⇒ paste, all else ⇒ drop) + the injected-reader paste decision (non-empty ⇒ wrapped, empty/fail ⇒ no-op); NO int (live clipboard + console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent).\r\n- **Source:** v0.13.0 P1 (operator HITL; doyle design 2026-06-19). Depends on P0 (7.17). AMENDED v0.13.0 P1b: scope narrowed to right-click-only (the dead ctrl+V interception removed); scroll-forward is 7.20; the input-flood non-wedge is 7.19.\r\n\r\n<!-- [doc->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] -->\r\n### 7.19 An operator input FLOOD must not deadlock the broker via the applied-ack on the same conn  `[REQ-HAZARD-INPUT-ACK-BACKPRESSURE]`\r\n- **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 → the broker→brain return direction fills (~10 frames = the IPC pipe buffer) → `send_frame` BLOCKS → the handler stops reading → the brain's writes block → mutual full-duplex DEADLOCK. (Capture: 11 frames, `write_input` 11/11 — P0 holds; `ack send` START=11/END=10…\n- **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=true`). EXACTLY-ONCE preserved: the broker dedups by `(session, op_id)` at the applied-set regardless of the ack. **N-1 caveat:** an OLD resident broker (the self-update window) ignores `ack=false` → still acks → the deadlock persists until a broker restart (inher…\n- **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 session stays live + a concurrent attach opens (real broker+brain, no mocks).\r\n- **Source:** v0.13.0 P1b (operator HITL re-open; doyle /diagnose on the enhanced rc+broker capture 2026-06-19). The real ship-blocker behind the ctrl+V wedge.\r\n\r\n<!-- [doc->REQ-RC-MOUSE-FORWARD] -->\r\n### 7.20 `spt rc` must forward the scroll wheel to the harness (our mouse capture steals WT's native scroll)  `[REQ-RC-MOUSE-FORWARD]`\r\n- **Failure (operator HITL):** scroll stopped working in `spt rc`. P1's `EnableMouseCapture` (for right-click paste, 7.18) makes Windows Terminal forward ALL mouse — including the wheel — to rc instead of scrolling its own buffer, but the rc mouse handler dropped everything except right-button-down → scroll DIED (and WT's native scrollback is stolen under the capture).\r\n- **Invariant:** on Windows, TRACK the harness's mouse-reporting mode from its OUTPUT — scan for DECSET `ESC[?1000h/1002h/1003h` (mouse on) + `ESC[?1006h` (SGR ext) and their `…l` (off) into a shared `MouseMode{enabled,sgr}` (the pump writes from the output render path, the stdin reader reads); the scan survives a sequence SPLIT across output chunks (a bounded carry buffer). The mouse handler: right-button-DOWN → bracketed clipboard paste (7.18, unchanged); `ScrollUp/Down` → an xterm SGR mouse report (`ESC[<64;col+1;row+1M` up / `ESC[<65;…M` down; 0-based crossterm → 1-based xterm), forwarded ONLY when `enabled && sgr` (else DROP — a legacy report the harness may misread is garbage); Moved/drag/left/middle DROP (scroll is the need; click-forward risks garbag…\n- **spt-core mapping:** impl `rc.rs` `MouseMode` + `MouseModeScanner`/`parse_decset_private`/`apply_mouse_mode` (carry-buffer scan, fed from the pump's output path) + `scroll_dir` + `scroll_sgr` + the `spawn_stdin_reader_events` scroll arm (threaded via `Arc<MouseMode>`); unit `scroll_dir` classify + `scroll_sgr` exact bytes + the DECSET scan (set/reset, combined, mixed, and split-across-chunks); NO int (live console mouse = HITL).\r\n- **Source:** v0.13.0 P1b (operator HITL; doyle design 2026-06-19). Bundled with 7.19.\r\n\r\n<!-- [doc->REQ-HAZARD-CONTROLLER-WRITER-REORDER] -->\r\n### 7.21 Exactly ONE `controller_writer` per brain↔broker connection — a superseded writer must write nothing further  `[REQ-HAZARD-CONTROLLER-WRITER-REORDER]`\r\n- **Failure (doyle instrumented RACEDIAG repro, kitsubito):** on a brain-restart re-serve the handoff brain registered as controller on the SAME session TWICE over the SAME socket — `Brain::handoff` eagerly `subscribe(prior.next_seq=1)` → `become_controller(from_seq=1)`, spawning writer-A (writes seq 1); then `serve_attach` re-handled the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, spawning writer-B (writes 0,1). `become_controller` dropped the prior `ControllerSink` (its `tx`) but did NOT stop the prior writer — writer-A kept flushing its OWNED `initial` batch, and both writers held clones of one `SharedSend` (`Arc<Mutex<socket>>`) with no inter-thread ordering. When writer-A's seq 1 beat writer-B's seq 0, the stri…\n- **Invariant:** on a single brain↔broker connection exactly ONE `controller_writer` is ever the LIVE writer; a SUPERSEDED writer writes no further frames after the epoch bump it observes; and **every `controller_writer` emits a strictly ASCENDING seq stream** (sorted initial batch + ascending live frames). The CORRECTNESS guarantee that falls out: a snap-above consumer over any interleaving of ascending writers — where the surviving writer (`serve_attach`'s `attach_as(sid,0)`) offers the COMPLETE range `[0, end]` — delivers `[K, end]` with **no skip and no dup** (the first sighting of any seq `>M` is always preceded by a sighting of `M` on that same ascending writer, so `M` is delivered before the cursor can pass it). Enforced/relied-on three ways (NB: fix …\n- **spt-core mapping:** impl `brain.rs` `Brain::handoff` (KEEP the eager `subscribe`; seed `session_cursors`) + `Brain::subscribe_with` (resume-mode dedup-cursor reset to `from_seq`, shared by `attach`/`attach_as` — the operator-stream boundary fix) + `broker.rs` `OutputLog.controller_epoch: Arc<AtomicU64>` / `become_controller` (atomic `fetch_add`, passes the new epoch + `Arc::clone` into the writer) / `controller_writer` (epoch gate read UNDER the lock on both loops) / `mark_controller_gone` + the `ControllerJob` epoch read; unit (white-box, `src/broker.rs`) the epoch-gated writer (a superseded writer flushes nothing — only the latest writer's monotonic stream reaches the wire) + `handoff` seeds `session_cursors`/resubscribes; int (keystone, `tests/broker.…\n- **Source:** v0.13.0 P1c (operator-ruled root-fix before ship; doyle root-cause via instrumented repro 2026-06-20; design corrected across two gate rounds — fix #1 reverted, then the kitsubito RACEDIAG pinned the residual to the consumer-side operator-stream boundary, fixed by the `subscribe_with` cursor reset on `attach_as` re-subscribe). The last v0.13.0 ship-blocker.\r\n\r\n### 7.22 An idle delivery with no working translation binary must SPOOL, never raw-inject a pseudo-delivery reported as delivered  `[REQ-HAZARD-IDLE-SILENT-NONDELIVERY]`\r\n- **Failure (F-019 post-mortem):** the v0.11.0 path raw-injected `payload+\\r` into the spt-hosted PTY whenever no working translation binary handled an inbound message — none declared, spawn-failed, FAULTED (commit-deadline miss / binary death), or its inject-worker channel dropped — AND acked `delivered=true`. But a bare `payload+\\r` does NOT submit on a modern TUI (Claude Code): the message was TYPED into the harness but never sent, a silent pseudo-delivery reported as success. That silent degrade-to-raw-inject is precisely what MASKED F-019 (`[REQ-INSTALL-11]`, above) through a multi-hour black-box hunt — every behavioral hypothesis was moot because the \"delivery\" never delivered.\r\n- **Invariant:** spt-hosted idle delivery is translation-binary-ONLY (ADR-0022 amendment). `dispatch_endpoint_input` with no working binary replies `endpoint_injected_envelope(ep, delivered=false)` and writes NOTHING to the PTY; the caller (`try_broker_inject` → `cmd_send`) reads `delivered=false` and falls through to `deliver::send` = SPOOL (poll-fed, never lost), reporting an honest `QUEUED`, never a confident-but-false `SENT`. The failure is LOUD (`ENDPOINT_INJECT:<ep>: no working translation binary … -> SPOOLED, not injected`). The raw-inject fallback (`input.enqueue`) is removed from the no-binary, worker-dropped, AND post-fault paths; the operator-keystroke floor-flush on FAULT is UNCHANGED (operator input is never stranded). Out of scope (follow-up): …\n- **Boundary — what this does NOT cover (the STATE-vs-transient precision):** the guarantee is the steady FAILED STATE — once a binary IS faulted/absent/worker-gone, subsequent deliveries spool (`faulted` is MONOTONIC: set once, never respawns, so the state is reached deterministically). It does NOT cover the FAULT-TRANSIENT: a delivery that lands in the worker's commit window — BEFORE `event_rx` is dropped / `faulted` is set — can be optimistically enqueue-acked (`delivered=true` the instant `event_tx.send` succeeds) then DROPPED when the worker faults+returns. This is a SEPARATE, PRE-EXISTING hazard (raw-inject removal did not touch it — the old code dropped that queued event too; v0.14.3 makes nothing worse) and is tracked for **v0.15.0 under `[REQ-MSG-DE…\n- **spt-core mapping:** impl `broker.rs` `dispatch_endpoint_input` (no-working-binary → `delivered=false` + loud log, no `input.enqueue`; the `input` writer is no longer resolved from the sessions table) + `build_translation`/`fault_translation` (`None`/fault now MEAN spool); unit `msg.rs` `endpoint_injected_envelope_carries_delivered_both_ways` (the `delivered=false` spool signal round-trips); int (real broker+PTY) `tests/broker.rs` `endpoint_keyed_inject_without_binary_spools_not_pty` + `tests/inject_control_wedge.rs` `large_endpoint_inject_to_a_no_binary_session_spools_promptly_without_wedging` + `g2_no_commit_deadline_faults_binary_and_does_not_wedge_controller_input` (post-FAULT inbound EVENTUALLY spools via bounded-retry-until-spool, marker NEVER on PT…\n- **Source:** v0.14.3 (ADR-0022 amendment @1eaeef1, operator-ruled; doyle-scoped, the F-019 follow-up). CHANGE-5 (`cli.rs` honest report) verified a NO-OP — the spool fall-through already reported `QUEUED`, never `SENT`.\r\n<!-- [doc->REQ-HAZARD-IDLE-SILENT-NONDELIVERY] -->\r\n\r\n---\r\n\r\n### 7.23 A message that has REACHED a node's spool must NEVER depend on an adapter hook-poll cadence to reach an spt-hosted (relay-less) endpoint — the daemon drives delivery on the events it owns  `[REQ-HAZARD-DELIVERY-STARVATION]`\r\n- **Failure (F-023, RCA @ `f023-f024-wan-idle-starvation`):** a message that has already landed in a node's spool — WAN-arrived, or locally spooled-while-active — stranded INDEFINITELY on an spt-hosted, relay-LESS endpoint whenever the adapter's hooks went quiet (an idle session with no user turns). An spt-hosted perch has no `api listen` relay to wake it, and NOTHING in the daemon re-offered a spooled message once the arrival moment passed — delivery silently depended on the adapter happening to call `api poll`. Presented as \"sent but never lands\" with a HEALTHY translation binary and a visibly-idle perch (F-023: verified 7 min idle, binary alive, zero injection). TWO gaps: (1) the WAN-ingress path (`receive_wan`) tried the harness-hosted TCP relay then fel…\n- **Invariant:** the daemon DRIVES delivery of an already-spooled message on the events IT owns — never on hook cadence — for a relay-less spt-hosted endpoint. TWO daemon triggers feed ONE shared spt-hosted inject leg (`spt_daemon::inject::try_spt_hosted_inject`, translation-binary-ONLY): (1) **WAN ingress** — `receive_wan` injects via the live binary BEFORE the spool fallback (`[REQ-WAN-SPT-HOSTED-DELIVERY]`, leg 1); (2) **the ACTIVE→IDLE edge** — `drain_idle_window` (fired by `api state idle`) claims the pending spool (deferred AND non-deferred) and injects each via the same leg, reusing the hook-poll take/ack so a concurrent `api poll` cannot double-deliver (`[REQ-MSG-IDLE-EDGE-DRAIN]`, leg 2). The v0.14.3 LAW holds on BOTH triggers (7.22): no working bin…\n- **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 delivery with NO hook poll.\r\n- **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).\r\n<!-- [doc->REQ-HAZARD-DELIVERY-STARVATION] -->\r\n\r\n---\r\n\r\n### 7.24 A delegated live adapter apply must NEVER report success without swapping, and the live-update seam must use ONE parent-aware adapter matcher  `[REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP]`\r\n…\n\n…\n","startLine":287,"lineNumbers":[287,null,325,326,null,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,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,null,531,null,664]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\KNOWN-HAZARDS.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":736,"totalBytes":71115,"outputLines":195,"outputBytes":71115,"shownRange":{"start":329,"end":523},"nextOffset":524},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784196066598}}
{"type":"message","id":"d96e23bf","parentId":"6b0c5499","timestamp":"2026-07-16T10:01:13.546Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Confirming PTY ownership in pty.rs**"},{"type":"toolCall","id":"call_2ngsJXESuEYTyRqMAJP5cpPE|fc_047bb49263dfa303016a58abe7d7a481989b92fc5e261e9f9a","name":"grep","arguments":{"i":"Indexing PTY session symbols","pattern":"pub struct PtySession|pub fn spawn|pub fn drain|pub fn write_input|pub fn resize|pub fn wait|pub fn kill|take_reader|MasterPty|writer","path":"crates/spt-term/src/pty.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":18829,"output":132,"cacheRead":162304,"cacheWrite":0,"totalTokens":181265,"reasoningTokens":37,"cost":{"input":0.094145,"output":0.00396,"cacheRead":0.081152,"cacheWrite":0,"total":0.179257}},"stopReason":"toolUse","timestamp":1784196066646,"responseId":"resp_047bb49263dfa303016a58abe3124881989fe8e9c596340dc5","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58abe6a5848198afc662db2ed417d4","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKvneGurxPyBjm1RGx4NipGJ4Z3JU46YhW2XkuvPbom13lbJ4C8xvpLgwjiCkpQfeElpbRx9AWSoZ0JG5v83alcTmQEThWCMD0OI5dNTDXDLDc8bfUM6i7ZDHlYon-cO2IDrqqCWn0RHJxnDiapmqqINgDsUwZu9ZIsAH8KjgTTZf5niUW4kY7UgALfjVovBVsbgxaqcv7EFacCd9QF5-CyzV15u_UZcdLSPJjfKEcHDluIctkpkTyXFA3CY77ptCVq_5gsoHDgenl5raqLLEE95R62zwNhQg6a81UDbBw9F_Xz_gvQJGhA1x919bvNV-qMJvzdqFW0xxcIfh2WqRH6z0OwlN2zVGH-XVY833UOM_0mYfdc-4g8z83ZagbRECK-apFoWGjX6hxSPV4flfzPXzk2_M2G73oKEUq_VTRtniQ1objJGn2kF1KkNuE0bviygtKwqLSf9ClM4ZjYyL-cfSMEQqFrbPeZuYFdn5Ix9zACnV_thTvYO8NK5wd7kX3QKQKWRPMrOHiZ6r1NmsLN6cMFe2exkUuUGtDKLE_WVRzOCh3-yqSAX-dzUuwpe0f17ugf62GRWA7hD9HaxpagedyNLzQgZB_XLbtp2OoaETkmICdAysApRE-LbwT2yjZsg88o9DzN90TX2UileU9uEnEhw-2TdTI0LcHBGWP8cHKiqty2Jzzm-yBV1LV7Fnd6ntZZzI27ojORQ13V4PPDkZuXEDcHRqfVSQQ2GnJY7JognGqLSOULLV5899julYCFkUMG2qGIDYJeuHmv7fUuoBVHMej6ukAD-Zd1BGN1tm-aiFpBUQXjcdf265E6g1MsYvqU0DzYdO36HjghGi7Qwp_Pn1GeFxJaE57vfsLL2hl-GuuJgbCd2VOi0KRWrVoT62pYNMaHI-U8CfxFl4-BdRlVgUdXqqq8jeI0c2-XxixvP1KO572ILwC7nz9QIYq22biRwggfWciFa4g8vPKwlfjNxftTQWy7XrFkdOwROPXB3XvPMvHPhn0te33mUokjTekCLhvTl5K1Ofm-XTbIAtDKp4Ovs_n6f7XslYe2IczwFrLWp5ndWOKySnJyemdbsQt3oqMS1XajtvPbz2xubWQ==","summary":[{"type":"summary_text","text":"**Confirming PTY ownership in pty.rs**"}]},{"id":"fc_047bb49263dfa303016a58abe7d7a481989b92fc5e261e9f9a","type":"function_call","status":"completed","arguments":"{\"i\":\"Indexing PTY session symbols\",\"pattern\":\"pub struct PtySession|pub fn spawn|pub fn drain|pub fn write_input|pub fn resize|pub fn wait|pub fn kill|take_reader|MasterPty|writer\",\"path\":\"crates/spt-term/src/pty.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_2ngsJXESuEYTyRqMAJP5cpPE","name":"grep"}]},"duration":6899.900600000285,"ttft":3977.0352999996394,"contextSnapshot":{"promptTokens":181133,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_2ngsJXESuEYTyRqMAJP5cpPE|fc_047bb49263dfa303016a58abe7d7a481989b92fc5e261e9f9a","toolName":"grep","startedAt":"2026-07-16T10:01:13.547Z","args":{"path":"crates/spt-term/src/pty.rs"},"intent":"Indexing PTY session symbols"},"id":"62271306","parentId":"d96e23bf","timestamp":"2026-07-16T10:01:13.547Z"}
{"type":"message","id":"e2878328","parentId":"62271306","timestamp":"2026-07-16T10:01:13.549Z","message":{"role":"toolResult","toolCallId":"call_2ngsJXESuEYTyRqMAJP5cpPE|fc_047bb49263dfa303016a58abe7d7a481989b92fc5e261e9f9a","toolName":"grep","content":[{"type":"text","text":" 5|//! [`PtySession`] spawns a child under a real PTY, owns the master reader and\n*6|//! writer, and serves the [`SessionSurface`] contract. It is **single-surface\n 7|//! per handle** — one child's PTY, not the multiplexed fleet (that supervisor is\n 8|//! the M3b broker).\n 9|//!\n...\n 12|//! `resize()` races the reader thread on Windows; the master is therefore held\n*13|//! behind a `Mutex` and every `resize` takes that lock. The writer is a shared\n 14|//! `Arc<Mutex<…>>` so the A2 DSR drain thread can answer ConPTY's startup\n*15|//! cursor query through the same serialized writer the caller injects input\n 16|//! through — no torn writes.\n 17|\n 18|use std::io::{Read, Write};\n...\n 20|\n*21|use portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, ExitStatus, MasterPty};\n 22|\n 23|use crate::surface::{SessionSurface, SurfaceError, SurfaceSize};\n 24|\n*25|/// Shared, serialized handle to the PTY master writer. Cloned to the A2 drain\n 26|/// thread so the DSR auto-answer and caller input never interleave mid-write.\n 27|pub(crate) type SharedWriter = Arc<Mutex<Box<dyn Write + Send>>>;\n 28|\n...\n 32|/// (or the A3 `send_keys`/`send_line` helpers), drain output via\n*33|/// [`PtySession::take_reader`], and observe lifecycle via [`PtySession::try_wait`] /\n 34|/// [`PtySession::wait`] / [`PtySession::kill`].\n*35|pub struct PtySession {\n 36|    /// The master end, behind a lock so `resize` never races the reader (Spike #5).\n*37|    master: Mutex<Box<dyn MasterPty + Send>>,\n*38|    /// The single PTY writer, shared so the A2 drain thread can answer DSR.\n*39|    writer: SharedWriter,\n 40|    /// The spawned child; behind a lock for `wait`/`try_wait`.\n 41|    child: Mutex<Box<dyn Child + Send + Sync>>,\n 42|    /// A kill handle split from the child so we can terminate from another\n...\n 44|    killer: Mutex<Box<dyn ChildKiller + Send + Sync>>,\n*45|    /// The master reader, taken at most once (`take_reader`).\n 46|    reader: Mutex<Option<Box<dyn Read + Send>>>,\n 47|    /// The child OS pid, captured at spawn.\n 48|    pid: Option<u32>,\n...\n 56|    /// the Unix raw-pipe backend once the child exits. (On Windows the no-EOF\n*57|    /// behaviour is governed by the writer, handled by the A2 drain discipline.)\n 58|    //\n 59|    // [impl->REQ-TERM-1]\n*60|    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {\n 61|        let pty_system = native_pty_system();\n 62|        let pair = pty_system\n 63|            .openpty(size.into())\n...\n 76|            .map_err(|e| SurfaceError::Io(io_other(e)))?;\n*77|        let writer = pair\n 78|            .master\n*79|            .take_writer()\n 80|            .map_err(|e| SurfaceError::Io(io_other(e)))?;\n 81|        let killer = child.clone_killer();\n 82|        let pid = child.process_id();\n...\n 85|            master: Mutex::new(pair.master),\n*86|            writer: Arc::new(Mutex::new(writer)),\n 87|            child: Mutex::new(child),\n 88|            killer: Mutex::new(killer),\n 89|            reader: Mutex::new(Some(reader)),\n...\n 95|    /// inheriting the caller's working directory.\n*96|    pub fn spawn_program<P, A, S>(\n 97|        program: P,\n 98|        args: A,\n 99|        size: SurfaceSize,\n...\n 113|    // [impl->REQ-HOST-RUN-2]\n*114|    pub fn spawn_program_in<P, A, S, D>(\n 115|        program: P,\n 116|        args: A,\n 117|        size: SurfaceSize,\n...\n 135|    // [impl->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]\n*136|    pub fn spawn_program_in_env<P, A, S, D, E, K, V>(\n 137|        program: P,\n 138|        args: A,\n 139|        size: SurfaceSize,\n...\n 177|    /// thread in the hosted path; a test consumer otherwise).\n*178|    pub fn take_reader(&self) -> Result<Box<dyn Read + Send>, SurfaceError> {\n 179|        self.reader\n 180|            .lock()\n 181|            .unwrap()\n...\n 185|\n*186|    /// The shared writer handle — cloned to the A2 DSR drain thread so its\n*187|    /// auto-answer goes through the same serialized writer the caller injects\n 188|    /// input through (no torn writes).\n*189|    pub(crate) fn shared_writer(&self) -> SharedWriter {\n*190|        Arc::clone(&self.writer)\n 191|    }\n 192|\n 193|    /// Poll for child exit without blocking.\n...\n 202|    /// Block until the child exits, yielding its status.\n*203|    pub fn wait(&self) -> Result<ExitStatus, SurfaceError> {\n 204|        self.child.lock().unwrap().wait().map_err(SurfaceError::Io)\n 205|    }\n 206|\n...\n 209|    /// [`PtySession::wait`] / [`PtySession::try_wait`].\n*210|    pub fn kill(&self) -> Result<(), SurfaceError> {\n 211|        let res = self.killer.lock().unwrap().kill();\n 212|        #[cfg(windows)]\n 213|        {\n...\n 236|    fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError> {\n*237|        let mut w = self.writer.lock().unwrap();\n 238|        w.write_all(bytes).map_err(SurfaceError::Io)?;\n 239|        w.flush().map_err(SurfaceError::Io)\n 240|    }"}],"details":{"scopePath":"crates/spt-term/src/pty.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term\\src\\pty.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":27,"fileCount":1,"files":["crates/spt-term/src/pty.rs"],"fileMatches":[{"path":"crates/spt-term/src/pty.rs","count":27}],"truncated":false,"displayContent":"   5│//! [`PtySession`] spawns a child under a real PTY, owns the master reader and\n  *6│//! writer, and serves the [`SessionSurface`] contract. It is **single-surface\n   7│//! per handle** — one child's PTY, not the multiplexed fleet (that supervisor is\n   8│//! the M3b broker).\n   9│//!\n    │...\n  12│//! `resize()` races the reader thread on Windows; the master is therefore held\n *13│//! behind a `Mutex` and every `resize` takes that lock. The writer is a shared\n  14│//! `Arc<Mutex<…>>` so the A2 DSR drain thread can answer ConPTY's startup\n *15│//! cursor query through the same serialized writer the caller injects input\n  16│//! through — no torn writes.\n  17│\n  18│use std::io::{Read, Write};\n    │...\n  20│\n *21│use portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, ExitStatus, MasterPty};\n  22│\n  23│use crate::surface::{SessionSurface, SurfaceError, SurfaceSize};\n  24│\n *25│/// Shared, serialized handle to the PTY master writer. Cloned to the A2 drain\n  26│/// thread so the DSR auto-answer and caller input never interleave mid-write.\n  27│pub(crate) type SharedWriter = Arc<Mutex<Box<dyn Write + Send>>>;\n  28│\n    │...\n  32│/// (or the A3 `send_keys`/`send_line` helpers), drain output via\n *33│/// [`PtySession::take_reader`], and observe lifecycle via [`PtySession::try_wait`] /\n  34│/// [`PtySession::wait`] / [`PtySession::kill`].\n *35│pub struct PtySession {\n  36│    /// The master end, behind a lock so `resize` never races the reader (Spike #5).\n *37│    master: Mutex<Box<dyn MasterPty + Send>>,\n *38│    /// The single PTY writer, shared so the A2 drain thread can answer DSR.\n *39│    writer: SharedWriter,\n  40│    /// The spawned child; behind a lock for `wait`/`try_wait`.\n  41│    child: Mutex<Box<dyn Child + Send + Sync>>,\n  42│    /// A kill handle split from the child so we can terminate from another\n    │...\n  44│    killer: Mutex<Box<dyn ChildKiller + Send + Sync>>,\n *45│    /// The master reader, taken at most once (`take_reader`).\n  46│    reader: Mutex<Option<Box<dyn Read + Send>>>,\n  47│    /// The child OS pid, captured at spawn.\n  48│    pid: Option<u32>,\n    │...\n  56│    /// the Unix raw-pipe backend once the child exits. (On Windows the no-EOF\n *57│    /// behaviour is governed by the writer, handled by the A2 drain discipline.)\n  58│    //\n  59│    // [impl->REQ-TERM-1]\n *60│    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {\n  61│        let pty_system = native_pty_system();\n  62│        let pair = pty_system\n  63│            .openpty(size.into())\n    │...\n  76│            .map_err(|e| SurfaceError::Io(io_other(e)))?;\n *77│        let writer = pair\n  78│            .master\n *79│            .take_writer()\n  80│            .map_err(|e| SurfaceError::Io(io_other(e)))?;\n  81│        let killer = child.clone_killer();\n  82│        let pid = child.process_id();\n    │...\n  85│            master: Mutex::new(pair.master),\n *86│            writer: Arc::new(Mutex::new(writer)),\n  87│            child: Mutex::new(child),\n  88│            killer: Mutex::new(killer),\n  89│            reader: Mutex::new(Some(reader)),\n    │...\n  95│    /// inheriting the caller's working directory.\n *96│    pub fn spawn_program<P, A, S>(\n  97│        program: P,\n  98│        args: A,\n  99│        size: SurfaceSize,\n    │...\n 113│    // [impl->REQ-HOST-RUN-2]\n*114│    pub fn spawn_program_in<P, A, S, D>(\n 115│        program: P,\n 116│        args: A,\n 117│        size: SurfaceSize,\n    │...\n 135│    // [impl->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]\n*136│    pub fn spawn_program_in_env<P, A, S, D, E, K, V>(\n 137│        program: P,\n 138│        args: A,\n 139│        size: SurfaceSize,\n    │...\n 177│    /// thread in the hosted path; a test consumer otherwise).\n*178│    pub fn take_reader(&self) -> Result<Box<dyn Read + Send>, SurfaceError> {\n 179│        self.reader\n 180│            .lock()\n 181│            .unwrap()\n    │...\n 185│\n*186│    /// The shared writer handle — cloned to the A2 DSR drain thread so its\n*187│    /// auto-answer goes through the same serialized writer the caller injects\n 188│    /// input through (no torn writes).\n*189│    pub(crate) fn shared_writer(&self) -> SharedWriter {\n*190│        Arc::clone(&self.writer)\n 191│    }\n 192│\n 193│    /// Poll for child exit without blocking.\n    │...\n 202│    /// Block until the child exits, yielding its status.\n*203│    pub fn wait(&self) -> Result<ExitStatus, SurfaceError> {\n 204│        self.child.lock().unwrap().wait().map_err(SurfaceError::Io)\n 205│    }\n 206│\n    │...\n 209│    /// [`PtySession::wait`] / [`PtySession::try_wait`].\n*210│    pub fn kill(&self) -> Result<(), SurfaceError> {\n 211│        let res = self.killer.lock().unwrap().kill();\n 212│        #[cfg(windows)]\n 213│        {\n    │...\n 236│    fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError> {\n*237│        let mut w = self.writer.lock().unwrap();\n 238│        w.write_all(bytes).map_err(SurfaceError::Io)?;\n 239│        w.flush().map_err(SurfaceError::Io)\n 240│    }"},"isError":false,"timestamp":1784196073549}}
{"type":"message","id":"f9ebefd2","parentId":"e2878328","timestamp":"2026-07-16T10:01:22.868Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Searching for drain function**"},{"type":"toolCall","id":"call_rvgMANKLKl1WgYQf6QZ7mlge|fc_047bb49263dfa303016a58abf0fbf48198962579d6219fb823","name":"grep","arguments":{"i":"Finding PTY drain implementation","pattern":"drain\\(|take_reader\\(|drain_pty|spawn_drain","path":"crates/spt-daemon/src/broker.rs;crates/spt-term/src","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4029,"output":101,"cacheRead":178688,"cacheWrite":0,"totalTokens":182818,"reasoningTokens":21,"cost":{"input":0.020145000000000003,"output":0.00303,"cacheRead":0.08934399999999999,"cacheWrite":0,"total":0.112519}},"stopReason":"toolUse","timestamp":1784196073557,"responseId":"resp_047bb49263dfa303016a58abea15a88198aee9f39ff5dde16e","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58abf013d48198878bb0c352a77767","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKvwM7Ld9w7VMdDB45Z5oI_FD70uGb4FiO55mUMLmbP8BKhZHYb5PoM7IcfU_EZbbgul8K7_bcWm9fK5T7nfqKIMEQ2s2SIU4Mi0qrSypSWdOzT2rgsOoC4S6vohI6771kev4DjYP8ZrO9cVuyUjIqJrIyCb7sZkCJiFI62SQh8q4ek__ZEqpgiQ_CXmrLzrht_mEkpgG08GsUPp1uwJbbtjBvXrPD1euTmkFqPoZhURKL29Aqk-z2KiSnpNY3ai4T9pMAyXHrAM3lS60RIR0IY6aPWoXrm5nMm0cA-PuJyOjSwOCnmIHEX8o7HSqhNW8OrteoDyxSCsXWmVStaKAN2dHDzxEu6R9Qsou3q37D2ly-b9ucXt9BWZrBZIN0GNxUgn2ARbwGwBvqYjXqJAq41EYcw-aSqrX37Jb3CVXZEx1IgvCUEX--j-mowj1Rl7QVao33471EvOKyE1RWd2U5MehiB_BXu0zj5RWIzVCwFAfFCbaSGc3AX_uApKthvEct9AfqQS3lGfPZJqaUmzQU2j8r6l8fmXo5m7kP7XOyWC2sC1jWPoJ0cttlfsImNfxYe4_V6kn-wrMjgKVLGe7KtHyfuF-Ho0tFeMuDmtPdbXtVZ8HFIEcYaJXDz_lBfujDHmlPm9VwnsU1j-d5OlXOcoPuqeSssxlVMF-C-cSi8_mlmiOjCX5LdqWV1rxYOdTpbF5ttpl7mxvILp_-RgPI1iFcC5WLV3kvydoNlfUxSWVE57Gx-3kCJNHDq55xvkeAqKVY0cE0u11rnq65EylgRcqGZ8UtcdsdOvs_lY1dwxNOKEiKGAAfY6QIn00CrjqN4gJenM-EOLcNk6OvLqQDH4RtaC_Y12gGkTc8CPUG9ppDuym8utECPrvQkHTDtVFCAU_IfOMHA_LCa5qtA3qh-dphIFqjwRZjnli-qD1A3KKfxJkLSmPU0xjrPIH9PJuG-eEksAPz7TB3FP8-EYgsUS3k1AyDzem1VOpIcQIYHMJd2Kfqz_AR-ZdLDEoLEtZ1-NYNOmzB_p7qhjNEvjNVkuGw==","summary":[{"type":"summary_text","text":"**Searching for drain function**"}]},{"id":"fc_047bb49263dfa303016a58abf0fbf48198962579d6219fb823","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding PTY drain implementation\",\"pattern\":\"drain\\\\(|take_reader\\\\(|drain_pty|spawn_drain\",\"path\":\"crates/spt-daemon/src/broker.rs;crates/spt-term/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_rvgMANKLKl1WgYQf6QZ7mlge","name":"grep"}]},"duration":9310.570199999958,"ttft":6498.884200000204,"contextSnapshot":{"promptTokens":182717,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_rvgMANKLKl1WgYQf6QZ7mlge|fc_047bb49263dfa303016a58abf0fbf48198962579d6219fb823","toolName":"grep","startedAt":"2026-07-16T10:01:22.868Z","args":{"path":"crates/spt-daemon/src/broker.rs;crates/spt-term/src"},"intent":"Finding PTY drain implementation"},"id":"8f0c6f56","parentId":"f9ebefd2","timestamp":"2026-07-16T10:01:22.868Z"}
{"type":"message","id":"2b34f4a2","parentId":"8f0c6f56","timestamp":"2026-07-16T10:01:22.873Z","message":{"role":"toolResult","toolCallId":"call_rvgMANKLKl1WgYQf6QZ7mlge|fc_047bb49263dfa303016a58abf0fbf48198962579d6219fb823","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/src/\n### broker.rs\n 3342|        let drain = session\n*3343|            .drain(move |chunk| {\n 3344|                // append() does the controller handoff inline now (one non-blocking\n 3345|                // try_send — b4) and returns Some(epoch) ONLY when the controller is\n 3346|                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\n...\n 4808|    #[test]\n*4809|    fn a_full_controller_does_not_starve_a_viewer_or_block_the_drain() {\n 4810|        let mut log = OutputLog::new(1, 1024, String::new(), (24, 80));\n 4811|        log.controller_epoch.store(9, Ordering::Release);\n 4812|        // A CONTROLLER whose 2-slot channel we NEVER drain (rx held, unread) — a\n...\n 5013|    #[test]\n*5014|    fn input_enqueue_drops_and_stamps_on_full_then_heals_on_drain() {\n 5015|        const DEPTH: usize = 3;\n 5016|        // Consumer is the test; it does NOT drain until we say so → the queue fills.\n 5017|        let (w, rx) = test_input_writer(DEPTH);\n\n## spt-term/src/\n### projection.rs\n 244|    if turns.len() > n {\n*245|        turns.drain(..turns.len() - n);\n 246|    }\n 247|    Digest { turns }\n 248|}\n...\n 457|        }\n*458|        turns.drain(..cut);\n 459|    }\n 460|    while turns\n 461|        .first()\n### pty.rs\n 177|    /// thread in the hosted path; a test consumer otherwise).\n*178|    pub fn take_reader(&self) -> Result<Box<dyn Read + Send>, SurfaceError> {\n 179|        self.reader\n 180|            .lock()\n 181|            .unwrap()\n### reader.rs\n 173|    {\n*174|        let reader = self.take_reader()?;\n 175|        let writer = self.shared_writer();\n 176|        Ok(Drain::spawn(reader, writer, sink))\n 177|    }\n### stream.rs\n 51|    /// and the cooperative stop flag.\n*52|    pub fn drain(&self) -> &Drain {\n 53|        &self.drain\n 54|    }\n 55|\n...\n 73|        let (tx, rx) = sync_channel::<Vec<u8>>(capacity);\n*74|        let drain = self.drain(move |chunk| {\n 75|            // Bounded send: blocks when `capacity` chunks are unconsumed\n 76|            // (backpressure); drops silently once the receiver is gone so a\n 77|            // torn-down consumer never wedges the pump."}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs, crates/spt-term/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":9,"fileCount":5,"files":["crates/spt-daemon/src/broker.rs","crates/spt-term/src/projection.rs","crates/spt-term/src/pty.rs","crates/spt-term/src/reader.rs","crates/spt-term/src/stream.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":3},{"path":"crates/spt-term/src/projection.rs","count":2},{"path":"crates/spt-term/src/pty.rs","count":1},{"path":"crates/spt-term/src/reader.rs","count":1},{"path":"crates/spt-term/src/stream.rs","count":2}],"truncated":false,"displayContent":"# crates/\n\n## spt-daemon/src/\n### broker.rs\n 3342│        let drain = session\n*3343│            .drain(move |chunk| {\n 3344│                // append() does the controller handoff inline now (one non-blocking\n 3345│                // try_send — b4) and returns Some(epoch) ONLY when the controller is\n 3346│                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\n     │...\n 4808│    #[test]\n*4809│    fn a_full_controller_does_not_starve_a_viewer_or_block_the_drain() {\n 4810│        let mut log = OutputLog::new(1, 1024, String::new(), (24, 80));\n 4811│        log.controller_epoch.store(9, Ordering::Release);\n 4812│        // A CONTROLLER whose 2-slot channel we NEVER drain (rx held, unread) — a\n     │...\n 5013│    #[test]\n*5014│    fn input_enqueue_drops_and_stamps_on_full_then_heals_on_drain() {\n 5015│        const DEPTH: usize = 3;\n 5016│        // Consumer is the test; it does NOT drain until we say so → the queue fills.\n 5017│        let (w, rx) = test_input_writer(DEPTH);\n\n## spt-term/src/\n### projection.rs\n 244│    if turns.len() > n {\n*245│        turns.drain(..turns.len() - n);\n 246│    }\n 247│    Digest { turns }\n 248│}\n    │...\n 457│        }\n*458│        turns.drain(..cut);\n 459│    }\n 460│    while turns\n 461│        .first()\n### pty.rs\n 177│    /// thread in the hosted path; a test consumer otherwise).\n*178│    pub fn take_reader(&self) -> Result<Box<dyn Read + Send>, SurfaceError> {\n 179│        self.reader\n 180│            .lock()\n 181│            .unwrap()\n### reader.rs\n 173│    {\n*174│        let reader = self.take_reader()?;\n 175│        let writer = self.shared_writer();\n 176│        Ok(Drain::spawn(reader, writer, sink))\n 177│    }\n### stream.rs\n 51│    /// and the cooperative stop flag.\n*52│    pub fn drain(&self) -> &Drain {\n 53│        &self.drain\n 54│    }\n 55│\n   │...\n 73│        let (tx, rx) = sync_channel::<Vec<u8>>(capacity);\n*74│        let drain = self.drain(move |chunk| {\n 75│            // Bounded send: blocks when `capacity` chunks are unconsumed\n 76│            // (backpressure); drops silently once the receiver is gone so a\n 77│            // torn-down consumer never wedges the pump."},"isError":false,"timestamp":1784196082873}}
{"type":"message","id":"1ca67fc6","parentId":"2b34f4a2","timestamp":"2026-07-16T10:01:30.520Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting remote vs local attach paths**"},{"type":"toolCall","id":"call_wAd1pLDLxdOdtqxsHYWqBMVd|fc_047bb49263dfa303016a58abf75eb48198807a524829d973a6","name":"read","arguments":{"i":"Reading rc establish ownership","path":"crates/spt/src/rc.rs","selector":"1070-1285,1760-2028"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3756,"output":115,"cacheRead":179712,"cacheWrite":0,"totalTokens":183583,"reasoningTokens":68,"cost":{"input":0.01878,"output":0.00345,"cacheRead":0.08985599999999999,"cacheWrite":0,"total":0.11208599999999999}},"stopReason":"toolUse","timestamp":1784196082879,"responseId":"resp_047bb49263dfa303016a58abf357608198bd1f4c44ea3580d4","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58abf4fa448198bca120d98eff439e","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKv3xYl5-rLtH7hEokPkaVFckYJ6DyVCpUtzeJyub7HQQRd-OGAFJKJVvMTQjU9wk8rFAw-5YHpgWUps0iWEuUbWF-tkXC4DuUlT7ROL58wdZr6lVmcirOAqxT0B2YHM443F8yCHfbPQfGkhzSdCjDoTn4G-_FLcS6crvb_Qmf9Z4_UwhmzvpGGowjMtgXIfNS7wtUghFAodisH6_OinZU1T3SYd-wcYJT4lkPQ_3lCe9AojdCDjUjDB_AuAoSExYw5PlAGjSJ7HUxau_zAOcCLbZP8i6HZSrYSvgYwJMGCh6XmSftOkXqMqTF13gM6kPzTllpg2UTL22pNJhkClgY3l6RbbfRMA7ROGgKQuwdqHqqk8cQnL-jw7SP5BmC1pF4jO3qhIMX-U595Y9RWgnJZ9QFSkl4Z86rJqbgCOSvONX1v_JiuPbMVkH1saW4S4WJ5rEeWS5wEB7b_6JlNbHa6l389W64vho1pjO8popWsOM8bnLsVzxeba7v3An3_wk8RXMB6X75fEkAL9ZB1___JRt66aDOfleqUMiq5EemcbBIkjtXGwgl2ayYlz-AUwY6SxqNABkyf7O-YlEtUi5RpnP9tCFPte0i_yplpk8q_pTZneODELYmZ8rTXItpHbI4tuJ47wxr0GG0UNRfsfg3iE8HTGTMOm3zK7idKZDLmxN4NIIKzyfy0fbzRubnyZwoDZaD9YgTHL26X9ICscotYNM7ol1cJ9AM1C9uFtKX1JLWSA2zQ8IfD9pZ3nJ7gMgYmpO5o5QXoWkuWufkhVHCfmesy6JKsNYYI-4bOqHIXodVGcjd6VXNNys0HWnL8RRZNeJ1oQXjdTLXKbYR98jSo5MuQxT4Nqw2qsR-JCtIg9qlkcm4oizMIV-DKfndyN27LefVOsJvySvXHOjWDe7Owuux0i_CD707GGcS7CcmDUlQ5-FBkfeYBqbbx0fjUKQt6OU4RDOK8NZQB75hFNob2pJHyKgHaAZ7hPUr-hebH6elYRehrOcLDo_2yfcbPW1l1JMF_p7cqBPfx0l6B3KQMuP5s2ApHAthXMYMK2oq0QbHZeYfwhx9Covek26mtY-3U9qADpnZxlSlRTA8SCghu_85l3soIGD2CmYEMaYbmMH-FUSnpQAwhV4no3h2vBWdgjJuZMOtwZddxukJMH_8JPnS1TivD0oBRYJt5NenSZUiEt7OQizGJDDl_ZJ3_b_GgenoQYNiJ5TSRtKRl-lGAP0ZVTgTj_7nWsmIYDut2725TVAjh_Gxc2NHm12PU4axznatGHp14bXWXPptpj1bBJ2Z-XFvGws4tX5SvOAzYMJxw=","summary":[{"type":"summary_text","text":"**Inspecting remote vs local attach paths**"}]},{"id":"fc_047bb49263dfa303016a58abf75eb48198807a524829d973a6","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading rc establish ownership\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1070-1285,1760-2028\"}","call_id":"call_wAd1pLDLxdOdtqxsHYWqBMVd","name":"read"}]},"duration":7640.429500000551,"ttft":2107.655600000173,"contextSnapshot":{"promptTokens":183468,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_wAd1pLDLxdOdtqxsHYWqBMVd|fc_047bb49263dfa303016a58abf75eb48198807a524829d973a6","toolName":"read","startedAt":"2026-07-16T10:01:30.521Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading rc establish ownership"},"id":"c1a06bad","parentId":"1ca67fc6","timestamp":"2026-07-16T10:01:30.521Z"}
{"type":"message","id":"7c05b00f","parentId":"c1a06bad","timestamp":"2026-07-16T10:01:30.542Z","message":{"role":"toolResult","toolCallId":"call_wAd1pLDLxdOdtqxsHYWqBMVd|fc_047bb49263dfa303016a58abf75eb48198807a524829d973a6","toolName":"read","content":[{"type":"text","text":"fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {\n…\n    std::thread::spawn(move || {\n…\n        while let Ok(ev) = event::read() {\n            match ev {\n…\n            }\n        }\n    });\n}\n\n/// Attach a local terminal to the broker-held session for `endpoint_id` under\n/// `intent` (REQ-RCVIEW-1 / REQ-KICK-1): `Control` drives a free endpoint (and is\n/// busy-refused with guidance on a controlled one), `Viewer` watches read-only,\n/// `Take` kicks the incumbent controller (loud `Displaced` notice to them) and\n/// takes control. Returns when the session exits, the operator detaches, or an\n/// error occurs.\n// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\npub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, false)\n}\n\n/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\n/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\n/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\n/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND\n/// stamp), yet the confirmed live session IS attachable — \"attach to a live\n/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).\n/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a\n/// session that dies after confirmation, so this is not a blank-hang reopening.\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub fn run_attach_session_confirmed(\n    endpoint_id: &str,\n    intent: AttachIntent,\n) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, true)\n}\n\nfn run_attach_inner(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    session_confirmed: bool,\n) -> Result<(), String> {\n    let view = intent == AttachIntent::Viewer;\n    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\n    // an endpoint a DIFFERENT node already controls gets actionable guidance here\n    // (the broker would also refuse on the wire — this is the friendly path). A\n    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\n    if intent == AttachIntent::Control {\n        if let Some(node) = current_driver(endpoint_id) {\n            println!(\"'{endpoint_id}' is controlled by {node}.\");\n            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\n            println!(\"  spt rc {endpoint_id} --take   to take control (kicks {node})\");\n            return Ok(());\n        }\n    }\n    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\n    // `status=offline` is not attachable — short-circuit with clean guidance\n    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\n    // instead of the broker dance that would block on a dead session (the\n    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\n    // spt-hosted perch offline, so this catches the common case before any IPC. An\n    // ABSENT or status-less perch falls through to the broker path, which already\n    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\n    // is NOT used here: it returns true for an absent info.json by interim parity.)\n    //\n    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\n    // NOT on STATUS_ONLINE (REQ-ENDPOINT-UNBOUND-ATTACH). A STATUS_UNBOUND skeleton\n    // (session spawned, perch not yet bound) is `!= offline`, so it falls through\n    // here and attaches via the live-session resolve below — the headless /\n    // clear-a-bind-prompt path. `offline` is stamped only on session DEATH, so it\n    // never co-occurs with a live session.\n    //\n    // KNOWN BOUNDARY (doyle, W4): a RESUMING perch is still `offline` during its\n    // pre-bind window — the W2 skeleton-write short-circuits on an existing perch,\n    // so resume gets NO UNBOUND stamp. A standalone `spt rc <id>` against it hits\n    // this failfast (\"offline — start it\") rather than waiting for the bind, and\n    // retries fine once the bind flips the perch online. The `endpoint run\n    // --attach` path does NOT hit it: it arrives via run_attach_session_confirmed\n    // (session already proven live by await_endpoint_session), so the gate is\n    // SKIPPED — it attaches to the confirmed live session even while the perch is\n    // still stamped offline mid-rebringup. Accepted, not contorted around.\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    let perch_path =\n        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);\n    if !session_confirmed\n        && spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.status)\n            .as_deref()\n            == Some(spt_store::liveness::STATUS_OFFLINE)\n    {\n        println!(\"Endpoint '{endpoint_id}' is offline — nothing to attach to.\");\n        println!(\"  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it\");\n        return Ok(());\n    }\n    // Establish the attach (broker conn → session resolve → dial → attach-open →\n    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\n    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\n    // keeps today's clean-exit copy on a no-target miss.\n    let est = match establish_attach(endpoint_id, intent) {\n        Ok(est) => est,\n        Err(EstablishFail::NoTarget(msg)) => {\n            println!(\"{msg}\");\n            return Ok(());\n        }\n        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\n        // rc won't birth one. Plain operator language — what's wrong, what to do —\n        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\n        // [impl->REQ-RC-RECONNECT-TRUTH]\n        Err(EstablishFail::DaemonDown) => {\n            println!(\n                \"The spt daemon isn't running — nothing to attach to. \\\n                 Start it with `spt daemon start`, then `spt rc {endpoint_id}`.\"\n            );\n            return Ok(());\n        }\n        Err(EstablishFail::Error(e)) => return Err(e),\n    };\n    let mut remote_node: Option<String> = est.remote_node.clone();\n\n    let _raw = RawGuard::enable();\n    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n    // pump and read by the Windows stdin reader to gate scroll forwarding\n    // (REQ-RC-MOUSE-FORWARD).\n    let mouse_mode = std::sync::Arc::new(MouseMode::default());\n    let rx = if view {\n        None\n    } else {\n        let (tx, rx) = mpsc::channel();\n        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\n        Some(rx)\n    };\n\n    // Controller status row (REQ-RC-IDENTITY): the operator-side identity banner\n    // on the real terminal's reserved row 1 — controller only (a viewer's viewport\n    // is read-only and may letterbox, so it owns no row). Resolved ONCE here.\n    // [impl->REQ-RC-IDMARKER-DISABLE]\n    let mut status = if status_row_active(view) {\n        let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n        Some(StatusRow {\n            text: resolve_identity_line(endpoint_id),\n            rows,\n            cols,\n        })\n    } else {\n        None\n    };\n\n    let mut stdout = std::io::stdout();\n    let outcome = attach_viewport(\n        endpoint_id,\n        intent,\n        view,\n        est,\n        rx.as_ref(),\n        &mouse_mode,\n        status.as_mut(),\n        &mut remote_node,\n        &mut stdout,\n    );\n\n    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\n    // leaving raw mode, on every pump-exit path (REQ-RC-IDENTITY).\n    if status.is_some() {\n        let _ = stdout.write_all(&StatusRow::restore_bytes());\n        let _ = stdout.flush();\n    }\n    // Out of raw mode (guard drops after) before the parting line.\n    drop(_raw);\n    match outcome {\n        Ok(PumpEnd::Exited(code)) => {\n            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Detached) => {\n            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Displaced(by)) => {\n            // The loud kick notice (REQ-KICK-1): a different operator took control.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\n            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\n            // successful re-attach. Plain operator language (Q4 UX rule): what\n            // happened, what it means, what to do next — never op/read-err lingo.\n            if daemon_down {\n                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\n                // went down and never came back — rc did NOT resurrect it. Name that\n                // plainly so the operator restarts the daemon rather than guessing.\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\n                     didn't come back within {}s. rc won't auto-start it. Bring it \\\n                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            } else {\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\n                     within {}s — the session may still be running. Try `spt rc \\\n                     {endpoint_id}` again once the node/daemon is back]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            }\n            Ok(())\n        }\n        // Reconnectable ends never escape the loop (they enter the retry arm and\n        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n        // so a future PumpEnd variant still forces a decision here.\n        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n        }\n        Ok(PumpEnd::Stalled) => {\n            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced\n            // no output at all — a dead/wedged session, not a slow one. A clear\n…\n        }\n…\n    }\n}\n…\nenum PumpEnd {\n…\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n    NoLiveSession,\n}\n\n/// Generous first-event backstop for the attach pump (REQ-HAZARD-RC-ATTACH-FAILFAST\n/// path b): if NO broker event arrives within this window after attach, the\n/// session is treated as dead/wedged. Long on purpose — a fresh, mid-init alive\n/// session (empty OutputLog, still painting) MUST pass, so this only catches a\n/// true hang, never the working fresh-attach.\nconst FIRST_EVENT_GRACE: Duration = Duration::from_secs(30);\n\n/// The path-b stall decision (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach has\n/// stalled iff NO broker event has ever arrived AND the generous grace has\n/// elapsed. Pulled out so the decision is unit-testable without a 30s pump run.\n// [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\nfn first_event_stalled(seen_any: bool, elapsed: Duration) -> bool {\n    !seen_any && elapsed >= FIRST_EVENT_GRACE\n}\n\n/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ReadDisposition {\n    /// Transient poll-slice timeout — loop and poll again.\n    Retry,\n    /// Severed broker stream (broker bounced / hung up) — recoverable, surface\n    /// gracefully.\n    BrokerGone,\n    /// A genuine read failure — propagate.\n    Fatal,\n}\n\n/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\n/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\n/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\n/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\n/// gracefully rather than crashing the PTY with the raw io text (the same EOF\n/// family the v0.9.1 seed fix handles); anything else is fatal.\n// [impl->REQ-HAZARD-RC-EOF]\nfn classify_read_err(kind: std::io::ErrorKind) -> ReadDisposition {\n    use std::io::ErrorKind::*;\n    match kind {\n        WouldBlock | TimedOut => ReadDisposition::Retry,\n        UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {\n            ReadDisposition::BrokerGone\n        }\n        _ => ReadDisposition::Fatal,\n    }\n}\n\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n#[allow(clippy::too_many_arguments)]\nfn pump(\n    brain: &mut Brain,\n    stream_id: u64,\n    ops: &OpMinter,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    decoder: &mut AttachDecoder,\n    stdout: &mut impl std::io::Write,\n    controller: bool,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n) -> Result<PumpEnd, String> {\n    // Track the harness's mouse-reporting mode from its output DECSET sequences so\n    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The\n    // scanner is pump-local (single producer) and survives a sequence split across\n    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.\n    let mut mouse_scanner = MouseModeScanner::default();\n    // The reserved-row re-assert scanner (REQ-RC-IDENTITY): watches the harness\n    // output for an alt-screen enter / DECSTBM reset that would destroy our\n    // scroll-region margin, so we can re-paint the status row. Pump-local, single\n    // producer, survives a trigger split across output chunks (like mouse_scanner).\n    let mut reassert_scanner = ReassertScanner::default();\n    // Reserve the top row + paint the identity banner up front (REQ-RC-IDENTITY).\n    // Controller-only (the caller passes `Some` only for a control attach).\n    if let Some(status) = status.as_deref_mut() {\n        stdout\n            .write_all(&status.assert_bytes())\n            .map_err(|e| format!(\"status assert: {e}\"))?;\n        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\n    }\n    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n    // (the session outlives the viewport). Best-effort: an already-finished or\n    // exited stream send is harmless to attempt.\n    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\n        let _ = brain.net_stream_send(stream_id, &[], None, true);\n        PumpEnd::Detached\n    }\n    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n    // drop any `seq` already rendered (the operator render cursor, matching the\n    // local `OutputLog` replay discipline).\n    let mut cursor = 0u64;\n    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size\n    // each slice and forward a Resize when it changes (cross-platform without\n    // hooking SIGWINCH / the crossterm event loop, which would conflict with the\n    // raw-stdin reader). `None` until the first read.\n    let mut last_size: Option<(u16, u16)> = None;\n    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown\n    // as a one-line indicator when it differs from our own terminal.\n    let mut pty_size: Option<(u16, u16)> = None;\n    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged\n    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE\n    // rather than an infinite blank. Any broker event clears it.\n    let started = Instant::now();\n    let mut seen_any = false;\n    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\n    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n    // Any live session replays its buffered ring on attach, so a real attach sets\n    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\n    let mut rendered_any = false;\n    loop {\n        // ── controller window-change → resize ──────────────────────────────\n        if controller {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                if last_size != Some((rows, cols)) {\n                    last_size = Some((rows, cols));\n                    match status.as_deref_mut() {\n                        // Status active: the harness PTY owns rows-1 (we keep row 1\n                        // for the banner), and we re-assert the margin + repaint on\n                        // the new size (REQ-RC-IDENTITY). Never resize the PTY to 0.\n                        Some(status) => {\n                            status.update_size(rows, cols);\n                            let pty_rows = rows.saturating_sub(1).max(1);\n                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\n                            if stdout.write_all(&status.assert_bytes()).is_ok() {\n                                let _ = stdout.flush();\n                            }\n                        }\n                        None => {\n                            let _ = send_attach_resize(brain, stream_id, rows, cols);\n                        }\n                    }\n                }\n            }\n        }\n\n        // ── stdin → session ────────────────────────────────────────────────\n        if let Some(rx) = rx {\n            loop {\n                match rx.try_recv() {\n                    Ok(StdinMsg::Bytes(bytes)) => {\n                        send_attach_input(brain, stream_id, &bytes, ops.next())\n                            .map_err(|e| format!(\"send input: {e}\"))?;\n                    }\n                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\n                    Err(mpsc::TryRecvError::Empty) => break,\n                    Err(mpsc::TryRecvError::Disconnected) => {\n                        return Ok(detach(brain, stream_id))\n                    }\n                }\n            }\n        }\n\n        // ── session → terminal ─────────────────────────────────────────────\n        let ev = match brain.read_event_until(Some(Instant::now() + POLL_SLICE)) {\n            Ok(ev) => ev,\n            Err(e) => match classify_read_err(e.kind()) {\n                // A poll-slice timeout: no event this slice. Before looping, the\n                // first-event backstop — if NOTHING has ever arrived past the\n                // generous grace, the attach is dead/wedged (path b).\n                ReadDisposition::Retry => {\n                    if first_event_stalled(seen_any, started.elapsed()) {\n                        return Ok(PumpEnd::Stalled);\n                    }\n                    continue;\n                }\n                // A severed broker stream (broker bounce / hang-up) — surface it\n                // gracefully, never as a raw RC_FAIL crash (REQ-HAZARD-RC-EOF).\n                ReadDisposition::BrokerGone => return Ok(PumpEnd::BrokerGone),\n                ReadDisposition::Fatal => return Err(format!(\"read event: {e}\")),\n            },\n        };\n        seen_any = true; // any broker event proves the attach stream is live\n        match ev {\n            BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Output { seq, data_b64 } => {\n                            if seq < cursor {\n                                continue; // already rendered — re-serve dedup\n                            }\n                            cursor = seq + 1;\n                            rendered_any = true;\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\n                            // Track the harness's mouse-reporting mode from its output\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\n                            mouse_scanner.feed(mouse_mode, &bytes);\n                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\n                            // Re-assert the reserved row if the harness output just\n                            // destroyed our scroll region — alt-screen enter or a\n                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\n                            // margin is gone (alt-screen has its own full-screen\n                            // margins; a DECSTBM reset clears ours), so re-set the\n                            // MARGIN + repaint (assert_bytes), not just the text —\n                            // else the harness scrolls back over row 1. Scanned on the\n                            // SAME bytes we rendered, after writing them.\n                            if let Some(status) = status.as_deref_mut() {\n                                if reassert_scanner.feed(&bytes) {\n                                    stdout\n                                        .write_all(&status.assert_bytes())\n                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\n                                }\n                            }\n                            stdout.flush().map_err(|e| format!(\"flush: {e}\"))?;\n                        }\n                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\n                        AttachRecord::Displaced { by } => {\n                            return Ok(PumpEnd::Displaced(by));\n                        }\n                        // Viewer letterbox feed (REQ-RCVIEW-1): on a size change,\n                        // show a one-line indicator when our terminal differs from\n                        // the new PTY size (rc is a byte pump — true clip/pad needs\n                        // a grid model, deferred; the honest size cue renders here).\n                        AttachRecord::Size { rows, cols } if pty_size != Some((rows, cols)) => {\n                            pty_size = Some((rows, cols));\n                            match crossterm::terminal::size() {\n                                Ok((mycols, myrows)) if (myrows, mycols) != (rows, cols) => {\n                                    let _ = write!(\n                                        stdout,\n                                        \"\\r\\n[viewing {cols}x{rows} — your terminal is {mycols}x{myrows}]\\r\\n\"\n                                    );\n                                    let _ = stdout.flush();\n                                }\n                                _ => {}\n                            }\n                        }\n                        _ => {} // Request/Input echoed back: protocol noise, ignore.\n                    }\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                return Ok(serve_eof_end(rendered_any));\n            }\n            BrokerEvent::Error { message } => return Err(message),\n            _ => {} // acks + unrelated events: not ours.\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn parse(armed: &mut bool, input: &[u8]) -> (Vec<u8>, bool) {\n        let p = parse_stdin_chunk(armed, input);\n        (p.forward, p.detach)\n    }\n\n    // [unit->REQ-RC-WIN-VT-OUTPUT] the rc attach path composes the OUTPUT console\n    // mode that renders ANSI escapes instead of printing them literally (#12): it\n    // adds VT + processed-output, preserves the prior bits, and is idempotent.\n    #[cfg(windows)]\n    #[test]\n    fn with_vt_output_enables_virtual_terminal_processing() {\n        use windows_sys::Win32::System::Console::{\n            ENABLE_PROCESSED_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WRAP_AT_EOL_OUTPUT,\n        };\n        let prior = ENABLE_WRAP_AT_EOL_OUTPUT;\n        let got = with_vt_output(prior);\n        assert!(\n            got & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0,\n            \"VT-output processing enabled\"\n        );\n        assert!(got & ENABLE_PROCESSED_OUTPUT != 0, \"processed output enabled\");\n        assert!(\n            got & ENABLE_WRAP_AT_EOL_OUTPUT != 0,\n            \"prior console bits preserved\"\n        );\n…\n    }\n…\n}"}],"details":{"displayContent":{"text":"fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {\n…\n    std::thread::spawn(move || {\n…\n        while let Ok(ev) = event::read() {\n            match ev {\n…\n            }\n        }\n    });\n}\n\n/// Attach a local terminal to the broker-held session for `endpoint_id` under\n/// `intent` (REQ-RCVIEW-1 / REQ-KICK-1): `Control` drives a free endpoint (and is\n/// busy-refused with guidance on a controlled one), `Viewer` watches read-only,\n/// `Take` kicks the incumbent controller (loud `Displaced` notice to them) and\n/// takes control. Returns when the session exits, the operator detaches, or an\n/// error occurs.\n// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\npub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, false)\n}\n\n/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\n/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\n/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\n/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND\n/// stamp), yet the confirmed live session IS attachable — \"attach to a live\n/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).\n/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a\n/// session that dies after confirmation, so this is not a blank-hang reopening.\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub fn run_attach_session_confirmed(\n    endpoint_id: &str,\n    intent: AttachIntent,\n) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, true)\n}\n\nfn run_attach_inner(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    session_confirmed: bool,\n) -> Result<(), String> {\n    let view = intent == AttachIntent::Viewer;\n    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\n    // an endpoint a DIFFERENT node already controls gets actionable guidance here\n    // (the broker would also refuse on the wire — this is the friendly path). A\n    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\n    if intent == AttachIntent::Control {\n        if let Some(node) = current_driver(endpoint_id) {\n            println!(\"'{endpoint_id}' is controlled by {node}.\");\n            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\n            println!(\"  spt rc {endpoint_id} --take   to take control (kicks {node})\");\n            return Ok(());\n        }\n    }\n    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\n    // `status=offline` is not attachable — short-circuit with clean guidance\n    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\n    // instead of the broker dance that would block on a dead session (the\n    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\n    // spt-hosted perch offline, so this catches the common case before any IPC. An\n    // ABSENT or status-less perch falls through to the broker path, which already\n    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\n    // is NOT used here: it returns true for an absent info.json by interim parity.)\n    //\n    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\n    // NOT on STATUS_ONLINE (REQ-ENDPOINT-UNBOUND-ATTACH). A STATUS_UNBOUND skeleton\n    // (session spawned, perch not yet bound) is `!= offline`, so it falls through\n    // here and attaches via the live-session resolve below — the headless /\n    // clear-a-bind-prompt path. `offline` is stamped only on session DEATH, so it\n    // never co-occurs with a live session.\n    //\n    // KNOWN BOUNDARY (doyle, W4): a RESUMING perch is still `offline` during its\n    // pre-bind window — the W2 skeleton-write short-circuits on an existing perch,\n    // so resume gets NO UNBOUND stamp. A standalone `spt rc <id>` against it hits\n    // this failfast (\"offline — start it\") rather than waiting for the bind, and\n    // retries fine once the bind flips the perch online. The `endpoint run\n    // --attach` path does NOT hit it: it arrives via run_attach_session_confirmed\n    // (session already proven live by await_endpoint_session), so the gate is\n    // SKIPPED — it attaches to the confirmed live session even while the perch is\n    // still stamped offline mid-rebringup. Accepted, not contorted around.\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    let perch_path =\n        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);\n    if !session_confirmed\n        && spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.status)\n            .as_deref()\n            == Some(spt_store::liveness::STATUS_OFFLINE)\n    {\n        println!(\"Endpoint '{endpoint_id}' is offline — nothing to attach to.\");\n        println!(\"  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it\");\n        return Ok(());\n    }\n    // Establish the attach (broker conn → session resolve → dial → attach-open →\n    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\n    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\n    // keeps today's clean-exit copy on a no-target miss.\n    let est = match establish_attach(endpoint_id, intent) {\n        Ok(est) => est,\n        Err(EstablishFail::NoTarget(msg)) => {\n            println!(\"{msg}\");\n            return Ok(());\n        }\n        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\n        // rc won't birth one. Plain operator language — what's wrong, what to do —\n        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\n        // [impl->REQ-RC-RECONNECT-TRUTH]\n        Err(EstablishFail::DaemonDown) => {\n            println!(\n                \"The spt daemon isn't running — nothing to attach to. \\\n                 Start it with `spt daemon start`, then `spt rc {endpoint_id}`.\"\n            );\n            return Ok(());\n        }\n        Err(EstablishFail::Error(e)) => return Err(e),\n    };\n    let mut remote_node: Option<String> = est.remote_node.clone();\n\n    let _raw = RawGuard::enable();\n    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n    // pump and read by the Windows stdin reader to gate scroll forwarding\n    // (REQ-RC-MOUSE-FORWARD).\n    let mouse_mode = std::sync::Arc::new(MouseMode::default());\n    let rx = if view {\n        None\n    } else {\n        let (tx, rx) = mpsc::channel();\n        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\n        Some(rx)\n    };\n\n    // Controller status row (REQ-RC-IDENTITY): the operator-side identity banner\n    // on the real terminal's reserved row 1 — controller only (a viewer's viewport\n    // is read-only and may letterbox, so it owns no row). Resolved ONCE here.\n    // [impl->REQ-RC-IDMARKER-DISABLE]\n    let mut status = if status_row_active(view) {\n        let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n        Some(StatusRow {\n            text: resolve_identity_line(endpoint_id),\n            rows,\n            cols,\n        })\n    } else {\n        None\n    };\n\n    let mut stdout = std::io::stdout();\n    let outcome = attach_viewport(\n        endpoint_id,\n        intent,\n        view,\n        est,\n        rx.as_ref(),\n        &mouse_mode,\n        status.as_mut(),\n        &mut remote_node,\n        &mut stdout,\n    );\n\n    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\n    // leaving raw mode, on every pump-exit path (REQ-RC-IDENTITY).\n    if status.is_some() {\n        let _ = stdout.write_all(&StatusRow::restore_bytes());\n        let _ = stdout.flush();\n    }\n    // Out of raw mode (guard drops after) before the parting line.\n    drop(_raw);\n    match outcome {\n        Ok(PumpEnd::Exited(code)) => {\n            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Detached) => {\n            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Displaced(by)) => {\n            // The loud kick notice (REQ-KICK-1): a different operator took control.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\n            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\n            // successful re-attach. Plain operator language (Q4 UX rule): what\n            // happened, what it means, what to do next — never op/read-err lingo.\n            if daemon_down {\n                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\n                // went down and never came back — rc did NOT resurrect it. Name that\n                // plainly so the operator restarts the daemon rather than guessing.\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\n                     didn't come back within {}s. rc won't auto-start it. Bring it \\\n                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            } else {\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\n                     within {}s — the session may still be running. Try `spt rc \\\n                     {endpoint_id}` again once the node/daemon is back]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            }\n            Ok(())\n        }\n        // Reconnectable ends never escape the loop (they enter the retry arm and\n        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n        // so a future PumpEnd variant still forces a decision here.\n        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n        }\n        Ok(PumpEnd::Stalled) => {\n            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced\n            // no output at all — a dead/wedged session, not a slow one. A clear\n…\n        }\n…\n    }\n}\n…\nenum PumpEnd {\n…\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n    NoLiveSession,\n}\n\n/// Generous first-event backstop for the attach pump (REQ-HAZARD-RC-ATTACH-FAILFAST\n/// path b): if NO broker event arrives within this window after attach, the\n/// session is treated as dead/wedged. Long on purpose — a fresh, mid-init alive\n/// session (empty OutputLog, still painting) MUST pass, so this only catches a\n/// true hang, never the working fresh-attach.\nconst FIRST_EVENT_GRACE: Duration = Duration::from_secs(30);\n\n/// The path-b stall decision (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach has\n/// stalled iff NO broker event has ever arrived AND the generous grace has\n/// elapsed. Pulled out so the decision is unit-testable without a 30s pump run.\n// [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\nfn first_event_stalled(seen_any: bool, elapsed: Duration) -> bool {\n    !seen_any && elapsed >= FIRST_EVENT_GRACE\n}\n\n/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ReadDisposition {\n    /// Transient poll-slice timeout — loop and poll again.\n    Retry,\n    /// Severed broker stream (broker bounced / hung up) — recoverable, surface\n    /// gracefully.\n    BrokerGone,\n    /// A genuine read failure — propagate.\n    Fatal,\n}\n\n/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\n/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\n/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\n/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\n/// gracefully rather than crashing the PTY with the raw io text (the same EOF\n/// family the v0.9.1 seed fix handles); anything else is fatal.\n// [impl->REQ-HAZARD-RC-EOF]\nfn classify_read_err(kind: std::io::ErrorKind) -> ReadDisposition {\n    use std::io::ErrorKind::*;\n    match kind {\n        WouldBlock | TimedOut => ReadDisposition::Retry,\n        UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {\n            ReadDisposition::BrokerGone\n        }\n        _ => ReadDisposition::Fatal,\n    }\n}\n\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n#[allow(clippy::too_many_arguments)]\nfn pump(\n    brain: &mut Brain,\n    stream_id: u64,\n    ops: &OpMinter,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    decoder: &mut AttachDecoder,\n    stdout: &mut impl std::io::Write,\n    controller: bool,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n) -> Result<PumpEnd, String> {\n    // Track the harness's mouse-reporting mode from its output DECSET sequences so\n    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The\n    // scanner is pump-local (single producer) and survives a sequence split across\n    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.\n    let mut mouse_scanner = MouseModeScanner::default();\n    // The reserved-row re-assert scanner (REQ-RC-IDENTITY): watches the harness\n    // output for an alt-screen enter / DECSTBM reset that would destroy our\n    // scroll-region margin, so we can re-paint the status row. Pump-local, single\n    // producer, survives a trigger split across output chunks (like mouse_scanner).\n    let mut reassert_scanner = ReassertScanner::default();\n    // Reserve the top row + paint the identity banner up front (REQ-RC-IDENTITY).\n    // Controller-only (the caller passes `Some` only for a control attach).\n    if let Some(status) = status.as_deref_mut() {\n        stdout\n            .write_all(&status.assert_bytes())\n            .map_err(|e| format!(\"status assert: {e}\"))?;\n        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\n    }\n    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n    // (the session outlives the viewport). Best-effort: an already-finished or\n    // exited stream send is harmless to attempt.\n    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\n        let _ = brain.net_stream_send(stream_id, &[], None, true);\n        PumpEnd::Detached\n    }\n    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n    // drop any `seq` already rendered (the operator render cursor, matching the\n    // local `OutputLog` replay discipline).\n    let mut cursor = 0u64;\n    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size\n    // each slice and forward a Resize when it changes (cross-platform without\n    // hooking SIGWINCH / the crossterm event loop, which would conflict with the\n    // raw-stdin reader). `None` until the first read.\n    let mut last_size: Option<(u16, u16)> = None;\n    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown\n    // as a one-line indicator when it differs from our own terminal.\n    let mut pty_size: Option<(u16, u16)> = None;\n    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged\n    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE\n    // rather than an infinite blank. Any broker event clears it.\n    let started = Instant::now();\n    let mut seen_any = false;\n    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\n    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n    // Any live session replays its buffered ring on attach, so a real attach sets\n    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\n    let mut rendered_any = false;\n    loop {\n        // ── controller window-change → resize ──────────────────────────────\n        if controller {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                if last_size != Some((rows, cols)) {\n                    last_size = Some((rows, cols));\n                    match status.as_deref_mut() {\n                        // Status active: the harness PTY owns rows-1 (we keep row 1\n                        // for the banner), and we re-assert the margin + repaint on\n                        // the new size (REQ-RC-IDENTITY). Never resize the PTY to 0.\n                        Some(status) => {\n                            status.update_size(rows, cols);\n                            let pty_rows = rows.saturating_sub(1).max(1);\n                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\n                            if stdout.write_all(&status.assert_bytes()).is_ok() {\n                                let _ = stdout.flush();\n                            }\n                        }\n                        None => {\n                            let _ = send_attach_resize(brain, stream_id, rows, cols);\n                        }\n                    }\n                }\n            }\n        }\n\n        // ── stdin → session ────────────────────────────────────────────────\n        if let Some(rx) = rx {\n            loop {\n                match rx.try_recv() {\n                    Ok(StdinMsg::Bytes(bytes)) => {\n                        send_attach_input(brain, stream_id, &bytes, ops.next())\n                            .map_err(|e| format!(\"send input: {e}\"))?;\n                    }\n                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\n                    Err(mpsc::TryRecvError::Empty) => break,\n                    Err(mpsc::TryRecvError::Disconnected) => {\n                        return Ok(detach(brain, stream_id))\n                    }\n                }\n            }\n        }\n\n        // ── session → terminal ─────────────────────────────────────────────\n        let ev = match brain.read_event_until(Some(Instant::now() + POLL_SLICE)) {\n            Ok(ev) => ev,\n            Err(e) => match classify_read_err(e.kind()) {\n                // A poll-slice timeout: no event this slice. Before looping, the\n                // first-event backstop — if NOTHING has ever arrived past the\n                // generous grace, the attach is dead/wedged (path b).\n                ReadDisposition::Retry => {\n                    if first_event_stalled(seen_any, started.elapsed()) {\n                        return Ok(PumpEnd::Stalled);\n                    }\n                    continue;\n                }\n                // A severed broker stream (broker bounce / hang-up) — surface it\n                // gracefully, never as a raw RC_FAIL crash (REQ-HAZARD-RC-EOF).\n                ReadDisposition::BrokerGone => return Ok(PumpEnd::BrokerGone),\n                ReadDisposition::Fatal => return Err(format!(\"read event: {e}\")),\n            },\n        };\n        seen_any = true; // any broker event proves the attach stream is live\n        match ev {\n            BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Output { seq, data_b64 } => {\n                            if seq < cursor {\n                                continue; // already rendered — re-serve dedup\n                            }\n                            cursor = seq + 1;\n                            rendered_any = true;\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\n                            // Track the harness's mouse-reporting mode from its output\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\n                            mouse_scanner.feed(mouse_mode, &bytes);\n                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\n                            // Re-assert the reserved row if the harness output just\n                            // destroyed our scroll region — alt-screen enter or a\n                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\n                            // margin is gone (alt-screen has its own full-screen\n                            // margins; a DECSTBM reset clears ours), so re-set the\n                            // MARGIN + repaint (assert_bytes), not just the text —\n                            // else the harness scrolls back over row 1. Scanned on the\n                            // SAME bytes we rendered, after writing them.\n                            if let Some(status) = status.as_deref_mut() {\n                                if reassert_scanner.feed(&bytes) {\n                                    stdout\n                                        .write_all(&status.assert_bytes())\n                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\n                                }\n                            }\n                            stdout.flush().map_err(|e| format!(\"flush: {e}\"))?;\n                        }\n                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\n                        AttachRecord::Displaced { by } => {\n                            return Ok(PumpEnd::Displaced(by));\n                        }\n                        // Viewer letterbox feed (REQ-RCVIEW-1): on a size change,\n                        // show a one-line indicator when our terminal differs from\n                        // the new PTY size (rc is a byte pump — true clip/pad needs\n                        // a grid model, deferred; the honest size cue renders here).\n                        AttachRecord::Size { rows, cols } if pty_size != Some((rows, cols)) => {\n                            pty_size = Some((rows, cols));\n                            match crossterm::terminal::size() {\n                                Ok((mycols, myrows)) if (myrows, mycols) != (rows, cols) => {\n                                    let _ = write!(\n                                        stdout,\n                                        \"\\r\\n[viewing {cols}x{rows} — your terminal is {mycols}x{myrows}]\\r\\n\"\n                                    );\n                                    let _ = stdout.flush();\n                                }\n                                _ => {}\n                            }\n                        }\n                        _ => {} // Request/Input echoed back: protocol noise, ignore.\n                    }\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                return Ok(serve_eof_end(rendered_any));\n            }\n            BrokerEvent::Error { message } => return Err(message),\n            _ => {} // acks + unrelated events: not ours.\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn parse(armed: &mut bool, input: &[u8]) -> (Vec<u8>, bool) {\n        let p = parse_stdin_chunk(armed, input);\n        (p.forward, p.detach)\n    }\n\n    // [unit->REQ-RC-WIN-VT-OUTPUT] the rc attach path composes the OUTPUT console\n    // mode that renders ANSI escapes instead of printing them literally (#12): it\n    // adds VT + processed-output, preserves the prior bits, and is idempotent.\n    #[cfg(windows)]\n    #[test]\n    fn with_vt_output_enables_virtual_terminal_processing() {\n        use windows_sys::Win32::System::Console::{\n            ENABLE_PROCESSED_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WRAP_AT_EOL_OUTPUT,\n        };\n        let prior = ENABLE_WRAP_AT_EOL_OUTPUT;\n        let got = with_vt_output(prior);\n        assert!(\n            got & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0,\n            \"VT-output processing enabled\"\n        );\n        assert!(got & ENABLE_PROCESSED_OUTPUT != 0, \"processed output enabled\");\n        assert!(\n            got & ENABLE_WRAP_AT_EOL_OUTPUT != 0,\n            \"prior console bits preserved\"\n        );\n…\n    }\n…\n}","startLine":1015,"lineNumbers":[1015,null,1018,null,1020,1021,null,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,null,1294,null,1314,1315,null,1720,null,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,null,2031,null,3213]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"}}},"isError":false,"timestamp":1784196090542}}
{"type":"message","id":"e4c8a464","parentId":"7c05b00f","timestamp":"2026-07-16T10:01:37.153Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Searching for establish_attach function**"},{"type":"toolCall","id":"call_B8NRebGMlqM4w3ah8FMYDoAQ|fc_047bb49263dfa303016a58abffc67881989e72c2b3c4fb7436","name":"grep","arguments":{"i":"Locating rc establishment function","pattern":"fn establish_attach|struct EstablishedAttach|net_dial_loopback|request_attach_endpoint|net_stream_subscribe","path":"crates/spt/src/rc.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":8542,"output":90,"cacheRead":180736,"cacheWrite":0,"totalTokens":189368,"reasoningTokens":13,"cost":{"input":0.042710000000000005,"output":0.0027,"cacheRead":0.09036799999999999,"cacheWrite":0,"total":0.135778}},"stopReason":"toolUse","timestamp":1784196090586,"responseId":"resp_047bb49263dfa303016a58abfafe9c81989f5b17336cecabb0","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58abfefff8819896cb16258dbe4892","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKv_Pqzg4H3YR-0OQbbw_mBAQ1yk9hFoWFv42_P4A_YbpYfdJwRBBHbuXomGqYWjkQdySCltu7Ue-RoUHjOd6smDoCrn_kIcqZ9kv-fZ2d76iPhcH1VD2Ga2BfS6K1PHlNTtm6K0IqT1N1o2U85kebpX8sBKHtsDBjEHc6HdviBwc2Z5q1jHuhh1WiImxa9k6I5xCJappVvUfpAGlx_HKKi2HhJ0r9vY23xxBd2oIwN_tM3WXLOgZFwvxWvADsZdA5S7anbOctAaaWBmq00NB_VOKkeSS0JdODyrImr-wj4TUwb3t_K2ZVQyej8fH2SX3v0y14D_3u0rAlPz1wn7FwBK8dI8mqaQjm1lHpfaAj09dvVCDc_ThHAPD_7j-2CRh70vRPrjBXXEoneeyAW7HIot9kIkpVVloLu4DoyCikaqEoA9WtQh64ifJTrvFpty5wR3Ak887ECQnognWgpcfaqTJ8JbUej0rnGZDooOJLYdcCgitxITsHsPjcAIrfjrnJsVuomYaEXO0OS3jbHz_MfBYuos1W63_CYjUmHeDBcSR-I8jCi-KR07KEZ1oZwmYH39ayWnr_udGIYyuiBdfRR24cpqtD-OcixSi3pjvoplbtFRqF-SjarGIq-pGJfrxfrrIT2dQFIdZaqVMHTcU0RcZqvgD_FfkYN-nN5YaqmCpIDuyOgIYpfyHaJBuZwx-RTKfyMnKU87vTtPKtpZmwqpFlVpmU3xoJXHhy2SwpKjvTuv4uZiEpY39EzhqzhpeB8EJRKEN2u5KngpLKXZaAIeW9wGcjdPXfxbuk7BXCX-d174r6Hq84g_Auae-J1QYchkoqv6M14gtzGVp_sxiBEfy6LeXbpf-uWSGTK3i6xD2rwsz4QDEcj2RWnqt_5JV5v-ivAOo246lqlQ3j5xOJj7mjBwrxs1T13bkW23ReOz7Ud02_VCr4isxX7jmjWxFtQzgUFDiiIRd8bqSfXMBqniZMiLXeSKNFwzeLWVYn4rwCg=","summary":[{"type":"summary_text","text":"**Searching for establish_attach function**"}]},{"id":"fc_047bb49263dfa303016a58abffc67881989e72c2b3c4fb7436","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating rc establishment function\",\"pattern\":\"fn establish_attach|struct EstablishedAttach|net_dial_loopback|request_attach_endpoint|net_stream_subscribe\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_B8NRebGMlqM4w3ah8FMYDoAQ","name":"grep"}]},"duration":6565.732599999756,"ttft":4391.961999999359,"contextSnapshot":{"promptTokens":189278,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_B8NRebGMlqM4w3ah8FMYDoAQ|fc_047bb49263dfa303016a58abffc67881989e72c2b3c4fb7436","toolName":"grep","startedAt":"2026-07-16T10:01:37.153Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Locating rc establishment function"},"id":"89bd90e8","parentId":"e4c8a464","timestamp":"2026-07-16T10:01:37.153Z"}
{"type":"message","id":"7a434764","parentId":"89bd90e8","timestamp":"2026-07-16T10:01:37.154Z","message":{"role":"toolResult","toolCallId":"call_B8NRebGMlqM4w3ah8FMYDoAQ|fc_047bb49263dfa303016a58abffc67881989e72c2b3c4fb7436","toolName":"grep","content":[{"type":"text","text":" 28|\n*29|use spt_daemon::attach::{request_attach_endpoint, send_attach_input, send_attach_resize};\n 30|use spt_daemon::effect::{Minter, MintedOp};\n 31|use spt_daemon::brain::{now_ms, Brain, BrokerEvent};\n 32|use spt_daemon::msg::decode_bytes;\n...\n 1388|// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\n*1389|fn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n 1390|    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n 1391|    // launcher. It must NEVER auto-start the broker — the historical\n 1392|    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n...\n 1425|            let dialed = brain\n*1426|                .net_dial_loopback()\n 1427|                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\n 1428|            (dialed.conn_id, sid, None)\n 1429|        }\n...\n 1469|                    Some(_) => brain\n*1470|                        .net_dial_loopback()\n 1471|                        .map_err(|e| {\n 1472|                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\n 1473|                        })?\n...\n 1485|            };\n*1486|            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\n 1487|        },\n 1488|    )\n 1489|    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n...\n 1492|    brain\n*1493|        .net_stream_subscribe(stream_id, 0)\n 1494|        .map_err(|e| EstablishFail::Error(format!(\"stream subscribe: {e}\")))?;\n 1495|    Ok(Established {\n 1496|        brain,\n...\n 2357|    /// canonical broker socket + a hermetic net host (loopback scope — the\n*2358|    /// net_dial_loopback carrier) and serves until killed. Env-gated: inert in\n 2359|    /// every normal sweep.\n 2360|    #[test]\n 2361|    fn broker_host_child() {"}],"details":{"scopePath":"crates/spt/src/rc.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":7,"fileCount":1,"files":["crates/spt/src/rc.rs"],"fileMatches":[{"path":"crates/spt/src/rc.rs","count":7}],"truncated":false,"displayContent":"   28│\n  *29│use spt_daemon::attach::{request_attach_endpoint, send_attach_input, send_attach_resize};\n   30│use spt_daemon::effect::{Minter, MintedOp};\n   31│use spt_daemon::brain::{now_ms, Brain, BrokerEvent};\n   32│use spt_daemon::msg::decode_bytes;\n     │...\n 1388│// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\n*1389│fn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n 1390│    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n 1391│    // launcher. It must NEVER auto-start the broker — the historical\n 1392│    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n     │...\n 1425│            let dialed = brain\n*1426│                .net_dial_loopback()\n 1427│                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\n 1428│            (dialed.conn_id, sid, None)\n 1429│        }\n     │...\n 1469│                    Some(_) => brain\n*1470│                        .net_dial_loopback()\n 1471│                        .map_err(|e| {\n 1472│                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\n 1473│                        })?\n     │...\n 1485│            };\n*1486│            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\n 1487│        },\n 1488│    )\n 1489│    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n     │...\n 1492│    brain\n*1493│        .net_stream_subscribe(stream_id, 0)\n 1494│        .map_err(|e| EstablishFail::Error(format!(\"stream subscribe: {e}\")))?;\n 1495│    Ok(Established {\n 1496│        brain,\n     │...\n 2357│    /// canonical broker socket + a hermetic net host (loopback scope — the\n*2358│    /// net_dial_loopback carrier) and serves until killed. Env-gated: inert in\n 2359│    /// every normal sweep.\n 2360│    #[test]\n 2361│    fn broker_host_child() {"},"isError":false,"timestamp":1784196097154}}
{"type":"message","id":"e351ad07","parentId":"7a434764","timestamp":"2026-07-16T10:01:41.325Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_0lqHMwoIG2SaIyxgeiPP8KxW|fc_047bb49263dfa303016a58ac0412a88198acb588e7598322a5","name":"read","arguments":{"i":"Reading rc attach establishment","path":"crates/spt/src/rc.rs","selector":"1360-1510"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3067,"output":39,"cacheRead":186880,"cacheWrite":0,"totalTokens":189986,"cost":{"input":0.015335000000000001,"output":0.00117,"cacheRead":0.09344,"cacheWrite":0,"total":0.109945}},"stopReason":"toolUse","timestamp":1784196097169,"responseId":"resp_047bb49263dfa303016a58ac01bd6081989e3e436ca309a823","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_047bb49263dfa303016a58ac0412a88198acb588e7598322a5","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading rc attach establishment\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1360-1510\"}","call_id":"call_0lqHMwoIG2SaIyxgeiPP8KxW","name":"read"}]},"duration":4155.779299999587,"ttft":2888.773699999787,"contextSnapshot":{"promptTokens":189947,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_0lqHMwoIG2SaIyxgeiPP8KxW|fc_047bb49263dfa303016a58ac0412a88198acb588e7598322a5","toolName":"read","startedAt":"2026-07-16T10:01:41.325Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading rc attach establishment"},"id":"9d8deec2","parentId":"e351ad07","timestamp":"2026-07-16T10:01:41.325Z"}
{"type":"message","id":"b1df6b89","parentId":"9d8deec2","timestamp":"2026-07-16T10:01:41.348Z","message":{"role":"toolResult","toolCallId":"call_0lqHMwoIG2SaIyxgeiPP8KxW|fc_047bb49263dfa303016a58ac0412a88198acb588e7598322a5","toolName":"read","content":[{"type":"text","text":"enum EstablishFail {\n…\n    /// isn't running — start it\" exit; inside the reconnect loop it's just another\n    /// retry (the operator may bring the daemon back), never a resurrection.\n    /// [impl->REQ-RC-RECONNECT-TRUTH]\n    DaemonDown,\n    /// A setup error (broker connect / dial / attach-open / subscribe).\n    Error(String),\n}\n\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n///\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\n/// the broker wires an in-process loopback conn whose streams surface as the\n/// SAME net-stream events the cross-node QUIC path produces. From\n/// `request_attach` down this is byte-identical to a cross-node attach — the\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\n/// The loopback peer's origin is this node's own id, so the target's serve gates\n/// same-node.\n///\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\n/// identically to loopback — so from here down the two legs converge. Honest\n/// failure on every resolve/dial miss: a clean message, never a false attach or\n/// a hang (D2/D6).\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n    // launcher. It must NEVER auto-start the broker — the historical\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\n    // broker cold-connect below is already deadline-bounded, so a down daemon\n    // never hangs.\n    // [impl->REQ-RC-RECONNECT-TRUTH]\n    if !spt_daemon::daemon::is_running() {\n        return Err(EstablishFail::DaemonDown);\n    }\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\n    // pipes), so the single-threaded pump interleaves stdin + broker events\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\n    // sessions, attach-open ack); the pump itself passes its own short slice.\n    let mut brain = Brain::cold_start_pump(\n        &spt_daemon::endpoint::broker_socket_name(),\n        now_ms(),\n        Duration::from_secs(10),\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\n\n    let ops = OpMinter::new();\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    // resolve the OWNING node from the registry and dial it, letting THAT node\n    // resolve endpoint→its session server-side.\n    let local_session = resolve_session(&mut brain, endpoint_id)\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\n    let mut remote_node: Option<String> = None;\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\n        Some(sid) => {\n            let dialed = brain\n                .net_dial_loopback()\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\n            (dialed.conn_id, sid, None)\n        }\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\n                remote_node = Some(node);\n                // session_id is a placeholder the owning node ignores — it\n                // resolves the endpoint to its OWN session id server-side.\n                (conn_id, 0, Some(endpoint_id))\n            }\n            crate::wansend::OwnerDial::NotFound => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"No live session for '{endpoint_id}' — not running on this node \\\n                     or any visible subnet.\"\n                )))\n            }\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\n                return Err(EstablishFail::NoTarget(msg))\n            }\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\n                )))\n            }\n        },\n    };\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\n    // ONCE more. The first attempt uses the conn already established above (happy\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\n    // drops the CONN too, not just the stream, so opening against the dead conn\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\n    let mut established: Option<u64> = Some(conn_id);\n    let stream_id = spt_daemon::with_tracing_retry(\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\n        |op| {\n            let cid = match established.take() {\n                Some(c) => c,\n                None => match local_session {\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\n                    Some(_) => brain\n                        .net_dial_loopback()\n                        .map_err(|e| {\n                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\n                        })?\n                        .conn_id,\n                    // Remote: re-resolve + re-dial the owning node.\n                    None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n                        crate::wansend::OwnerDial::Dialed { conn_id, .. } => conn_id,\n                        _ => {\n                            return Err(std::io::Error::other(format!(\n                                \"'{endpoint_id}' is no longer reachable\"\n                            )))\n                        }\n                    },\n                },\n            };\n            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\n        },\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n    // Subscribe our side of the stream so the target's `Output` records surface\n    // as `NetStreamData` events (the open + Request do not auto-subscribe).\n    brain\n        .net_stream_subscribe(stream_id, 0)\n        .map_err(|e| EstablishFail::Error(format!(\"stream subscribe: {e}\")))?;\n    Ok(Established {\n        brain,\n        ops,\n        stream_id,\n        remote_node,\n    })\n}\n\n/// Drive an established viewport to its FINAL end: the B-3 reconnect loop\n/// (REQ-RC-RECONNECT) — each pass is one full viewport (initial resize → pump).\n/// A severed viewport (broker bounce, remote conn drop — the reconnectable\n/// class) paints a centered reconnect banner and RE-DRIVES [`establish_attach`]\n/// inside a bounded window; every other end is FINAL and returns to the\n/// caller's parting copy. The FAULT-MATRIX row-9 promise (\"rc reconnects\") is\n/// made TRUE here, not edited down (triage F-3).\n///\n/// The SEAM (doyle rig ruling #4): everything terminal-owning stays at the\n/// interactive caller — raw-mode guard, stdin-reader spawn, StatusRow\n/// construction/teardown, the parting copy. This fn only needs an established\n\n[Showing lines 1359-1513 of 3214. Use :1514 to continue]"}],"details":{"truncation":{"content":"    /// isn't running — start it\" exit; inside the reconnect loop it's just another\r\n    /// retry (the operator may bring the daemon back), never a resurrection.\r\n    /// [impl->REQ-RC-RECONNECT-TRUTH]\r\n    DaemonDown,\r\n    /// A setup error (broker connect / dial / attach-open / subscribe).\r\n    Error(String),\r\n}\r\n\r\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\r\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\r\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\r\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\r\n///\r\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\r\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\r\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\r\n/// the broker wires an in-process loopback conn whose streams surface as the\r\n/// SAME net-stream events the cross-node QUIC path produces. From\r\n/// `request_attach` down this is byte-identical to a cross-node attach — the\r\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\r\n/// The loopback peer's origin is this node's own id, so the target's serve gates\r\n/// same-node.\r\n///\r\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\r\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\r\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\r\n/// identically to loopback — so from here down the two legs converge. Honest\r\n/// failure on every resolve/dial miss: a clean message, never a false attach or\r\n/// a hang (D2/D6).\r\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\r\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\r\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\r\n    // launcher. It must NEVER auto-start the broker — the historical\r\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\r\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\r\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\r\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\r\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\r\n    // broker cold-connect below is already deadline-bounded, so a down daemon\r\n    // never hangs.\r\n    // [impl->REQ-RC-RECONNECT-TRUTH]\r\n    if !spt_daemon::daemon::is_running() {\r\n        return Err(EstablishFail::DaemonDown);\r\n    }\r\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\r\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\r\n    // pipes), so the single-threaded pump interleaves stdin + broker events\r\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\r\n    // sessions, attach-open ack); the pump itself passes its own short slice.\r\n    let mut brain = Brain::cold_start_pump(\r\n        &spt_daemon::endpoint::broker_socket_name(),\r\n        now_ms(),\r\n        Duration::from_secs(10),\r\n    )\r\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\r\n\r\n    let ops = OpMinter::new();\r\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\r\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\r\n    // resolve the OWNING node from the registry and dial it, letting THAT node\r\n    // resolve endpoint→its session server-side.\r\n    let local_session = resolve_session(&mut brain, endpoint_id)\r\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\r\n    let mut remote_node: Option<String> = None;\r\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\r\n        Some(sid) => {\r\n            let dialed = brain\r\n                .net_dial_loopback()\r\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\r\n            (dialed.conn_id, sid, None)\r\n        }\r\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\r\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\r\n                remote_node = Some(node);\r\n                // session_id is a placeholder the owning node ignores — it\r\n                // resolves the endpoint to its OWN session id server-side.\r\n                (conn_id, 0, Some(endpoint_id))\r\n            }\r\n            crate::wansend::OwnerDial::NotFound => {\r\n                return Err(EstablishFail::NoTarget(format!(\r\n                    \"No live session for '{endpoint_id}' — not running on this node \\\r\n                     or any visible subnet.\"\r\n                )))\r\n            }\r\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\r\n                return Err(EstablishFail::NoTarget(msg))\r\n            }\r\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\r\n                return Err(EstablishFail::NoTarget(format!(\r\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\r\n                )))\r\n            }\r\n        },\r\n    };\r\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\r\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\r\n    // ONCE more. The first attempt uses the conn already established above (happy\r\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\r\n    // drops the CONN too, not just the stream, so opening against the dead conn\r\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\r\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\r\n    let mut established: Option<u64> = Some(conn_id);\r\n    let stream_id = spt_daemon::with_tracing_retry(\r\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\r\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\r\n        |op| {\r\n            let cid = match established.take() {\r\n                Some(c) => c,\r\n                None => match local_session {\r\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\r\n                    Some(_) => brain\r\n                        .net_dial_loopback()\r\n                        .map_err(|e| {\r\n                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\r\n                        })?\r\n                        .conn_id,\r\n                    // Remote: re-resolve + re-dial the owning node.\r\n                    None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\r\n                        crate::wansend::OwnerDial::Dialed { conn_id, .. } => conn_id,\r\n                        _ => {\r\n                            return Err(std::io::Error::other(format!(\r\n                                \"'{endpoint_id}' is no longer reachable\"\r\n                            )))\r\n                        }\r\n                    },\r\n                },\r\n            };\r\n            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\r\n        },\r\n    )\r\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\r\n    // Subscribe our side of the stream so the target's `Output` records surface\r\n    // as `NetStreamData` events (the open + Request do not auto-subscribe).\r\n    brain\r\n        .net_stream_subscribe(stream_id, 0)\r\n        .map_err(|e| EstablishFail::Error(format!(\"stream subscribe: {e}\")))?;\r\n    Ok(Established {\r\n        brain,\r\n        ops,\r\n        stream_id,\r\n        remote_node,\r\n    })\r\n}\r\n\r\n/// Drive an established viewport to its FINAL end: the B-3 reconnect loop\r\n/// (REQ-RC-RECONNECT) — each pass is one full viewport (initial resize → pump).\r\n/// A severed viewport (broker bounce, remote conn drop — the reconnectable\r\n/// class) paints a centered reconnect banner and RE-DRIVES [`establish_attach`]\r\n/// inside a bounded window; every other end is FINAL and returns to the\r\n/// caller's parting copy. The FAULT-MATRIX row-9 promise (\"rc reconnects\") is\r\n/// made TRUE here, not edited down (triage F-3).\r\n///\r\n/// The SEAM (doyle rig ruling #4): everything terminal-owning stays at the\r\n/// interactive caller — raw-mode guard, stdin-reader spawn, StatusRow\r\n/// construction/teardown, the parting copy. This fn only needs an established\r","truncated":true,"truncatedBy":"lines","totalLines":1856,"totalBytes":8592,"outputLines":155,"outputBytes":8592,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"enum EstablishFail {\n…\n    /// isn't running — start it\" exit; inside the reconnect loop it's just another\r\n    /// retry (the operator may bring the daemon back), never a resurrection.\r\n    /// [impl->REQ-RC-RECONNECT-TRUTH]\r\n    DaemonDown,\r\n    /// A setup error (broker connect / dial / attach-open / subscribe).\r\n    Error(String),\r\n}\r\n\r\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\r\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\r\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\r\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\r\n///\r\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\r\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\r\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\r\n/// the broker wires an in-process loopback conn whose streams surface as the\r\n/// SAME net-stream events the cross-node QUIC path produces. From\r\n/// `request_attach` down this is byte-identical to a cross-node attach — the\r\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\r\n/// The loopback peer's origin is this node's own id, so the target's serve gates\r\n/// same-node.\r\n///\r\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\r\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\r\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\r\n/// identically to loopback — so from here down the two legs converge. Honest\r\n/// failure on every resolve/dial miss: a clean message, never a false attach or\r\n/// a hang (D2/D6).\r\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\r\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\r\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\r\n    // launcher. It must NEVER auto-start the broker — the historical\r\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\r\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\r\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\r\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\r\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\r\n    // broker cold-connect below is already deadline-bounded, so a down daemon\r\n    // never hangs.\r\n    // [impl->REQ-RC-RECONNECT-TRUTH]\r\n    if !spt_daemon::daemon::is_running() {\r\n        return Err(EstablishFail::DaemonDown);\r\n    }\r\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\r\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\r\n    // pipes), so the single-threaded pump interleaves stdin + broker events\r\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\r\n    // sessions, attach-open ack); the pump itself passes its own short slice.\r\n    let mut brain = Brain::cold_start_pump(\r\n        &spt_daemon::endpoint::broker_socket_name(),\r\n        now_ms(),\r\n        Duration::from_secs(10),\r\n    )\r\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\r\n\r\n    let ops = OpMinter::new();\r\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\r\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\r\n    // resolve the OWNING node from the registry and dial it, letting THAT node\r\n    // resolve endpoint→its session server-side.\r\n    let local_session = resolve_session(&mut brain, endpoint_id)\r\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\r\n    let mut remote_node: Option<String> = None;\r\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\r\n        Some(sid) => {\r\n            let dialed = brain\r\n                .net_dial_loopback()\r\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\r\n            (dialed.conn_id, sid, None)\r\n        }\r\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\r\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\r\n                remote_node = Some(node);\r\n                // session_id is a placeholder the owning node ignores — it\r\n                // resolves the endpoint to its OWN session id server-side.\r\n                (conn_id, 0, Some(endpoint_id))\r\n            }\r\n            crate::wansend::OwnerDial::NotFound => {\r\n                return Err(EstablishFail::NoTarget(format!(\r\n                    \"No live session for '{endpoint_id}' — not running on this node \\\r\n                     or any visible subnet.\"\r\n                )))\r\n            }\r\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\r\n                return Err(EstablishFail::NoTarget(msg))\r\n            }\r\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\r\n                return Err(EstablishFail::NoTarget(format!(\r\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\r\n                )))\r\n            }\r\n        },\r\n    };\r\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\r\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\r\n    // ONCE more. The first attempt uses the conn already established above (happy\r\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\r\n    // drops the CONN too, not just the stream, so opening against the dead conn\r\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\r\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\r\n    let mut established: Option<u64> = Some(conn_id);\r\n    let stream_id = spt_daemon::with_tracing_retry(\r\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\r\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\r\n        |op| {\r\n            let cid = match established.take() {\r\n                Some(c) => c,\r\n                None => match local_session {\r\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\r\n                    Some(_) => brain\r\n                        .net_dial_loopback()\r\n                        .map_err(|e| {\r\n                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\r\n                        })?\r\n                        .conn_id,\r\n                    // Remote: re-resolve + re-dial the owning node.\r\n                    None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\r\n                        crate::wansend::OwnerDial::Dialed { conn_id, .. } => conn_id,\r\n                        _ => {\r\n                            return Err(std::io::Error::other(format!(\r\n                                \"'{endpoint_id}' is no longer reachable\"\r\n                            )))\r\n                        }\r\n                    },\r\n                },\r\n            };\r\n            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\r\n        },\r\n    )\r\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\r\n    // Subscribe our side of the stream so the target's `Output` records surface\r\n    // as `NetStreamData` events (the open + Request do not auto-subscribe).\r\n    brain\r\n        .net_stream_subscribe(stream_id, 0)\r\n        .map_err(|e| EstablishFail::Error(format!(\"stream subscribe: {e}\")))?;\r\n    Ok(Established {\r\n        brain,\r\n        ops,\r\n        stream_id,\r\n        remote_node,\r\n    })\r\n}\r\n\r\n/// Drive an established viewport to its FINAL end: the B-3 reconnect loop\r\n/// (REQ-RC-RECONNECT) — each pass is one full viewport (initial resize → pump).\r\n/// A severed viewport (broker bounce, remote conn drop — the reconnectable\r\n/// class) paints a centered reconnect banner and RE-DRIVES [`establish_attach`]\r\n/// inside a bounded window; every other end is FINAL and returns to the\r\n/// caller's parting copy. The FAULT-MATRIX row-9 promise (\"rc reconnects\") is\r\n/// made TRUE here, not edited down (triage F-3).\r\n///\r\n/// The SEAM (doyle rig ruling #4): everything terminal-owning stays at the\r\n/// interactive caller — raw-mode guard, stdin-reader spawn, StatusRow\r\n/// construction/teardown, the parting copy. This fn only needs an established\r","startLine":1351,"lineNumbers":[1351,null,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":3214,"totalBytes":8592,"outputLines":155,"outputBytes":8592,"shownRange":{"start":1359,"end":1513},"nextOffset":1514}}},"isError":false,"timestamp":1784196101348}}
{"type":"message","id":"d64b0d2e","parentId":"b1df6b89","timestamp":"2026-07-16T10:02:13.107Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Summarizing existing test coverage**\n\n**Formulating falsifiable hypotheses for active dispatch tests**\n\n**Designing stale serve simulation tests**"},{"type":"thinking","thinking":"**Designing stale stream attach handling**\n\n**Extending daemon refresh tests with eviction scenarios**"},{"type":"toolCall","id":"call_7EevEp1BCYtlxc6ICTL7N4Sh|fc_047bb49263dfa303016a58ac247494819895201b041e097890","name":"read","arguments":{"i":"Reading cursor resume split","path":"crates/spt-daemon/src/brain.rs","selector":"1025-1085"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":5174,"output":1001,"cacheRead":186880,"cacheWrite":0,"totalTokens":193055,"reasoningTokens":958,"cost":{"input":0.02587,"output":0.03003,"cacheRead":0.09344,"cacheWrite":0,"total":0.14934}},"stopReason":"toolUse","timestamp":1784196101402,"responseId":"resp_047bb49263dfa303016a58ac05d3248198ad5f23da121438d5","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58ac0861688198b5b5548ec6399acf","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKwXIYUBCbdCD8P3OnnGsjEZmRsW9eC5O58SdZroKGkEKNHlY09nlc_XXdr1LPfCOoYSTjMRYXptMf3TsbuRTBBedQDU_IngTp-q6K8ueRrgXVvkOg2cfm5pL0CLjeehgELEDZLYGUStpoF8--s8aZ2vqTXG2q1PyjsNA3n-8hqiefSRbh7GrecFSt9gioysDPTg22t8wlZi_PrYm-_vCFQq5IEhjekRUXcpKTDwAgZ72PzouCJomsTyvkMwl5pBOB5kKHYF8-hRmeJXQ9tHGWLkrDvnrY9b6TbXvv9Ybl2keTGxoo61_XVAhPTRNOjkPxdh5NCtZHE0PzQzldvISKcNsNG9GdWgwyi6cD_s4fex7bPPYKvbynNBbmGRkFlOsG_JXYdr6NGC3GWmwSHdr2d0kqHP7YxFMRSKLf4a_PGCE-eLxQRfB26ZWxouIPIXZQBGIfMpVKbZLqAw54BFyCR9cyChYyicJHclzeNK_DQx5L-GRhP-hKrl93y_-a5m5qh3KFgOkwXgLi9JHhzaL8Q6roXZx03uJrAR0sVtTsWiFeCY_kPay83V6tnb30hL1VcN9J3E0cZ8tr6ZGIzPPk1768_MmQ-h_PFe533PfBzKWGguj9zVyctHJZ31uzefIreVLRC_1dXj7n4pyze5RidGAEoq9A_Wv8OUdpWWgqD5uEGLxpem0Mn-XxsXTxriKHbJY46WqkLnBWqMnadD-yxVeXZwUbijZR0Lz3YKPElr2tLZIXQQZBYPJXcNTiVSh3zHJ1L26AYl0Tn-3zD7usaFVEMG459MytyhhvI2SIMbtp5NEF0ZsmIqAqUelHKNnsjq2fgt7bOqKkCSNIqqC6V0c0VDuh64v-ur9iFLW0pK-mTkLbQYzZDkUlCGvSunaClQUb2LigvGIkzRueB1EMj7bHDdJDIgpL9hAbnFem5OzTWrI5ZiIYTDIMRL4pazPKymkMGLJRUCcs7wbC9dLC5eypWcr2s5sXwep0m3BYTWH65ywMxKTifs5MgGnlNfES_qPq_jyon_ULsYne4b9F1HXkQfISawkaCJ2tfyAbcZ4nRMOZ_jpT61jZsroa8q75-NkmxT4xmoU6umafdoBc75OAX1QMH4RId3KlRKP3HqBN0nVAWswxR4e0DIJtoC-S4XUG7QxaMzrOgTmXQjymu7PD_EI-0bb-rGeoPguGuIObTw7Q6d-BQPzgtQmcSimiTrztr2c-UP7gn_xgUfFmb-zXL3s6E8YujQL5XBjpMu9JWraPBGo7pZxgQHt-ChQ9z7rHLH3aAkpHt7SlnB9qhHwioyKhEo9Nxmsyv6fQwwh6-dyrhzaxkCxHZZAz98pdhZSf6PtyOWZ2YIoWy1JK3Qw6NCgSDbTFzeKrx19hVYrq9XUko6EVN3byUbGl6L3V-x4o20ROejPkEoLIfYxjWqO7zliWtXYyzPrqIVX8G3yw3tlHOdec_4D2-bTF1zJFvqsd4EWBmnrPikA-SK6_2CQA7t7kt2yESWNCABREpb5RHaAzeGzxCk5w12pFTMklcIMctwIwdT0MwAR_9cCeVFaNNG55rB8pz7kYepHrMCBqApZS9k_pxiCWI5SwoucwPrko5cIExeMUnMb2vHSpaezHbe6NEdy6KbyXsN8B96pIGBdm8YR-OthslVmF3nw-Tp7QPQ0Swd_mikdSkJ8hQ4qu3mSeV0QMLnjceA5aQjlDaCSKSjNUvRFCmMNj_nUvRZML2SwdVEpMKBX_ITwWr11IJbyKGM-KGyYmXnuPgRCh1qZPhpkX4lOVgIQBlxEjPDrKvRdedq30TV7QAZ83fGMsGSTTWzbUGVddYkODuksfiWWuePqLTcxMvecLFJ9mMVfGW2BmweW4ncfxgghk8jU1-xT0tHuyOEv5kEElTv1X6DUYCAUaMCk5AjuWJmh-Cp61v9OJ_-exZp9Ssi9LTSQRWQhgx56b4ubkU5ZNVxcgEjNm9CWrY739duCN1Tj67sd85E43TCz0V09ai3Betbyc9NKV0HB8RUGzDFNr0qqPPofBoI_YKCei5lJsN7Ss2DdlZv2BQS3O05FvX5ryeiQP5GzV4Vv_-s8db5G6tmnCx7LjaWDrqB9ujlgFBK6TVfWd1zxJdSUTJXlj8YYsVS_Wpu66oG3RQhVZ4UjTbd45sN1jJZbKAujAPJ30GeB0pwx9an47a4EaW2k6SMfzqleC9_E4bsyWApt6pA6YlJ9HxobgsQ7KuhhMCnAZcsxlKs1YbY464yx74F1U48aTX6_3XDy-u3pIopb7dTdk31c0Z8RwseDBKIlLcHEnwqADSwJy4qy31IZ-WnPpENYw9JxrrcoHypNrMjOfcCM_ZwtESThQzm_sxlwUxldylkP7JQHm-XKQI6kwFTo6qdokwzxyHDpkYNVpFXRCjdkF1yVMxcuOhe3tIN6miO_oPaY8jhIb8zLvltEQ0xE2IP8yWLz85OnxQ1QL7dQdXZ-DoVccq0NZ1bMpH_eWgpI2MhdNfeIHA1naG1MaKfB-h-hlimxdQRB1ZyFivOf3O7b4Hki3xnyEEf8w5eIlTD4Z-tWomXkYtOt_jFpvH7UQp8H1q4DwI_-H5RHMKVNxcA27HFNhe9rdbul4uwscdukjcEyi9s2aVUzRRwfBlvEh0csSosNQnSsrPBVg76Bk_7OzIEktAALLb2Ei89_OdE-0a3KmMY43Kw1XQW9nAbS_kDR4Y_ZeXT52UzmD3UiuEjWoC5Ys0g7gCKWfDX1U8AofTCt4M7GTHgWI9jnvzEI_XrxSTgB8sbkGwEHxVqcOoBg0YqtZX3ZZuShZ3GF4Y44uXyo1HfL38cn7AvwtwCPITvd01mcGOGWO1vrE3XZbb9Q11T_N7nO0l9Sdk4j-4R1Jx6K1NhygDFbe730j7g4a0g-JuBPmaY6SipisgWD7DAl4hfGneb8WXoxPT1Z8tRmcMIHnO3jiSBJSmtAs8TryoUIxjd3Zu2Nk0k5P8cOm59TKmlooxZrLVpJSsC8yPAvPC4zLSwdAL4OdQKOE1DaadKYt5_V4dFgD4tOUU60wKk3LymG-0i48QfiuXXbdt_OHWdv48-ceTFwewvNvWVxsu2I9LhaPbvO_I3tEncF88CNLwktmt7AHX1jNoOWZqR7PbuFUL7pRm0pwI3X4AnG1ZuPlk1xMkgSPiLTj_j2ExoymLKiIdtIoAYypi5r5qg8nA3iJXs4dDuYrfl9NSb4H1kJ8R52pnNTf_19s-ZaLqRIhku-Bdaeo1uUpkZLFCsoUcGt_U-mlx4kqOeCbSG1NxqP9GY9aDyePavMJQEZnux7SGdGsQthBjVd2DWGQfb46RJGii9vjPJ0MrlA1ux0rcAY_M1Bo5qU--EiPsZy1HrwyD8Gq_YMTZzaLdorACGv5swjztLHiqM7yN2c_y_cWOHlhuqLOr9y-zRDW9fjP3DNsfRF1htE0dXNVVJ2ZCejP4ZWzPtssRlAGnNXp3aUTARDwcbS5OFOXsW2crDknu3jx39SzrDfiWY7ertqzqeCzZJlgV3svbxItajtBjR2ZMsLhqNcP5vhLgHmNWB24-XOYoS-tpihBYE-M5RuMiZR7ly1Ejfauyq6ErxvtqHmTmM_WBWZwthTkkOkc_ShQ51ykc6DHQNJ4GblXoCUiCq-31DCbBiAczHC8vWPQueIq9C0HsMFAsjIuoR9zsoiFC26wHssYmUyv1bwbh8ta5j6Uhj2QiEax0EX17zScZSTcjWepTeShdVzfxMGSOIOS4c_xkMvr9uhtuXDXmnsRc2AZCnznYxjqaHq9lJidXdgRvfcX8X1WZkA6_RCTQfTBZ582zX9zBvIWEueOKKOL2nscJsLeNzCtYsHlYpNF60TwQZ2IaQM9NLFHE1W7eDoaJruVWJ6V0u7IxBPSiG2bHuk1sAUSgRTkd86ok4syyY3m8axIZC3KO-48LoH9bCbxkU8LMzh3Sx25uhZzxZm0JC7F2kI62yEvGkf4Ig0uA-mUcxXBYdlfqN6VhzCUkuwpu-m1o7s7aGS6Z0JHrFX4EL3ZHsxoleDH4tQ59GwFLm6f7VcU0WIBXN4WVDODJL_UBMvVbglDLaCN_Xm1pSuw9Mb-bkCtoTzAPZJ40LET3W3mShkEJMiFuwCS0dtgwSfMD1b-9x265HG9IHHFUQx4ZyCHzLgB6k-sB7kdES6y1xDZxwem6Foe-vTD0FZ0FG4_z_13cCF-Z9P53z1HwUWooR-lwDDKs8ZU08GnbDFMLUwJE7NHB1fs_hndQVPqc5ulapErfnw0NYULDQofYP7GK64Th0MlIYkF4AYx_pr0vAL_XZoePiZ5f4I7vsjCd04Nu4HgwzdKRWA46e7rWvulLgXpEfPGV0","summary":[{"type":"summary_text","text":"**Summarizing existing test coverage**"},{"type":"summary_text","text":"**Formulating falsifiable hypotheses for active dispatch tests**"},{"type":"summary_text","text":"**Designing stale serve simulation tests**"}]},{"id":"rs_047bb49263dfa303016a58ac17d1f08198b36503d6d1123ab6","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKwk6IL7LWP5cfSNbZWBIsm_qZbglrZzEdrxNskL8auP1SKyMKGj9HMx-nH_Z9kGZzVLZr-_AigxZOAU4eF7yzzINKozHHGCLJeyZBU8ZPKsvOvRKhtPyya6yWqYCjtW_U9TjEhnsxaTcNGhAHyTyNrswQ0Ld5UTt7gwV3S4jpTB6KrzfuSvZQPx5LkDIxU2Gm4tWgttg0Sr_BV0u16DcCOxzPCCu9W6DDyJAVXLpEpfy1uhK3SdDzoStjku9nHrfybCL-P17h6ETetZ1Fz5psDW4TbZvYpcLjk6RJ5jTVaKHea4gRPgmJrSpTSTy3NFEYxOapjNVBLVN6lD9j6J1DlvRmnw22dzGbMlm6Bu1GSGwWeTfRzeeOXm68olM79PMNTJJ1rUMcQg-q0KMk7uy_bMSpGJ5SFvMscONXJhVOr8-OO77ltmckoHFOVYBJpwPRx0Bmp7_btVmGz84G_hDHnEytpMSNTjTbkvALd7QJ-eNhI3un_u8C4eakAuRYOo2qjb_Btd_Mw-jbNtfQNGvWvn_0I-pjfvWXDQCmB80t19t6BYrbFiLCML8FEBUHaprAHRFJx78Lq1y0MiSLgdStX3VNAOr4HpNBAXJupgAy5SPvQOd8-J3Y1VpswdMW5WLdnJM7MMQ-wOYWTMauacOJoZlzRY5TTJsU_sor1V5bslPgt1dywa6s9OCYXLy1ZCKjCR5PwFlVTnk0BQU7GGZh4BHeWk_q7AlpA7R6038IZz_MFLZ10d2lwiLyTWhWPupIkEzWClj3esdoA9cmBLmI4qWNewoA0PhQ1FU1MP64LUDfUpHCPQciSkAhhbga1Tuvo-mBBe9lxy4UwLPJg5JoceoSF0HPwnzY9emo3t-ppNOaLl01qlczAv-Dz2vlY7xcdZi0IHGEQkucKYw7vzc6QDa69gQPgGUK29AsZz2V13zD6FOfPBpdEdGUAhCvCpSA9ubV-xLmA0saXFnE_w1EwAtffHZwExNieyPXR7uCI5RXgcbXo4rYiek8AlnE00S0vLE8AZlWXJj_JKiyRsPIYgSsu9A7TL3A2rLXhwpLcP5ZGw02d3ZdZOzV_GLXsLGZECT0DckgPlN8899hwHtZi1DiSw4c8BEbHxqMaZzgCvArz1_vkj_7p6m936MK8dGtoHaFnCrP4Ho4oXk-0_hectjjKlb0OWNh-Ks7725P8QKavtUS5pXkn3_rdnPKX8h-_U-262q-206372XSaEt9ud89qhEYLYTGsObRfH6EMKGHPR4dVnQDN_fJNFK01NeeiRX2kD7JQutIj-ZBXdW61HfwJU_hXqryHuP9KR9Jf2jWZGRmiesosYQ7fN1HSweHRiWFaYPZVO5qoVe8Xwebp7bINwAVBgRZAtMUDD0Fw7LslusPMWEwi5pdkrztKraVe0Fte7RaY1oKnD0rSw7UB8BIG2IQYxKyCc0D8owwTjYlYVd0Cgj9AqEqjFYdbzOUhgf0GcWHlkNnmX0Ks2eKr_cks5fHgeTgU02ex4FiCCCb8Vilj-NNGKm9hPN_nZFjbhvmyCVjyZ3QsC9VlFCWlLOfpZmpF-EqHA2mT09dAZ5DHsjBf_k7CR4VZCEG9XlMtegSxBk5RZ84mJEgTL-92onlT8oogF1DKJ_70JplThimuV1n5GktYS_tdnhS3pfRO0tt433GCM9wfOOTZCxP3rq3fUTQg06spcLFiS3KD_NDHRxOSK9TuHXLEoAK9G5eOyRWTqmxnA6ujAAyLPZ5AGqu5Q9djJTzvtBvaZDmLP32ggT_9ia__Lf8po_STjLZ9FM3riZVZHPBkobf00MgNWRKSi7g0eeIsQm9_qftQCMgp3uj-g2E1i7fqg0ZxZtZR-_CsR2Wnmo9IXrAuYAlG7OfwpdnPPSfeVhDhhcMnza-u9Y8LyFGnMy0P8GBmSZzD-neJHebBqsxQwnBqxVEUXs5iw9cl5PEHGd5ERMhGMELfFao1Ty7m2sfDhdM_XkbrJ7M6qxz_OfljAzqM2AFalVIt8NtSetNe9B0ynjmDmiCzwrtvJMXOLEtWQaQo8nOoJxVRAIO-GP20DNQJBETuCXv6stUarF1SpZM0ye9iibJUBb7VZx4nlPRnrNUKCIZVVrmUmxCY_CxQG4LbLhpBZb_foYx1mw6ZVSH4kmQEzPhhAVZkgRnqX7-62duLkSqfxWD-TFxMMpRNMdgJYAiVDdtgJHAXw0kxUqWcSq3pnjKF2wDyjfoPyqJF5EtnWXXTk41yj8SN_zMi0Cpwt47eRvPCJLdthaBegC88vfeUZycb-sq2mtRu5ezI9u_BJwfBnfCXiR-WnYC30KqQXtHGINTnRXHktT2N1I29GYQlbLGqgZMVw8Lu_wlwrdNBIIOC8aMiNWQKbmLmBdPynwtDLM9EmyXAPFuVTtYvuMHJMr3hqMLeuan0bZRnPVeCt3NEIr-gDE1Pa-f5QNmCyUYC-a3bBX7x0VcE72PXEoBzfKAcd2e0ZZHDWe41Tu-0Us4YR5-mF1ZO-ZzpDz91ELbwM-SFvRj74zrDzqmTuSiuLC5uivwOU9eSAtBVcl5epMuD_ZjAKyDAqMRO4r5coa7f4-0OoEEwzt9i-vwPOVR49CcDT_sAJks69RhHStkZfCRtGyhFD1ZqsHbSP93YAUV4I8CR-f9pnT0uUnNa2yNmDrpxXP6Lk1oRHWdgUZLg0JMWfLujsZbkXHPxKUsaxzjpRSJBX3eOpReNzM8RZeZMFX4_QLldM5ob4cJyFyPmbKGcy3CnJOcrH7Vk-Ov39FcmhjwWz88c4zBvWrN9qZ-zDYO-zECeXIrspfUVNYSAtL4mSNsYCe4BZ9yHFee6wvgIBmskPd1jGm9KS_KheOAl_OdX8dPHKME-WyHOcIZDJsHQfvTYo_0RpE4B-FiZVQAkJ5sQbTJtamJcC-hKwhk3O-mDfHBhmBzVOev-hkKsx-KBW5djZwgEOMd_T3CqRO1yTX1Y8YzO2r75Ko0Vh3G7p-wOXGK_vDvhI2hhXL4dkkU2DC-iRhmY5jX2REnouaAJlYIa2igszVMcXuh5X670lBwN89ydgcHma2dY1xsNDOWYu6e8KgpL0Y9FDaMVNkNF-1XJ2nizTdg0MYl4HZfxiAeQRtx8WttbI_t2iUBARiLp2KUZLACqqi5W_GzGieCF-wqgYRvm6-ZtOHv_-4UCdaf5n6eE3-jRTPPIE_yXZF5wwj-JbDjN9V0tdN18OhQ4Lmp7uqsOcSqS3y2hKzIk_i4ILkIW--IK7b5gsRjBfHan0L2RnpdKq8CriFeIJOKFt7ftAta_JZt7N_dVfsogeBwczs8oqkNs8eVJhuBwa94E_KqUJpePzzAnCTJiz1OzPjhwBib4ujmdDVCul0L_G2tEuTUAQix9vaPR4SJVEbxGibkhgO21U9iomIkY-EusIvhan75EzGyERsdqipwoMX-E-xWqfRJC7HFCosQNtISLc3uRFmUYVXMm3FxhulkQMR4ERuGqxv5F5evOyBursW8aTun7ECOPHs1TXKUQJG8dtNMeizA7f5vaRCPnyxmUbRJPFHk-M0aOhnFKh6yOmSN0aILQ0uDgSi3N0QndtOFXeWMQ2sdwPa7o3uwQLaff1nA==","summary":[{"type":"summary_text","text":"**Designing stale stream attach handling**"},{"type":"summary_text","text":"**Extending daemon refresh tests with eviction scenarios**"}]},{"id":"fc_047bb49263dfa303016a58ac247494819895201b041e097890","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading cursor resume split\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":\"1025-1085\"}","call_id":"call_7EevEp1BCYtlxc6ICTL7N4Sh","name":"read"}]},"duration":31704.30400000047,"ttft":2967.9371000006795,"contextSnapshot":{"promptTokens":192054,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_7EevEp1BCYtlxc6ICTL7N4Sh|fc_047bb49263dfa303016a58ac247494819895201b041e097890","toolName":"read","startedAt":"2026-07-16T10:02:13.107Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Reading cursor resume split"},"id":"326b6f9c","parentId":"d64b0d2e","timestamp":"2026-07-16T10:02:13.107Z"}
{"type":"message","id":"14723961","parentId":"326b6f9c","timestamp":"2026-07-16T10:02:13.124Z","message":{"role":"toolResult","toolCallId":"call_7EevEp1BCYtlxc6ICTL7N4Sh|fc_047bb49263dfa303016a58ac247494819895201b041e097890","toolName":"read","content":[{"type":"text","text":"    /// REQ-BRAIN-RESUME-NO-CONTROL-STEAL). Genuinely daemon-brain-driven sessions (the\n    /// live-agent adapter future) will re-attach as Control on a drained carrier; that\n    /// set is empty today.\n    ///\n    /// ⚠️ DEADLOCK WARNING — the CALLER MUST DRAIN this conn (UPDATE-WEDGE round 3,\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK). This subscribes the sessions onto THIS conn.\n    /// A NON-DRAINING consumer will SELF-DEADLOCK: an actively-streaming session backs\n    /// the conn up, the broker-side subscriber writer blocks holding the conn's single\n    /// `SharedSend` mutex ACROSS a blocking `write_frame` (broker.rs\n    /// `viewer_writer`/`controller_writer`), the dispatch reply path (`send_frame`) needs\n    /// that same lock, so any request/reply the non-draining consumer issues is starved\n    /// (dead-peer-independent — the v0.30.4 field re-wedge). The supervised daemon brain\n    /// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn\n    /// only in `net_status()`/`sessions()` drain-and-discard loops; it drives no PTY\n    /// session — brainproc.rs:184 — and nothing on its side consumes the output, §3\n    /// guard), so it MUST call [`Self::resume_session_cursors`] (cursor-only, NO\n    /// subscribe) instead. This subscribing method stays for a DRAINING caller (the\n    /// resume int tests today; the forward live-agent-adapter / Option-B drained carrier).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-DAEMON-2]\n    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    // [doc->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {\n        let reply = self.sessions()?;\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\n        for info in reply.sessions {\n            // Seed the dedup cursor at the broker's resume point BEFORE the subscribe\n            // replay starts arriving, then re-attach (Viewer) from there.\n            self.session_cursors\n                .insert(info.session_id, info.resume_seq);\n            // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n            self.subscribe_with(info.session_id, info.resume_seq, AttachIntent::Viewer, None)?;\n            resumed.push(info.session_id);\n        }\n        Ok(resumed)\n    }\n\n    /// Cursor-only resume for a NON-DRAINING caller (the supervised daemon brain): query\n    /// every hosted session and seed [`Self::session_cursors`] at each `resume_seq`,\n    /// returning the re-attached ids — but DO **NOT** subscribe. This is the safe resume\n    /// the daemon `run_brain` heartbeat uses in place of [`Self::resume_sessions`]:\n    /// carrying only request/reply on the brain's conn (no UNDRAINED subscription) makes\n    /// the `SharedSend` self-deadlock structurally impossible (UPDATE-WEDGE round 3,\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK — see the deadlock warning on\n    /// [`Self::resume_sessions`]). The daemon brain drives no broker PTY session\n    /// (brainproc.rs:184) and nothing on its side consumes session output (§3 guard), so\n    /// the observation cursor is all it needs; no attach also means no controller steal\n    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL, a fortiori). Rides the brain-swap ⇒ seamless.\n    /// When genuinely daemon-brain-driven sessions land (the live-agent adapter future),\n    /// attach THOSE on a DEDICATED drained carrier (Option B, deferred) — never on this\n    /// request/reply conn.\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\n    pub fn resume_session_cursors(&mut self) -> io::Result<Vec<u64>> {\n        let reply = self.sessions()?;\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\n        for info in reply.sessions {\n            self.session_cursors\n                .insert(info.session_id, info.resume_seq);\n            resumed.push(info.session_id);\n        }\n        Ok(resumed)\n    }\n\n    /// The per-session resume cursor (the next seq still needed for a session\n\n[Showing lines 1024-1088 of 2178. Use :1089 to continue]"}],"details":{"truncation":{"content":"    /// REQ-BRAIN-RESUME-NO-CONTROL-STEAL). Genuinely daemon-brain-driven sessions (the\r\n    /// live-agent adapter future) will re-attach as Control on a drained carrier; that\r\n    /// set is empty today.\r\n    ///\r\n    /// ⚠️ DEADLOCK WARNING — the CALLER MUST DRAIN this conn (UPDATE-WEDGE round 3,\r\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK). This subscribes the sessions onto THIS conn.\r\n    /// A NON-DRAINING consumer will SELF-DEADLOCK: an actively-streaming session backs\r\n    /// the conn up, the broker-side subscriber writer blocks holding the conn's single\r\n    /// `SharedSend` mutex ACROSS a blocking `write_frame` (broker.rs\r\n    /// `viewer_writer`/`controller_writer`), the dispatch reply path (`send_frame`) needs\r\n    /// that same lock, so any request/reply the non-draining consumer issues is starved\r\n    /// (dead-peer-independent — the v0.30.4 field re-wedge). The supervised daemon brain\r\n    /// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn\r\n    /// only in `net_status()`/`sessions()` drain-and-discard loops; it drives no PTY\r\n    /// session — brainproc.rs:184 — and nothing on its side consumes the output, §3\r\n    /// guard), so it MUST call [`Self::resume_session_cursors`] (cursor-only, NO\r\n    /// subscribe) instead. This subscribing method stays for a DRAINING caller (the\r\n    /// resume int tests today; the forward live-agent-adapter / Option-B drained carrier).\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    // [impl->REQ-DAEMON-2]\r\n    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\r\n    // [doc->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {\r\n        let reply = self.sessions()?;\r\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\r\n        for info in reply.sessions {\r\n            // Seed the dedup cursor at the broker's resume point BEFORE the subscribe\r\n            // replay starts arriving, then re-attach (Viewer) from there.\r\n            self.session_cursors\r\n                .insert(info.session_id, info.resume_seq);\r\n            // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\r\n            self.subscribe_with(info.session_id, info.resume_seq, AttachIntent::Viewer, None)?;\r\n            resumed.push(info.session_id);\r\n        }\r\n        Ok(resumed)\r\n    }\r\n\r\n    /// Cursor-only resume for a NON-DRAINING caller (the supervised daemon brain): query\r\n    /// every hosted session and seed [`Self::session_cursors`] at each `resume_seq`,\r\n    /// returning the re-attached ids — but DO **NOT** subscribe. This is the safe resume\r\n    /// the daemon `run_brain` heartbeat uses in place of [`Self::resume_sessions`]:\r\n    /// carrying only request/reply on the brain's conn (no UNDRAINED subscription) makes\r\n    /// the `SharedSend` self-deadlock structurally impossible (UPDATE-WEDGE round 3,\r\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK — see the deadlock warning on\r\n    /// [`Self::resume_sessions`]). The daemon brain drives no broker PTY session\r\n    /// (brainproc.rs:184) and nothing on its side consumes session output (§3 guard), so\r\n    /// the observation cursor is all it needs; no attach also means no controller steal\r\n    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL, a fortiori). Rides the brain-swap ⇒ seamless.\r\n    /// When genuinely daemon-brain-driven sessions land (the live-agent adapter future),\r\n    /// attach THOSE on a DEDICATED drained carrier (Option B, deferred) — never on this\r\n    /// request/reply conn.\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\r\n    pub fn resume_session_cursors(&mut self) -> io::Result<Vec<u64>> {\r\n        let reply = self.sessions()?;\r\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\r\n        for info in reply.sessions {\r\n            self.session_cursors\r\n                .insert(info.session_id, info.resume_seq);\r\n            resumed.push(info.session_id);\r\n        }\r\n        Ok(resumed)\r\n    }\r\n\r\n    /// The per-session resume cursor (the next seq still needed for a session\r","truncated":true,"truncatedBy":"lines","totalLines":1155,"totalBytes":4098,"outputLines":65,"outputBytes":4098,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    /// REQ-BRAIN-RESUME-NO-CONTROL-STEAL). Genuinely daemon-brain-driven sessions (the\r\n    /// live-agent adapter future) will re-attach as Control on a drained carrier; that\r\n    /// set is empty today.\r\n    ///\r\n    /// ⚠️ DEADLOCK WARNING — the CALLER MUST DRAIN this conn (UPDATE-WEDGE round 3,\r\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK). This subscribes the sessions onto THIS conn.\r\n    /// A NON-DRAINING consumer will SELF-DEADLOCK: an actively-streaming session backs\r\n    /// the conn up, the broker-side subscriber writer blocks holding the conn's single\r\n    /// `SharedSend` mutex ACROSS a blocking `write_frame` (broker.rs\r\n    /// `viewer_writer`/`controller_writer`), the dispatch reply path (`send_frame`) needs\r\n    /// that same lock, so any request/reply the non-draining consumer issues is starved\r\n    /// (dead-peer-independent — the v0.30.4 field re-wedge). The supervised daemon brain\r\n    /// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn\r\n    /// only in `net_status()`/`sessions()` drain-and-discard loops; it drives no PTY\r\n    /// session — brainproc.rs:184 — and nothing on its side consumes the output, §3\r\n    /// guard), so it MUST call [`Self::resume_session_cursors`] (cursor-only, NO\r\n    /// subscribe) instead. This subscribing method stays for a DRAINING caller (the\r\n    /// resume int tests today; the forward live-agent-adapter / Option-B drained carrier).\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    // [impl->REQ-DAEMON-2]\r\n    // [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\r\n    // [doc->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {\r\n        let reply = self.sessions()?;\r\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\r\n        for info in reply.sessions {\r\n            // Seed the dedup cursor at the broker's resume point BEFORE the subscribe\r\n            // replay starts arriving, then re-attach (Viewer) from there.\r\n            self.session_cursors\r\n                .insert(info.session_id, info.resume_seq);\r\n            // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\r\n            self.subscribe_with(info.session_id, info.resume_seq, AttachIntent::Viewer, None)?;\r\n            resumed.push(info.session_id);\r\n        }\r\n        Ok(resumed)\r\n    }\r\n\r\n    /// Cursor-only resume for a NON-DRAINING caller (the supervised daemon brain): query\r\n    /// every hosted session and seed [`Self::session_cursors`] at each `resume_seq`,\r\n    /// returning the re-attached ids — but DO **NOT** subscribe. This is the safe resume\r\n    /// the daemon `run_brain` heartbeat uses in place of [`Self::resume_sessions`]:\r\n    /// carrying only request/reply on the brain's conn (no UNDRAINED subscription) makes\r\n    /// the `SharedSend` self-deadlock structurally impossible (UPDATE-WEDGE round 3,\r\n    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK — see the deadlock warning on\r\n    /// [`Self::resume_sessions`]). The daemon brain drives no broker PTY session\r\n    /// (brainproc.rs:184) and nothing on its side consumes session output (§3 guard), so\r\n    /// the observation cursor is all it needs; no attach also means no controller steal\r\n    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL, a fortiori). Rides the brain-swap ⇒ seamless.\r\n    /// When genuinely daemon-brain-driven sessions land (the live-agent adapter future),\r\n    /// attach THOSE on a DEDICATED drained carrier (Option B, deferred) — never on this\r\n    /// request/reply conn.\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]\r\n    pub fn resume_session_cursors(&mut self) -> io::Result<Vec<u64>> {\r\n        let reply = self.sessions()?;\r\n        let mut resumed = Vec::with_capacity(reply.sessions.len());\r\n        for info in reply.sessions {\r\n            self.session_cursors\r\n                .insert(info.session_id, info.resume_seq);\r\n            resumed.push(info.session_id);\r\n        }\r\n        Ok(resumed)\r\n    }\r\n\r\n    /// The per-session resume cursor (the next seq still needed for a session\r","startLine":1024,"lineNumbers":[1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2178,"totalBytes":4098,"outputLines":65,"outputBytes":4098,"shownRange":{"start":1024,"end":1088},"nextOffset":1089}}},"isError":false,"timestamp":1784196133123}}
{"type":"message","id":"db9b9375","parentId":"14723961","timestamp":"2026-07-16T10:02:21.573Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reviewing spawn_net_consumers dispatch logic**"},{"type":"toolCall","id":"call_LNuBWnQzWf8k1xsoFgPJJjyf|fc_047bb49263dfa303016a58ac2d8f708198b288752cabbe85f0","name":"grep","arguments":{"i":"Locating net consumer startup","pattern":"run_dispatch_loop|spawn_net_consumers|BRAIN_NET_CONSUMERS_UP","path":"crates/spt-daemon/src/brainproc.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4145,"output":96,"cacheRead":189952,"cacheWrite":0,"totalTokens":194193,"reasoningTokens":22,"cost":{"input":0.020725,"output":0.00288,"cacheRead":0.09497599999999999,"cacheWrite":0,"total":0.11858099999999999}},"stopReason":"toolUse","timestamp":1784196133137,"responseId":"resp_047bb49263dfa303016a58ac259c3081988bbe5a1f0229d1e1","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58ac275ed08198994c8f60985015c3","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKwtZ3PfVQyLGLOMLxo-IjNGUn0DQhsPHw1Pgxw0NwfF3S2nbZX2qIzLgUH_AhEOlNX7INpNil1G7iGShlLu98lW65bhKeSmq_cCcy7faJsnZn9E8jDHc6cc4EMBRmbWmRaYfSaMa0ehrNGW7ryrQS3S6KLfceRfb8LlDl18gxyeq30mRgBLq8-xDaAVt31cJIObfKkM3EUwSi2xXu072Gb-rg4bGnb7OsZj9AoSKQyioKZAAc6wDBtQFT71yBMzn4bqM7g35BqDF8sOX2JqPpiQZN7cYDL-Ku-pgAqD-oBm2nHVaXTSVIVV0GoHriy2OKW04fEX_i-wqmu4LK6mfAniPR8JFHw5RmEyOwPyTRf6la9PfshUBxGlzJKPdvP6T9b-m2OhApOvszCHQZRCpVnpnM3F6yGtFhswh0b24pq52Gl0ZYFJtNEvCuSKz3nnXxHvBJGxx85uvKWu7_BkgfOOSP43fQbrjfcOkVYoTCoSGwLIOoJbF0z0tQNoJt1v3p_kG6FLCtRliO1eb_nVUNa2E8v0FQ8D-B97V6wArX4HdC8XN4LT75i6eIXvG0KddgG2JWK3eDvE2fAc-d9ljHy2fvXltC_6Qv2PWNhWRdH4IT5zcoJQByJXHa0roo10YXgGhzSr_kh3S-aTsXwh-_d6woDPZXMT8GIHqYn7hJo_1q0lSLKfoVGK2L4Wtuf0PbWIo1NYV7EPVyiyNYX5-f_fbVdyD7gxoKAlnCTbz2UoXv6Arm86GDS1_P4Yje9ROwNTUj12sRoFlRMD7UoVni7qv7vxiFfI705gVPLbYZZZnvGOBMR4QAtdfpdPkH6kaJ9iujkeEivqZDWHcaUt7OG5Ch8ueSEAkCM_dKmT-k_g8ZYPpOBOfLD3ctGSLu-As887odLZCa5wdbwux66mhGjKJCMyZHWi584eb4medthdveRmnzeN3UmesOeYgDGSwwDjPe0EnVmgGZU8fSNxB-DY6RgyIViXqA7a1qe_BbGHCBZKcm7hyND-VK7sIpB3lVND","summary":[{"type":"summary_text","text":"**Reviewing spawn_net_consumers dispatch logic**"}]},{"id":"fc_047bb49263dfa303016a58ac2d8f708198b288752cabbe85f0","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating net consumer startup\",\"pattern\":\"run_dispatch_loop|spawn_net_consumers|BRAIN_NET_CONSUMERS_UP\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_LNuBWnQzWf8k1xsoFgPJJjyf","name":"grep"}]},"duration":8435.554099999368,"ttft":2209.3865999998525,"contextSnapshot":{"promptTokens":194097,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_LNuBWnQzWf8k1xsoFgPJJjyf|fc_047bb49263dfa303016a58ac2d8f708198b288752cabbe85f0","toolName":"grep","startedAt":"2026-07-16T10:02:21.574Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Locating net consumer startup"},"id":"ce70d2ef","parentId":"db9b9375","timestamp":"2026-07-16T10:02:21.574Z"}
{"type":"message","id":"0fd09957","parentId":"ce70d2ef","timestamp":"2026-07-16T10:02:21.575Z","message":{"role":"toolResult","toolCallId":"call_LNuBWnQzWf8k1xsoFgPJJjyf|fc_047bb49263dfa303016a58ac2d8f708198b288752cabbe85f0","toolName":"grep","content":[{"type":"text","text":" 272|            let hex = status.node_id_hex.clone().unwrap_or_default();\n*273|            spawn_net_consumers(&hex);\n 274|            consumers_started = true;\n 275|            eprintln!(\n*276|                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n 277|            );\n 278|        }\n 279|        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n...\n 320|// [impl->REQ-DAEMON-9]\n*321|fn spawn_net_consumers(node_hex: &str) {\n 322|    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\n 323|    let stop = Arc::new(AtomicBool::new(false));\n 324|    let _ = crate::dispatch::spawn_dispatcher("}],"details":{"scopePath":"crates/spt-daemon/src/brainproc.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":3,"fileCount":1,"files":["crates/spt-daemon/src/brainproc.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/brainproc.rs","count":3}],"truncated":false,"displayContent":" 272│            let hex = status.node_id_hex.clone().unwrap_or_default();\n*273│            spawn_net_consumers(&hex);\n 274│            consumers_started = true;\n 275│            eprintln!(\n*276│                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n 277│            );\n 278│        }\n 279│        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n    │...\n 320│// [impl->REQ-DAEMON-9]\n*321│fn spawn_net_consumers(node_hex: &str) {\n 322│    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\n 323│    let stop = Arc::new(AtomicBool::new(false));\n 324│    let _ = crate::dispatch::spawn_dispatcher("},"isError":false,"timestamp":1784196141575}}
{"type":"message","id":"1e457fa4","parentId":"0fd09957","timestamp":"2026-07-16T10:02:21.576Z","message":{"role":"user","content":"Your current interruptible wait was interrupted because an IRC message arrived from your parent agent `Main`.\n\nParent IRC message:\n\nRCA sent to Doyle with both stale finished-stream takeover as primary and evicted-opener one-shot failure as adjacent root. Please finalize now; no further exploration needed.","attribution":"agent","timestamp":1784196135937,"steering":true}}
{"type":"message","id":"59ad6958","parentId":"1e457fa4","timestamp":"2026-07-16T10:04:40.881Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Structuring detailed architecture summary**\n\n**Detailing PTY update and stream invariants**\n\n**Formulating PTY failure hypotheses**"},{"type":"thinking","thinking":""},{"type":"toolCall","id":"call_VAGz8Kk2kSa94TfeEBfxC6Rx|fc_047bb49263dfa303016a58ac3e5f608198ae8b2d8774e9614d","name":"yield","arguments":{"type":null,"result":{"data":{"summary":"The supervised daemon’s cursor-only `BRAIN_RESUMED` path is not, by itself, the missing reattach: after a brain refresh the new brain starts a fresh inbound dispatcher, whose empty per-generation `claimed` set enumerates all broker-held peer streams and attempts to re-serve each attach. The strongest code-grounded causal candidate is instead stale finished attach-stream replay: broker `NetShared.streams` retains finished rows indefinitely, the new dispatcher reclaims them alongside the live stream, and a stale same-origin `Control` Request can silently replace the live controller before its replayed EOF clears the slot. The original live worker gets no `Displaced`, remains connected, and receives no more PTY output; the existing `spt rc` therefore looks frozen. Intentional detach plus a new `spt rc` creates a fresh stream that takes the now-free controller slot after the stale replay storm. A second, independently code-grounded failure is opener eviction: the ordinary stream ring retains only 4096 transport chunks, so a long-lived active stream can lose its seq-0 Request; restart classification from cursor 0 then fails with a forward stream gap and the one-shot claim is never retried. Either mechanism naturally acts per stream, not globally; the reported 4-of-5 split is therefore plausible [INFERENCE], depending on stale-stream history, retained opener, and concurrent reclaim ordering.","files":[{"path":"crates/spt/src/rc.rs","description":"`run_attach_inner`, `establish_attach`, `attach_viewport`, and `pump` define the operator side. Local control uses a broker loopback connection; remote control uses QUIC, but both converge on `request_attach_endpoint` and `net_stream_subscribe`. `pump` sends Input/Resize and renders AttachRecord Output. Ctrl-B D finishes the operator stream. Only `BrokerGone`/`Severed` trigger automatic full re-establishment; a previously-active but subsequently silent stream emits neither. `FIRST_EVENT_GRACE` only fires when no event has ever arrived, so an established viewport that freezes after earlier output can wait indefinitely."},{"path":"crates/spt-daemon/src/attach.rs","description":"`serve_attach` is the target-side worker. Its primary `Brain` connection first becomes the attach stream subscriber and, after replaying `AttachRecord::Request`, becomes the PTY controller/viewer via `attach_as`; therefore one physical broker connection can be labeled both `stream-subscriber stream=X` and `controller session=Y`. A second `wire` Brain connection forwards PTY Output/Size/Exit onto the network stream. Replayed EOF calls `detach_session`, explicitly releasing the worker’s current PTY role. `read_controller_event_resuming` handles PTY-output gaps but cannot notice that another same-origin worker silently superseded its controller slot."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"`run_dispatch_loop` owns a process-local `HashSet<u64> claimed`, initialized empty on every brain generation. It enumerates every `NetStreamInfo` with `!initiated_locally && next_seq > 0`, inserts the stream id before starting a worker, and never retries that id during the generation even if the worker fails. `worker` calls `peek_first_line(stream_id, 0)`, opens a fresh Brain connection, classifies the replayed opener, and invokes `serve_attach(..., net_from_seq=0)`. This refutes the theory that a new brain intentionally ignores all active remote-control streams, but exposes stale-row replay and permanent one-shot failure."},{"path":"crates/spt-daemon/src/nethost.rs","description":"`NetShared` is broker-resident and owns `conns`, `streams`, monotonic connection/stream ids, and each `StreamEntry`. `StreamLog` has one replaceable subscriber, a bounded 4096-transport-chunk ordinary ring, and `finished`. `finish` sets `finished=true` and sends EOF but does not remove the `StreamEntry`; no stream-table removal path exists. `StreamLog::attach` replaces the subscriber and replays retained seqs plus EOF. `detach_if` uses `Arc::ptr_eq`, so late cleanup cannot detach a newer subscriber. These are the central persistent data-plane facts."},{"path":"crates/spt-daemon/src/broker.rs","description":"`OutputLog::resolve_subscribe` implements controller/viewer ownership. A `Control` subscribe with the same `by` identity silently calls `become_controller`, replacing the incumbent with no `Displaced`; only a different identity’s explicit `Take` is loud. `become_controller` bumps `controller_epoch`, replaces the sink, and spawns a new writer. `detach_if` clears only the role currently owned by that physical connection. `handle_conn` tracks session and net-stream subscriptions and runs pointer-conditional cleanup on EOF. `dispatch_spawn` owns real per-session PTY drain/exit tasks. The writer epoch protects against superseded writer bytes, but does not prevent a stale replay worker from legitimately becoming the newest controller and then explicitly detaching itself."},{"path":"crates/spt-daemon/src/conn.rs","description":"`BrokerConn` is the bounded, cancelable, poison-on-failure physical broker connection. `CONN_ID_SEQ`, role attribution, `CONN_LIFECYCLE`, `CONN_WRITE_POISONED`, and `CONN_WRITE_RETIRED` provide the observability needed to distinguish a logical stale-stream takeover from a half-open/black-holed IPC carrier. Its watchdog aborts a write that exceeds the production brain-write deadline and physically retires the connection."},{"path":"crates/spt-daemon/src/brain.rs","description":"`resume_session_cursors` seeds PTY-session cursors without subscribing the daemon heartbeat connection; `resume_sessions` is the dangerous subscribing variant reserved for draining callers. `net_stream_subscribe` seeds an exact stream cursor, and `read_event` rejects a forward `NetStreamData` gap. Consequently, if seq 0 has fallen out of a stream ring, `peek_first_line(...,0)` receives the retained floor and errors `stream X gap: got seq N want 0` rather than classifying the stream."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"`run_brain` uses `resume_session_cursors`, writes readiness, and after the first successful 500 ms `net_status` starts `spawn_net_consumers`, including a fresh dispatcher. Thus `BRAIN_RESUMED` refers to broker PTY-session cursors, not restoration of attach protocol workers. The stable broker process remains the owner of PTYs, NetHost connections, and stream rows while these brain workers die and respawn."},{"path":"crates/spt-term/src/pty.rs","description":"`PtySession` owns one real child PTY’s master reader, serialized master writer, child, killer, and single-take reader. This confirms that terminal compute and the PTY data plane remain in the broker process; an `spt rc` viewport or daemon brain worker owns no PTY handle."},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","description":"Production CLI refresh coverage proves brain PID/generation changes while the broker PID, harness PID, and ONLINE perch survive. It does not keep an `spt rc` process attached, create stale finished attach streams, or assert pre/post-refresh output progress on the same viewport, leaving the reported interaction boundary uncovered."},{"path":"crates/spt-daemon/tests/attach.rs","description":"Relevant tests include `attach_survives_target_brain_restart_exactly_once`, `controller_viewer_matrix_and_loud_take`, `same_origin_re_subscribe_does_not_displace`, `controller_restart_with_viewer_no_displace_and_viewer_survives`, `wedged_viewer_does_not_stall_controller`, and detach/stamp cases. The restart test manually re-serves one known stream before ring rollover; it does not restart the production dispatcher over a table containing both stale finished and active streams. The same-origin test directly confirms the silent replacement behavior needed by the primary hypothesis."},{"path":"crates/spt/src/rc.rs#tests","description":"`attach_viewport_reconnects_across_a_broker_bounce` validates frontend reconnection when the entire broker dies and the rc carrier gets EOF. It creates a replacement broker/session, unlike brain-only refresh where the broker stream remains open. `reconnect_class_only_severed_transport_redrives` explicitly makes Detached, Displaced, Stalled, and NoLiveSession final. This explains why a silent logical controller loss does not self-heal in the existing rc process."},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","description":"`daemon_resume_leaves_zero_brain_subscribers` structurally proves that the supervised heartbeat connection uses cursor-only PTY resume and attaches zero session viewers; a separate hazard arm proves the old subscribing seam is observable. This test addresses the earlier SharedSend self-deadlock, not re-serving broker-held attach streams."},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","description":"Process-level coverage proves daemon brain respawn does not bulk-steal active PTY controller slots through `resume_sessions`. It does not exercise per-stream dispatcher reclaim and therefore does not rule out stale attach workers taking the same controllers."},{"path":"crates/spt-daemon/tests/brain_decouple.rs","description":"`suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks` and the Windows physical-retirement leg reproduce a non-reading controller and assert bounded logical/physical release plus fresh attach. They cover black-holed physical connections, not stale finished stream replay after a normal brain generation rollover."},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","description":"Five-invariant Windows harness verifies unrelated-session progress, physical connection cancellation, writer/handler exit, fresh viewer attach, and no retained lock/task after a black-holed controller. It is the best diagnostic comparator for hypothesis 4."},{"path":"crates/spt/tests/attach_wedge_e2e.rs","description":"Real-daemon coverage kills an rc pump and PTY child, then proves a different fresh endpoint/attach still works and daemon stop remains bounded. It does not assert continuity of the original active rc across a brain refresh."},{"path":"crates/spt-daemon/tests/control_stamp_lifetime.rs","description":"Verifies controller/viewer stamps clear on PTY reap and converge on broker truth. Useful for observing the primary mechanism: after a stale replay worker detaches, broker truth should show no current controller even though the original rc stream and serve loop remain open."},{"path":"docs/KNOWN-HAZARDS.md","description":"Binding related hazards include 6.7 broker/brain process isolation; 7.2 restart idempotency; 7.7 viewer isolation; 7.11 dropped attach pump; 7.12 off-drain controller writes; 7.19 no-ack input flood; and 7.21 one controller writer/epoch ordering. None currently states the stale-finished-stream replay invariant."},{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","description":"Defines the stable broker as PTY/socket owner beneath a freely restartable brain. This is why refresh/update can preserve the terminal data plane while losing and reconstructing per-stream controller workers."},{"path":"docs/adr/0031-server-side-screen-grid-render-repaint.md","description":"Defines cold PTY attach as a synthesized current-screen repaint rather than raw output-ring replay. It affects PTY OutputLog replay after control is reacquired, but does not preserve or classify the separate attach protocol’s seq-0 Request."},{"path":"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","description":"Historical RCA for the old undrained brain-connection SharedSend self-deadlock. The current cursor-only daemon resume fixes that mechanism; the present investigation distinguishes it from production attach-worker reconstruction."},{"path":"docs/DEBUG-ROLLOUT.md","description":"Reiterates that a broker-touching candidate is refused while broker-held endpoints are live; safe field work should use brain-only refresh/update boundaries or an isolated test home, never force a broker-breaking apply over live PTYs."}],"architecture":"Ownership and call chain\n\n1. PTY data plane: `spt_term::PtySession` owns the real master reader/writer and child. `Broker::dispatch_spawn` registers a `HostedSession`, starts one PTY drain and one exit waiter, and stores `OutputLog`. This process and state survive a brain-only refresh/update.\n2. Operator/viewer connection: `spt rc` creates its own pump-mode `Brain`. For a local endpoint `establish_attach` resolves the broker session, mints a loopback connection and paired streams; remotely it dials QUIC. It writes `AttachRecord::Request`, subscribes its locally-initiated stream, forwards terminal Input/Resize, and renders Output records.\n3. Target controller/viewer connection: the daemon dispatcher claims the peer-initiated paired stream. `worker` peeks the opener, then `serve_attach` uses a fresh primary Brain connection to subscribe that stream and to subscribe the PTY session as Controller/Viewer. That one physical broker IPC connection therefore owns two logical roles. A separate `wire` Brain connection forwards session output to the stream, avoiding mutual IPC backpressure.\n4. Daemon brain lifecycle: refresh/update ends all target-side dispatcher threads and their broker IPC connections, but not the broker’s PTY sessions, NetHost connections, or `StreamEntry` rows. The replacement brain’s heartbeat starts a fresh dispatcher. Its empty local `claimed` set attempts to re-serve all peer-initiated stream rows from cursor 0.\n5. Broker lifecycle: physical Brain-IPC disconnect cleanup is pointer-conditional; old cleanup cannot unseat a genuinely newer controller/subscriber. Controller writers also carry an epoch so a superseded writer cannot continue an initial replay. These protections handle late physical cleanup and byte ordering, but deliberately permit a new same-origin worker to silently retake the controller.\n\nPrimary causal candidate — stale finished-stream takeover\n\n`StreamLog::finish` retains the stream row forever. On brain replacement, all historical finished peer attach streams are indistinguishable from active rows to `run_dispatch_loop`: each has `initiated_locally=false` and `next_seq>0`. Their buffered Request and EOF are replayed. For a stale control stream with the same stable origin identity as the current local operator, `resolve_subscribe` takes the `same_identity` branch and calls `become_controller`; this silently replaces the newly restored active worker and intentionally emits no `Displaced`. The stale worker then reads its buffered EOF and calls `detach_session`; because it is now the current controller, the unsubscribe clears the slot. The legitimate active worker remains alive, retains `role=Controller` in its local state, and its operator stream remains unfinished, but the broker no longer routes PTY output to it. No EOF, error, or kick reaches the existing rc frontend. Since that rc already rendered earlier output, its first-event stall guard is permanently disarmed. A fresh rc creates a new stream and a new worker that takes the free slot, matching the observed recovery.\n\nWhy only 4 of 5 [INFERENCE]: stream reclaim is per row and concurrent. Endpoints differ in historical finished-stream count, whether old Request records remain retained, and which worker performs the final same-origin take. A session with no usable stale stream, or whose active worker attaches after all stale EOF workers finish, remains live; one with a stale worker attaching last is left controllerless. This mechanism predicts correlation with attach/detach history and nondeterministic HashMap/thread ordering, not with PTY survival.\n\nAdjacent causal candidate — opener eviction plus one-shot claim\n\nOrdinary `StreamLog` retains 4096 transport read chunks, not logical Attach records. A sufficiently old/input-active control stream can evict its seq-0 Request. The fresh dispatcher still calls `peek_first_line(stream,0)`. `Brain::net_stream_subscribe` seeds cursor 0, the broker replays only from its surviving ring floor, and `Brain::read_event` rejects the first seq as a forward gap. `worker` returns `Failed(\"stream X gap: got seq N want 0\")`. Because `claimed.insert(stream_id)` happened before spawning the worker, that stream is never retried in the generation. Detach/reattach creates a new stream with a retained opener and therefore succeeds. This predicts failure by accumulated operator-side transport chunks even without historical detached streams.\n\nFive falsifiable hypotheses, ordered\n\nH1 — stale finished attach rows replay and logically steal/clear control. Confirm with logs showing, after `BRAIN_NET_CONSUMERS_UP`, multiple `stream-sub-attach` records for old finished stream ids, then `controller-replaced` from the active controller to a stale worker, followed by `session-detach was_controller=true`; the final session snapshot has no controller while its PTY output high-water advances and the active operator stream is unfinished. Falsify if finished stream rows are absent or never process a Request/Control after refresh.\n\nH2 — the active attach opener rolled out of the 4096-chunk stream ring. Confirm `NetStreamInfo.next_seq > 4096` and dispatcher `DISPATCH:<id>:Failed(\"stream <id> gap: got seq <floor> want 0\")`, with no subsequent worker attempt for that id. Falsify if seq 0/Request is retained and classification/serve completes.\n\nH3 — another transient per-stream worker failure becomes permanent because claim is one-shot. Any temporary broker-connect, access, session-resolution, or replay error after `claimed.insert` abandons the stream until the next brain generation. Confirm one `DISPATCH:<id>:Failed(...)`, no retry despite the row remaining active, and immediate success when the same operation is manually retried/new stream opened. Falsify if no worker failure occurs or a later retry is observed.\n\nH4 — a half-open old physical Brain connection black-holes controller writes. This is lower probability in the current tree because `BrokerConn` has a bounded watchdog, physical poison/retire, stall eviction, pointer-conditional detach, and same-origin takeover. Confirm with `CONN_WRITE_POISONED`, writer deadline, delayed `transport-close`, and controller slot held by that same old conn until eviction. Falsify with prompt old-conn close plus the stale logical replace/detach sequence. This mechanism is already substantially covered by `brain_decouple` and `conn_blackhole_lifecycle`.\n\nH5 — the target controller falls irrecoverably behind the PTY OutputLog during the brain gap. Confirm the explicit `ControllerIrrecoverablyBehind` truncation path and a finished target send side; the operator should then see EOF and enter its severed reconnect class, so a truly silent indefinitely-open freeze would falsify it. This is less consistent with the reported symptom than H1/H2.\n\nSafe deterministic feedback loop proposal\n\nA. Minimal deterministic stale-take test, real broker/NetHost/PTY but no machine daemon: use a temp `SPT_HOME`, loopback-only NetHost, and a ticker/echo PTY. Create a finished same-origin attach stream containing Request+EOF and keep its broker row. Establish a separate active same-origin controller stream, wait until its operator sees a marker, and barrier on broker controller ownership. Then deliberately re-serve the finished stream after the active barrier. Assert, before any teardown: (1) the stale worker resolves Controller without Displaced; (2) its EOF path returns Detached; (3) broker controller truth becomes empty; (4) the active operator stream remains unfinished/open; (5) PTY/ring high-water continues; (6) the active operator receives no post-event marker within a bounded window; and (7) a newly-created attach stream receives that marker and becomes controller. This ordering makes the red state deterministic rather than relying on thread scheduling.\n\nB. Deterministic opener-eviction arm: give a test NetHost a tiny ordinary stream-ring cap or send more than the real 4096 independently-read chunks after the Request. Restart only the dispatcher generation and assert the first replay is a forward gap, the worker fails once, the row remains, and no retry occurs. The desired future contract should make the attach opener/classification durable independently of the evicting payload ring or otherwise expose an explicit retained floor/retry outcome; the test should assert behavior, not source text.\n\nC. Production-topology capstone: extend `daemon_refresh_e2e.rs` under its isolated temp home. Bring up five mock ticker endpoints. For four, intentionally create and cleanly finish one or more preliminary rc streams; leave the fifth without stale history. Start five persistent `spt rc` children with piped stdin held open, prove each sees a pre-refresh tick, run the real `spt daemon refresh` with a deadline, verify the broker and PTY PIDs remain unchanged, then require every same rc process to see a post-generation tick and round-trip a unique input marker without detach/reattach. On failure, capture brain stderr plus stream table (`stream_id`, `finished`, `initiated_locally`, `next_seq`) and session controller identity/connection id. Reap only test-scoped children. Do not invoke production `spt update`, mutate real endpoints, or use a shared `SPT_HOME`.\n\nD. Diagnostics that discriminate without guesswork: correlate `BRAIN_UP`/`BRAIN_NET_CONSUMERS_UP`, `DISPATCH:<stream>`, `CONN_LIFECYCLE` (`stream-sub-attach`, `controller-attach`, `controller-replaced`, `session-detach`, `transport-close`), `CONN_WRITE_POISONED`/`RETIRED`, stream `finished/next_seq`, session controller truth, and PTY `next_seq`. H1 yields clean logical replace/detach without a poisoned conn; H2 yields a stream cursor gap and no controller attach; H4 yields deadline poison/physical retirement. The existing `BRAIN_RESUMED` line is not evidence that attach streams were or were not re-served—it only reports PTY cursor reconstruction."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":2507,"output":5236,"cacheRead":192000,"cacheWrite":0,"totalTokens":199743,"reasoningTokens":532,"cost":{"input":0.012535000000000001,"output":0.15708,"cacheRead":0.096,"cacheWrite":0,"total":0.265615}},"stopReason":"toolUse","timestamp":1784196141596,"responseId":"resp_047bb49263dfa303016a58ac2e050c8198a0de62c9e978e564","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_047bb49263dfa303016a58ac3015648198b574de663c73fcca","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKw-OPWgwWv7ME5023ycYXAPGHMANlD6tKxh5SKRk_nRORsZ2jgHPLbn2skRqSFookvaH1g8WJrutGGw4minY_ZY5iEr8Ghn4R_d43Jp3UZyXSw_5jgHbS1MbUAk2hlsWHzwud0MiiTpUjArDgHdm1QqnKOD4exh523Z-3h5RFETFtGus2rp1QjxO710x14boaSCUxKAYjd1V_N2hYM_jIhZ8r7Eot2xkmuIiDLFIijLS045ML02W1KAGRwN3yx-_3H7c0uvOOMoUa0EJUO5UWkPOH9fH0L5AcRcIkTLKxxikwG6UxUWjI1_siEgxxrZhuJ2ZH9_uRRu9vHa0d_whkcC8_KOmYuyh5kGCogmXZ3dMcD_7-9SPPXHGVd4YbXZgIw8CbJcCU5Vx0SN4l9ToC5Zgf8amsfPhBMAUjsYPNI_roOxXJ0R7oIUJeeHno8n3YzBUVqiLdWr63LaUV6LLpLxm4JwG5yFFWAl3gkeQwaLRhL7Ur9r4W6Z6bl62ibdHADHStHsswuTQbfXLM4r8_EHXPA0dzIxXDW_7HKkgjkQsdxAtQTKZJzhtjdvThfYnDWX3pMjmMG1dA_o0jaaIt0HGMqdkszazeDfDul1iB5IZcoAKFUhGWdoXDc4hOazZTarOHPkZz_qkB3Cyw7JWA64sN_vhqxqcNd9nTGODmEdwgctH0wDHMV9NLP3ACBjW__UtjOhIVejd-_cm6RazoV4bR4c5IPKN1SBfsvGzxkK1Cjp8NbkA8Ewj6KxCfTWOq3QK1uVhE_TSmr2vmkzzFY2v41xIQ4Do6fPA6e0rLSXWeL9DnPp9ONz5_UofyO9xoSqufk7ck5fslCLJ92OuOopJwPZvgzg5M_LzQsBYzL-IaLc09jNHixZFMgcG3-1POdTa26tVMeklRncOzzVZgy8PKexet5oeTEdHlYGWfEIxdjotGzlJnQju6kMBxdPhmoRDPqvKI9cy-Ak9ZenTTynrESQsmLWJ_LQ9EaJZxbkihV2OcwOvUeLPBvlM71rPk7yjTFlYRLfukqb4fUC8fte2lJVhkW_x0d-_AKtzjQOIZXVjKlJ-KxZ2js9EgkE3NSqiEp9i0EiJsco1k_0gkUMpyu9qYYAnn_oFFXuwM-urJ9xdqx6P7N1_6BQf80OLnwIFi3z42GRttJ0wwUu_t-gMHHxhvYJ7AR5DJgJ74n-9HlbGa1Imenz3mM9JFGhjyNpL291EgnKP_nz6J_WwpDUWwDXvlwmoETH_49F6wqEsbNIgrvE7ujX9T-Wx32mQMY4Es_MOK31jkBc_oCaxLlppfL4AFFcVoptjyWGDddUiOF_wRSuiAUuzI_VBLZoV3tJTTRosnmHD3_SeUspgn8Y_a_HO0DFgPLT7LjmC8pVKRB7DXCKLr1Q_PTuCwGGx3xsq9jaXdFu-87S2NI43FaMKgVhUbxFjy0gVSfEMAKAqQmD9R56UsqW_EAsnwZP2K7_VQLwXHIsqo_kHczpnGHqGfXe35yOhopyYpfn_qFiO7Xlzc9ryIcOaBCYt-mEUHJxXaiy58dixDDsM7stkON-JbrpLDv73jJsO7feCv4z0LqkBySy69wpKESqnZDYMmcQqiGBFCwugGv_M9pqmXQg7qcIVu9F8Jz85Ps6Iv2RN2WOW8w62C26mcz7tww4nWAEKZo9b4AXmMkh1nRQfQPOabcFwo5qIg0v_UNk53Q0X49JL2lVKksLQTFTd6YwPX-fOiIp2elG--jfhW79ptpcxn0F0VfJ5b-qtlPReuyRCfloRSn6N6kshv2Auvc6rJIKIh1Udh7A2nInCAb7xAmxfGxr34dtMolM8BBpsSIuLkj6L_Fd3ImZEN_5oMS00Tc9WubgU39GwWC772VkVElfh3uKhZdaNE-y6D84XCO3bU3TUvkXwjLDrqSV2zlAufG8jPd2p4FyWfsXlnVpFPDR575FI38r6F9IScDBPrd3Fkpjo-jNrNnRZ1i8RwbV4MrjClDYPuXG58ntSZlzp5NDWlcqbI6SkOlMf25f26wizESEk1VT78Vj1tJC0Cs3gdFkc6jfX8yWi1CefDcUkcdsOVH0R3n0DQ8kuewR99iGnrilqH9CSGebcmrV4IlMKU1MwGrkTJBrjIlVwU6VEYvPhNLkU7F14xAKimbs4WYnTM8nOPSPUWbym2cPGqJ9KALzzCFVnOGYo0-K20Ugd5GIqn0vRx4x9TJvEMy0tQ5JIn7SWdVLmjblBLdYM7rWIGVtr8lIHAwqE0GlXekdwKdRaI5SKFE2OOk99MldygMxK6Ru_eY07TjiKGeMBo6brnvEJ-eLUAGkEER9BNCOh0kvucddCQ3NAiPwUXfMVDDWHyxMgJOPTPuTvQ7t60tpJsIwTo0Acoc_RIhA-fr1_q9iC1B0wd0tN8KQMIocwfeYOymRPuFTRsLRsac3Pyp1Pv-40jh92rbF_7AUYVDY5TH3YyL2PLmq5HxXgHq9_R7MITMyWwte19-_ootjzBkN2J9KOjPRqHsyiCDJJ6tzJQ0mfnjOAt6E-dncdWlrF80hD9cPLx8A8pYceWzQL3C_FHMG5lB4NKIBJhS7be1LRKeMiyFqX2zWoojeEyD1_vER_RlnFucgQE-o8gjsbK49mTrvhJmAY-WMz_2WbCu1RIUPidY2MyU8W7wBdr4AyjeNH1sDKO7sjzM9DDXCGRUL2YFruvnc7xzTTyXBturmZCnnFgy-NcAx7Hk9pvABNNp_34tS-wZEk8V45YtwVwTNP4zt1TusoM_4eK3PIZtTV6WaCrcrdcuk01h0UibFYFHhivmYWvGWBGpuabXHAJL_bDMW8Vysp8AeNvUwq13MJrfNAP8O-ykvVYwlATOukGCONHaj-NV4BaHQvOnbyEpV4t3jPrny27zn2UFjirkxW4buO9YtOsUPbJS64q3kmlD_1F0kOTRwzr4JOMRJ1nNPwSEM5sy_wiZafSGWqejBICh86nQugRgiF_93uF6X1PDqgopns0bQc6y5DzIyEljmgYIrcGdey4E8tFFazbPjOlqNXlNg5wXnRRG5Spv5nb0EdPt0RNYG49jzuXAxsRxlkt0wi2ak0wLC_US6wsw-N8ykx9uwifTzMQQKjosPYPQqU3aa51KmSQuEYl6dSdv84fc-fRwlaFuUuWRyRwD-uGqjxLf4JCQhvNQEg_6KUCpoXcZZ-Nbv4tmW_jvoy8YUgIUrVnlkzXxJgzuwjWDov7eaLrSd2v-NYCEAX-T2NNkmVOnZNiCURA4uxVZOw9mLBMU_kX101_Zc88_jlqr7ZfCX_ZR8byDctrAHDa00zPU_lADm36AlVztOuTMQe16znfG4NwLJoXDZo6IV7qJqnap9dDmapdNJC9fdNvczGpvRYJaRizG0fsiZsxX5DRtVYw5_RdW4Wj94Vms6WumvLrxbIC3oE29xU_vlL-iBcL3pIb8qq-1IMA9QOsGejmffDzQ4D1QxXadNHg_vsqeR_7PYGOS3aM54oFrBciPI_ywTFegdyxylvAhzhxtF_lgMAc4kgw0ebm-n5IezXwsr136FlT16dAoyxDLOwCNwU6mWw2D--tV4kfNmXcvtumDhWiWW6C2fDGhp61m7mYrgbVnvjE-0XjwOEKXqojzOP0pSdkaF2guykfO_QCGMyYUMZgA0fGZNW6p3sMqn9vUH85I6akBNtp1zzWCef63TSdfxHL3Ds1sn2Q5knXAsPo5EjESAsKh56fW6QFFDfu6DG6WfcJckNq1cf62tj9OSVZ_YoKQp0cNQ-UKOMWcqiVk93NoAHyOM4UvBPaMFjqVRUJL4V_VQzctpPsbRNfPnFwWjk8SIlW8An75bGvH69S1hLjucXls4zRCcIRUp9-0Rcm3RVjo0In3qvFE8tiq_aQ5FY3mjBdj42I4k6z-y6gqNenvekTTe3gYY_tV6RGEa_kgzvXj4cKC8lYX9O7IKsd7EpJEscU5gx--_FY8Bg8RPjfqp4NZb0kvscSBTnFNZgtckxb9QZ-bxJ5x1Kc0BrltBCJSufWkrGrdKeIHPhJpKwTR6zJLM0WU0YYkmImXUOra8BxbkJ6LJIzM-DooBBTb4nmSpyND8ooku14LbLSeSu6oGHAfGUnpE3_CQMM3TiHyIV-pdPC_u870lhnzGQCLMGLjAgbTqB9duv9XVHxSddQuo6PgGTRmZ_pQuBjIL2mU4RWSA-fcwarOIVtG2RCJ9_hw=","summary":[{"type":"summary_text","text":"**Structuring detailed architecture summary**"},{"type":"summary_text","text":"**Detailing PTY update and stream invariants**"},{"type":"summary_text","text":"**Formulating PTY failure hypotheses**"}]},{"id":"rs_047bb49263dfa303016a58ac3e1e048198b9844b5c52b582ea","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWKw-EapkIsAhzSS6ijlOCTmtmQ-9-e54KEi7uIwGSZweMMgMULIKjIJzl_JPq_VsUJ-FZvFMkggrIYH5HyzA79flvJO_cht8uYJThMAO8P7VnQGuxzWi1PH9AI2r-hGIdhJMCHbxsnmCFDRNAxuymn0ZT3M4WHyf0dVQw_jo9a3h_rmsDDKQindPbWqFhNdNzPkBzkJeb5sZIs6z64jsgNq2qAKuLm0gFDOQRM0fikc2zH457wtFQNWsWMSkv1eKeUz_Uq0U-j7zNVd8rZy1rtsIjSZJv08VR8Tc4t7jLvMAkvVPT0GkjRID4VreoNOpqirTd_Do4-B5hEwL0alZm80yMDknPZ-MLffBKePpCeEqPG3_jn7hXA28oIdvqaZtfbVrNCRUMCv3NqNntmniaOZFNZv3MjpT-9rHfQRgJr7KPjfHVXiCN8c-5rMh1sNkkbJwD6TOeIna9rqm3iyAHX24aHfvcRHSRfIWnYb8Z4faWIINXUZSXoQiyGHVadf9K7xy7iVvF-4U-wF8xzKAvscS50hMdkvtOkZofXzGvVFr2sK2twjlxihVIKijO3XRD7Cs4g-5xb_ofk_djj7vbWgITJLQCf_cr7jvbnQc5B35MTRwNRQMaJ92orDuyI-mH_v24rCIIviMJK-EUc4FO3bQqPhrc471cSkoQhlB0WBDA6V4hC0no0IPEDB7KHFmh3gcl9CTp6z9MZX37yV-iHo4DjyYnEaICGuiDSr6kEzdzx_mb28k0Ipptyd12JbPCxV6bNeGM36TbFSQOQjuUVl4clCxCEuQ_mJM22m9CVrLnplecnII61vDxSbx0gErsCjP5aqtSpVpQ-yBmzw-4WYWaTMUx8Xu75e_1BCMug9u8uUvOVWR_Zrvwla7p1aWmlaVDLElnYZ_uL93kqyImRe_8PVk3dQlspV5_e55mWfYOh3pZFTcJ7PmFPNSwu2kfI49J48NIUboVi20t9U5fqV5l0FzsEjXDOCNTM40cjBWR_Q=","summary":[]},{"id":"fc_047bb49263dfa303016a58ac3e5f608198ae8b2d8774e9614d","type":"function_call","status":"completed","arguments":"{\"type\":null,\"result\":{\"data\":{\"summary\":\"The supervised daemon’s cursor-only `BRAIN_RESUMED` path is not, by itself, the missing reattach: after a brain refresh the new brain starts a fresh inbound dispatcher, whose empty per-generation `claimed` set enumerates all broker-held peer streams and attempts to re-serve each attach. The strongest code-grounded causal candidate is instead stale finished attach-stream replay: broker `NetShared.streams` retains finished rows indefinitely, the new dispatcher reclaims them alongside the live stream, and a stale same-origin `Control` Request can silently replace the live controller before its replayed EOF clears the slot. The original live worker gets no `Displaced`, remains connected, and receives no more PTY output; the existing `spt rc` therefore looks frozen. Intentional detach plus a new `spt rc` creates a fresh stream that takes the now-free controller slot after the stale replay storm. A second, independently code-grounded failure is opener eviction: the ordinary stream ring retains only 4096 transport chunks, so a long-lived active stream can lose its seq-0 Request; restart classification from cursor 0 then fails with a forward stream gap and the one-shot claim is never retried. Either mechanism naturally acts per stream, not globally; the reported 4-of-5 split is therefore plausible [INFERENCE], depending on stale-stream history, retained opener, and concurrent reclaim ordering.\",\"files\":[{\"path\":\"crates/spt/src/rc.rs\",\"description\":\"`run_attach_inner`, `establish_attach`, `attach_viewport`, and `pump` define the operator side. Local control uses a broker loopback connection; remote control uses QUIC, but both converge on `request_attach_endpoint` and `net_stream_subscribe`. `pump` sends Input/Resize and renders AttachRecord Output. Ctrl-B D finishes the operator stream. Only `BrokerGone`/`Severed` trigger automatic full re-establishment; a previously-active but subsequently silent stream emits neither. `FIRST_EVENT_GRACE` only fires when no event has ever arrived, so an established viewport that freezes after earlier output can wait indefinitely.\"},{\"path\":\"crates/spt-daemon/src/attach.rs\",\"description\":\"`serve_attach` is the target-side worker. Its primary `Brain` connection first becomes the attach stream subscriber and, after replaying `AttachRecord::Request`, becomes the PTY controller/viewer via `attach_as`; therefore one physical broker connection can be labeled both `stream-subscriber stream=X` and `controller session=Y`. A second `wire` Brain connection forwards PTY Output/Size/Exit onto the network stream. Replayed EOF calls `detach_session`, explicitly releasing the worker’s current PTY role. `read_controller_event_resuming` handles PTY-output gaps but cannot notice that another same-origin worker silently superseded its controller slot.\"},{\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"description\":\"`run_dispatch_loop` owns a process-local `HashSet<u64> claimed`, initialized empty on every brain generation. It enumerates every `NetStreamInfo` with `!initiated_locally && next_seq > 0`, inserts the stream id before starting a worker, and never retries that id during the generation even if the worker fails. `worker` calls `peek_first_line(stream_id, 0)`, opens a fresh Brain connection, classifies the replayed opener, and invokes `serve_attach(..., net_from_seq=0)`. This refutes the theory that a new brain intentionally ignores all active remote-control streams, but exposes stale-row replay and permanent one-shot failure.\"},{\"path\":\"crates/spt-daemon/src/nethost.rs\",\"description\":\"`NetShared` is broker-resident and owns `conns`, `streams`, monotonic connection/stream ids, and each `StreamEntry`. `StreamLog` has one replaceable subscriber, a bounded 4096-transport-chunk ordinary ring, and `finished`. `finish` sets `finished=true` and sends EOF but does not remove the `StreamEntry`; no stream-table removal path exists. `StreamLog::attach` replaces the subscriber and replays retained seqs plus EOF. `detach_if` uses `Arc::ptr_eq`, so late cleanup cannot detach a newer subscriber. These are the central persistent data-plane facts.\"},{\"path\":\"crates/spt-daemon/src/broker.rs\",\"description\":\"`OutputLog::resolve_subscribe` implements controller/viewer ownership. A `Control` subscribe with the same `by` identity silently calls `become_controller`, replacing the incumbent with no `Displaced`; only a different identity’s explicit `Take` is loud. `become_controller` bumps `controller_epoch`, replaces the sink, and spawns a new writer. `detach_if` clears only the role currently owned by that physical connection. `handle_conn` tracks session and net-stream subscriptions and runs pointer-conditional cleanup on EOF. `dispatch_spawn` owns real per-session PTY drain/exit tasks. The writer epoch protects against superseded writer bytes, but does not prevent a stale replay worker from legitimately becoming the newest controller and then explicitly detaching itself.\"},{\"path\":\"crates/spt-daemon/src/conn.rs\",\"description\":\"`BrokerConn` is the bounded, cancelable, poison-on-failure physical broker connection. `CONN_ID_SEQ`, role attribution, `CONN_LIFECYCLE`, `CONN_WRITE_POISONED`, and `CONN_WRITE_RETIRED` provide the observability needed to distinguish a logical stale-stream takeover from a half-open/black-holed IPC carrier. Its watchdog aborts a write that exceeds the production brain-write deadline and physically retires the connection.\"},{\"path\":\"crates/spt-daemon/src/brain.rs\",\"description\":\"`resume_session_cursors` seeds PTY-session cursors without subscribing the daemon heartbeat connection; `resume_sessions` is the dangerous subscribing variant reserved for draining callers. `net_stream_subscribe` seeds an exact stream cursor, and `read_event` rejects a forward `NetStreamData` gap. Consequently, if seq 0 has fallen out of a stream ring, `peek_first_line(...,0)` receives the retained floor and errors `stream X gap: got seq N want 0` rather than classifying the stream.\"},{\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"description\":\"`run_brain` uses `resume_session_cursors`, writes readiness, and after the first successful 500 ms `net_status` starts `spawn_net_consumers`, including a fresh dispatcher. Thus `BRAIN_RESUMED` refers to broker PTY-session cursors, not restoration of attach protocol workers. The stable broker process remains the owner of PTYs, NetHost connections, and stream rows while these brain workers die and respawn.\"},{\"path\":\"crates/spt-term/src/pty.rs\",\"description\":\"`PtySession` owns one real child PTY’s master reader, serialized master writer, child, killer, and single-take reader. This confirms that terminal compute and the PTY data plane remain in the broker process; an `spt rc` viewport or daemon brain worker owns no PTY handle.\"},{\"path\":\"crates/spt/tests/daemon_refresh_e2e.rs\",\"description\":\"Production CLI refresh coverage proves brain PID/generation changes while the broker PID, harness PID, and ONLINE perch survive. It does not keep an `spt rc` process attached, create stale finished attach streams, or assert pre/post-refresh output progress on the same viewport, leaving the reported interaction boundary uncovered.\"},{\"path\":\"crates/spt-daemon/tests/attach.rs\",\"description\":\"Relevant tests include `attach_survives_target_brain_restart_exactly_once`, `controller_viewer_matrix_and_loud_take`, `same_origin_re_subscribe_does_not_displace`, `controller_restart_with_viewer_no_displace_and_viewer_survives`, `wedged_viewer_does_not_stall_controller`, and detach/stamp cases. The restart test manually re-serves one known stream before ring rollover; it does not restart the production dispatcher over a table containing both stale finished and active streams. The same-origin test directly confirms the silent replacement behavior needed by the primary hypothesis.\"},{\"path\":\"crates/spt/src/rc.rs#tests\",\"description\":\"`attach_viewport_reconnects_across_a_broker_bounce` validates frontend reconnection when the entire broker dies and the rc carrier gets EOF. It creates a replacement broker/session, unlike brain-only refresh where the broker stream remains open. `reconnect_class_only_severed_transport_redrives` explicitly makes Detached, Displaced, Stalled, and NoLiveSession final. This explains why a silent logical controller loss does not self-heal in the existing rc process.\"},{\"path\":\"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs\",\"description\":\"`daemon_resume_leaves_zero_brain_subscribers` structurally proves that the supervised heartbeat connection uses cursor-only PTY resume and attaches zero session viewers; a separate hazard arm proves the old subscribing seam is observable. This test addresses the earlier SharedSend self-deadlock, not re-serving broker-held attach streams.\"},{\"path\":\"crates/spt/tests/resume_no_control_steal_e2e.rs\",\"description\":\"Process-level coverage proves daemon brain respawn does not bulk-steal active PTY controller slots through `resume_sessions`. It does not exercise per-stream dispatcher reclaim and therefore does not rule out stale attach workers taking the same controllers.\"},{\"path\":\"crates/spt-daemon/tests/brain_decouple.rs\",\"description\":\"`suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks` and the Windows physical-retirement leg reproduce a non-reading controller and assert bounded logical/physical release plus fresh attach. They cover black-holed physical connections, not stale finished stream replay after a normal brain generation rollover.\"},{\"path\":\"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs\",\"description\":\"Five-invariant Windows harness verifies unrelated-session progress, physical connection cancellation, writer/handler exit, fresh viewer attach, and no retained lock/task after a black-holed controller. It is the best diagnostic comparator for hypothesis 4.\"},{\"path\":\"crates/spt/tests/attach_wedge_e2e.rs\",\"description\":\"Real-daemon coverage kills an rc pump and PTY child, then proves a different fresh endpoint/attach still works and daemon stop remains bounded. It does not assert continuity of the original active rc across a brain refresh.\"},{\"path\":\"crates/spt-daemon/tests/control_stamp_lifetime.rs\",\"description\":\"Verifies controller/viewer stamps clear on PTY reap and converge on broker truth. Useful for observing the primary mechanism: after a stale replay worker detaches, broker truth should show no current controller even though the original rc stream and serve loop remain open.\"},{\"path\":\"docs/KNOWN-HAZARDS.md\",\"description\":\"Binding related hazards include 6.7 broker/brain process isolation; 7.2 restart idempotency; 7.7 viewer isolation; 7.11 dropped attach pump; 7.12 off-drain controller writes; 7.19 no-ack input flood; and 7.21 one controller writer/epoch ordering. None currently states the stale-finished-stream replay invariant.\"},{\"path\":\"docs/adr/0018-broker-brain-process-isolation-restoration.md\",\"description\":\"Defines the stable broker as PTY/socket owner beneath a freely restartable brain. This is why refresh/update can preserve the terminal data plane while losing and reconstructing per-stream controller workers.\"},{\"path\":\"docs/adr/0031-server-side-screen-grid-render-repaint.md\",\"description\":\"Defines cold PTY attach as a synthesized current-screen repaint rather than raw output-ring replay. It affects PTY OutputLog replay after control is reacquired, but does not preserve or classify the separate attach protocol’s seq-0 Request.\"},{\"path\":\"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md\",\"description\":\"Historical RCA for the old undrained brain-connection SharedSend self-deadlock. The current cursor-only daemon resume fixes that mechanism; the present investigation distinguishes it from production attach-worker reconstruction.\"},{\"path\":\"docs/DEBUG-ROLLOUT.md\",\"description\":\"Reiterates that a broker-touching candidate is refused while broker-held endpoints are live; safe field work should use brain-only refresh/update boundaries or an isolated test home, never force a broker-breaking apply over live PTYs.\"}],\"architecture\":\"Ownership and call chain\\n\\n1. PTY data plane: `spt_term::PtySession` owns the real master reader/writer and child. `Broker::dispatch_spawn` registers a `HostedSession`, starts one PTY drain and one exit waiter, and stores `OutputLog`. This process and state survive a brain-only refresh/update.\\n2. Operator/viewer connection: `spt rc` creates its own pump-mode `Brain`. For a local endpoint `establish_attach` resolves the broker session, mints a loopback connection and paired streams; remotely it dials QUIC. It writes `AttachRecord::Request`, subscribes its locally-initiated stream, forwards terminal Input/Resize, and renders Output records.\\n3. Target controller/viewer connection: the daemon dispatcher claims the peer-initiated paired stream. `worker` peeks the opener, then `serve_attach` uses a fresh primary Brain connection to subscribe that stream and to subscribe the PTY session as Controller/Viewer. That one physical broker IPC connection therefore owns two logical roles. A separate `wire` Brain connection forwards session output to the stream, avoiding mutual IPC backpressure.\\n4. Daemon brain lifecycle: refresh/update ends all target-side dispatcher threads and their broker IPC connections, but not the broker’s PTY sessions, NetHost connections, or `StreamEntry` rows. The replacement brain’s heartbeat starts a fresh dispatcher. Its empty local `claimed` set attempts to re-serve all peer-initiated stream rows from cursor 0.\\n5. Broker lifecycle: physical Brain-IPC disconnect cleanup is pointer-conditional; old cleanup cannot unseat a genuinely newer controller/subscriber. Controller writers also carry an epoch so a superseded writer cannot continue an initial replay. These protections handle late physical cleanup and byte ordering, but deliberately permit a new same-origin worker to silently retake the controller.\\n\\nPrimary causal candidate — stale finished-stream takeover\\n\\n`StreamLog::finish` retains the stream row forever. On brain replacement, all historical finished peer attach streams are indistinguishable from active rows to `run_dispatch_loop`: each has `initiated_locally=false` and `next_seq>0`. Their buffered Request and EOF are replayed. For a stale control stream with the same stable origin identity as the current local operator, `resolve_subscribe` takes the `same_identity` branch and calls `become_controller`; this silently replaces the newly restored active worker and intentionally emits no `Displaced`. The stale worker then reads its buffered EOF and calls `detach_session`; because it is now the current controller, the unsubscribe clears the slot. The legitimate active worker remains alive, retains `role=Controller` in its local state, and its operator stream remains unfinished, but the broker no longer routes PTY output to it. No EOF, error, or kick reaches the existing rc frontend. Since that rc already rendered earlier output, its first-event stall guard is permanently disarmed. A fresh rc creates a new stream and a new worker that takes the free slot, matching the observed recovery.\\n\\nWhy only 4 of 5 [INFERENCE]: stream reclaim is per row and concurrent. Endpoints differ in historical finished-stream count, whether old Request records remain retained, and which worker performs the final same-origin take. A session with no usable stale stream, or whose active worker attaches after all stale EOF workers finish, remains live; one with a stale worker attaching last is left controllerless. This mechanism predicts correlation with attach/detach history and nondeterministic HashMap/thread ordering, not with PTY survival.\\n\\nAdjacent causal candidate — opener eviction plus one-shot claim\\n\\nOrdinary `StreamLog` retains 4096 transport read chunks, not logical Attach records. A sufficiently old/input-active control stream can evict its seq-0 Request. The fresh dispatcher still calls `peek_first_line(stream,0)`. `Brain::net_stream_subscribe` seeds cursor 0, the broker replays only from its surviving ring floor, and `Brain::read_event` rejects the first seq as a forward gap. `worker` returns `Failed(\\\"stream X gap: got seq N want 0\\\")`. Because `claimed.insert(stream_id)` happened before spawning the worker, that stream is never retried in the generation. Detach/reattach creates a new stream with a retained opener and therefore succeeds. This predicts failure by accumulated operator-side transport chunks even without historical detached streams.\\n\\nFive falsifiable hypotheses, ordered\\n\\nH1 — stale finished attach rows replay and logically steal/clear control. Confirm with logs showing, after `BRAIN_NET_CONSUMERS_UP`, multiple `stream-sub-attach` records for old finished stream ids, then `controller-replaced` from the active controller to a stale worker, followed by `session-detach was_controller=true`; the final session snapshot has no controller while its PTY output high-water advances and the active operator stream is unfinished. Falsify if finished stream rows are absent or never process a Request/Control after refresh.\\n\\nH2 — the active attach opener rolled out of the 4096-chunk stream ring. Confirm `NetStreamInfo.next_seq > 4096` and dispatcher `DISPATCH:<id>:Failed(\\\"stream <id> gap: got seq <floor> want 0\\\")`, with no subsequent worker attempt for that id. Falsify if seq 0/Request is retained and classification/serve completes.\\n\\nH3 — another transient per-stream worker failure becomes permanent because claim is one-shot. Any temporary broker-connect, access, session-resolution, or replay error after `claimed.insert` abandons the stream until the next brain generation. Confirm one `DISPATCH:<id>:Failed(...)`, no retry despite the row remaining active, and immediate success when the same operation is manually retried/new stream opened. Falsify if no worker failure occurs or a later retry is observed.\\n\\nH4 — a half-open old physical Brain connection black-holes controller writes. This is lower probability in the current tree because `BrokerConn` has a bounded watchdog, physical poison/retire, stall eviction, pointer-conditional detach, and same-origin takeover. Confirm with `CONN_WRITE_POISONED`, writer deadline, delayed `transport-close`, and controller slot held by that same old conn until eviction. Falsify with prompt old-conn close plus the stale logical replace/detach sequence. This mechanism is already substantially covered by `brain_decouple` and `conn_blackhole_lifecycle`.\\n\\nH5 — the target controller falls irrecoverably behind the PTY OutputLog during the brain gap. Confirm the explicit `ControllerIrrecoverablyBehind` truncation path and a finished target send side; the operator should then see EOF and enter its severed reconnect class, so a truly silent indefinitely-open freeze would falsify it. This is less consistent with the reported symptom than H1/H2.\\n\\nSafe deterministic feedback loop proposal\\n\\nA. Minimal deterministic stale-take test, real broker/NetHost/PTY but no machine daemon: use a temp `SPT_HOME`, loopback-only NetHost, and a ticker/echo PTY. Create a finished same-origin attach stream containing Request+EOF and keep its broker row. Establish a separate active same-origin controller stream, wait until its operator sees a marker, and barrier on broker controller ownership. Then deliberately re-serve the finished stream after the active barrier. Assert, before any teardown: (1) the stale worker resolves Controller without Displaced; (2) its EOF path returns Detached; (3) broker controller truth becomes empty; (4) the active operator stream remains unfinished/open; (5) PTY/ring high-water continues; (6) the active operator receives no post-event marker within a bounded window; and (7) a newly-created attach stream receives that marker and becomes controller. This ordering makes the red state deterministic rather than relying on thread scheduling.\\n\\nB. Deterministic opener-eviction arm: give a test NetHost a tiny ordinary stream-ring cap or send more than the real 4096 independently-read chunks after the Request. Restart only the dispatcher generation and assert the first replay is a forward gap, the worker fails once, the row remains, and no retry occurs. The desired future contract should make the attach opener/classification durable independently of the evicting payload ring or otherwise expose an explicit retained floor/retry outcome; the test should assert behavior, not source text.\\n\\nC. Production-topology capstone: extend `daemon_refresh_e2e.rs` under its isolated temp home. Bring up five mock ticker endpoints. For four, intentionally create and cleanly finish one or more preliminary rc streams; leave the fifth without stale history. Start five persistent `spt rc` children with piped stdin held open, prove each sees a pre-refresh tick, run the real `spt daemon refresh` with a deadline, verify the broker and PTY PIDs remain unchanged, then require every same rc process to see a post-generation tick and round-trip a unique input marker without detach/reattach. On failure, capture brain stderr plus stream table (`stream_id`, `finished`, `initiated_locally`, `next_seq`) and session controller identity/connection id. Reap only test-scoped children. Do not invoke production `spt update`, mutate real endpoints, or use a shared `SPT_HOME`.\\n\\nD. Diagnostics that discriminate without guesswork: correlate `BRAIN_UP`/`BRAIN_NET_CONSUMERS_UP`, `DISPATCH:<stream>`, `CONN_LIFECYCLE` (`stream-sub-attach`, `controller-attach`, `controller-replaced`, `session-detach`, `transport-close`), `CONN_WRITE_POISONED`/`RETIRED`, stream `finished/next_seq`, session controller truth, and PTY `next_seq`. H1 yields clean logical replace/detach without a poisoned conn; H2 yields a stream cursor gap and no controller attach; H4 yields deadline poison/physical retirement. The existing `BRAIN_RESUMED` line is not evidence that attach streams were or were not re-served—it only reports PTY cursor reconstruction.\"}}}","call_id":"call_VAGz8Kk2kSa94TfeEBfxC6Rx","name":"yield"}]},"duration":139269.42420000024,"ttft":2471.1931000007316,"contextSnapshot":{"promptTokens":194507,"nonMessageTokens":15087}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_VAGz8Kk2kSa94TfeEBfxC6Rx|fc_047bb49263dfa303016a58ac3e5f608198ae8b2d8774e9614d","toolName":"yield","startedAt":"2026-07-16T10:04:40.881Z"},"id":"697c9541","parentId":"59ad6958","timestamp":"2026-07-16T10:04:40.881Z"}
{"type":"message","id":"0877158d","parentId":"697c9541","timestamp":"2026-07-16T10:04:40.882Z","message":{"role":"toolResult","toolCallId":"call_VAGz8Kk2kSa94TfeEBfxC6Rx|fc_047bb49263dfa303016a58ac3e5f608198ae8b2d8774e9614d","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"The supervised daemon’s cursor-only `BRAIN_RESUMED` path is not, by itself, the missing reattach: after a brain refresh the new brain starts a fresh inbound dispatcher, whose empty per-generation `claimed` set enumerates all broker-held peer streams and attempts to re-serve each attach. The strongest code-grounded causal candidate is instead stale finished attach-stream replay: broker `NetShared.streams` retains finished rows indefinitely, the new dispatcher reclaims them alongside the live stream, and a stale same-origin `Control` Request can silently replace the live controller before its replayed EOF clears the slot. The original live worker gets no `Displaced`, remains connected, and receives no more PTY output; the existing `spt rc` therefore looks frozen. Intentional detach plus a new `spt rc` creates a fresh stream that takes the now-free controller slot after the stale replay storm. A second, independently code-grounded failure is opener eviction: the ordinary stream ring retains only 4096 transport chunks, so a long-lived active stream can lose its seq-0 Request; restart classification from cursor 0 then fails with a forward stream gap and the one-shot claim is never retried. Either mechanism naturally acts per stream, not globally; the reported 4-of-5 split is therefore plausible [INFERENCE], depending on stale-stream history, retained opener, and concurrent reclaim ordering.","files":[{"path":"crates/spt/src/rc.rs","description":"`run_attach_inner`, `establish_attach`, `attach_viewport`, and `pump` define the operator side. Local control uses a broker loopback connection; remote control uses QUIC, but both converge on `request_attach_endpoint` and `net_stream_subscribe`. `pump` sends Input/Resize and renders AttachRecord Output. Ctrl-B D finishes the operator stream. Only `BrokerGone`/`Severed` trigger automatic full re-establishment; a previously-active but subsequently silent stream emits neither. `FIRST_EVENT_GRACE` only fires when no event has ever arrived, so an established viewport that freezes after earlier output can wait indefinitely."},{"path":"crates/spt-daemon/src/attach.rs","description":"`serve_attach` is the target-side worker. Its primary `Brain` connection first becomes the attach stream subscriber and, after replaying `AttachRecord::Request`, becomes the PTY controller/viewer via `attach_as`; therefore one physical broker connection can be labeled both `stream-subscriber stream=X` and `controller session=Y`. A second `wire` Brain connection forwards PTY Output/Size/Exit onto the network stream. Replayed EOF calls `detach_session`, explicitly releasing the worker’s current PTY role. `read_controller_event_resuming` handles PTY-output gaps but cannot notice that another same-origin worker silently superseded its controller slot."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"`run_dispatch_loop` owns a process-local `HashSet<u64> claimed`, initialized empty on every brain generation. It enumerates every `NetStreamInfo` with `!initiated_locally && next_seq > 0`, inserts the stream id before starting a worker, and never retries that id during the generation even if the worker fails. `worker` calls `peek_first_line(stream_id, 0)`, opens a fresh Brain connection, classifies the replayed opener, and invokes `serve_attach(..., net_from_seq=0)`. This refutes the theory that a new brain intentionally ignores all active remote-control streams, but exposes stale-row replay and permanent one-shot failure."},{"path":"crates/spt-daemon/src/nethost.rs","description":"`NetShared` is broker-resident and owns `conns`, `streams`, monotonic connection/stream ids, and each `StreamEntry`. `StreamLog` has one replaceable subscriber, a bounded 4096-transport-chunk ordinary ring, and `finished`. `finish` sets `finished=true` and sends EOF but does not remove the `StreamEntry`; no stream-table removal path exists. `StreamLog::attach` replaces the subscriber and replays retained seqs plus EOF. `detach_if` uses `Arc::ptr_eq`, so late cleanup cannot detach a newer subscriber. These are the central persistent data-plane facts."},{"path":"crates/spt-daemon/src/broker.rs","description":"`OutputLog::resolve_subscribe` implements controller/viewer ownership. A `Control` subscribe with the same `by` identity silently calls `become_controller`, replacing the incumbent with no `Displaced`; only a different identity’s explicit `Take` is loud. `become_controller` bumps `controller_epoch`, replaces the sink, and spawns a new writer. `detach_if` clears only the role currently owned by that physical connection. `handle_conn` tracks session and net-stream subscriptions and runs pointer-conditional cleanup on EOF. `dispatch_spawn` owns real per-session PTY drain/exit tasks. The writer epoch protects against superseded writer bytes, but does not prevent a stale replay worker from legitimately becoming the newest controller and then explicitly detaching itself."},{"path":"crates/spt-daemon/src/conn.rs","description":"`BrokerConn` is the bounded, cancelable, poison-on-failure physical broker connection. `CONN_ID_SEQ`, role attribution, `CONN_LIFECYCLE`, `CONN_WRITE_POISONED`, and `CONN_WRITE_RETIRED` provide the observability needed to distinguish a logical stale-stream takeover from a half-open/black-holed IPC carrier. Its watchdog aborts a write that exceeds the production brain-write deadline and physically retires the connection."},{"path":"crates/spt-daemon/src/brain.rs","description":"`resume_session_cursors` seeds PTY-session cursors without subscribing the daemon heartbeat connection; `resume_sessions` is the dangerous subscribing variant reserved for draining callers. `net_stream_subscribe` seeds an exact stream cursor, and `read_event` rejects a forward `NetStreamData` gap. Consequently, if seq 0 has fallen out of a stream ring, `peek_first_line(...,0)` receives the retained floor and errors `stream X gap: got seq N want 0` rather than classifying the stream."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"`run_brain` uses `resume_session_cursors`, writes readiness, and after the first successful 500 ms `net_status` starts `spawn_net_consumers`, including a fresh dispatcher. Thus `BRAIN_RESUMED` refers to broker PTY-session cursors, not restoration of attach protocol workers. The stable broker process remains the owner of PTYs, NetHost connections, and stream rows while these brain workers die and respawn."},{"path":"crates/spt-term/src/pty.rs","description":"`PtySession` owns one real child PTY’s master reader, serialized master writer, child, killer, and single-take reader. This confirms that terminal compute and the PTY data plane remain in the broker process; an `spt rc` viewport or daemon brain worker owns no PTY handle."},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","description":"Production CLI refresh coverage proves brain PID/generation changes while the broker PID, harness PID, and ONLINE perch survive. It does not keep an `spt rc` process attached, create stale finished attach streams, or assert pre/post-refresh output progress on the same viewport, leaving the reported interaction boundary uncovered."},{"path":"crates/spt-daemon/tests/attach.rs","description":"Relevant tests include `attach_survives_target_brain_restart_exactly_once`, `controller_viewer_matrix_and_loud_take`, `same_origin_re_subscribe_does_not_displace`, `controller_restart_with_viewer_no_displace_and_viewer_survives`, `wedged_viewer_does_not_stall_controller`, and detach/stamp cases. The restart test manually re-serves one known stream before ring rollover; it does not restart the production dispatcher over a table containing both stale finished and active streams. The same-origin test directly confirms the silent replacement behavior needed by the primary hypothesis."},{"path":"crates/spt/src/rc.rs#tests","description":"`attach_viewport_reconnects_across_a_broker_bounce` validates frontend reconnection when the entire broker dies and the rc carrier gets EOF. It creates a replacement broker/session, unlike brain-only refresh where the broker stream remains open. `reconnect_class_only_severed_transport_redrives` explicitly makes Detached, Displaced, Stalled, and NoLiveSession final. This explains why a silent logical controller loss does not self-heal in the existing rc process."},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","description":"`daemon_resume_leaves_zero_brain_subscribers` structurally proves that the supervised heartbeat connection uses cursor-only PTY resume and attaches zero session viewers; a separate hazard arm proves the old subscribing seam is observable. This test addresses the earlier SharedSend self-deadlock, not re-serving broker-held attach streams."},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","description":"Process-level coverage proves daemon brain respawn does not bulk-steal active PTY controller slots through `resume_sessions`. It does not exercise per-stream dispatcher reclaim and therefore does not rule out stale attach workers taking the same controllers."},{"path":"crates/spt-daemon/tests/brain_decouple.rs","description":"`suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks` and the Windows physical-retirement leg reproduce a non-reading controller and assert bounded logical/physical release plus fresh attach. They cover black-holed physical connections, not stale finished stream replay after a normal brain generation rollover."},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","description":"Five-invariant Windows harness verifies unrelated-session progress, physical connection cancellation, writer/handler exit, fresh viewer attach, and no retained lock/task after a black-holed controller. It is the best diagnostic comparator for hypothesis 4."},{"path":"crates/spt/tests/attach_wedge_e2e.rs","description":"Real-daemon coverage kills an rc pump and PTY child, then proves a different fresh endpoint/attach still works and daemon stop remains bounded. It does not assert continuity of the original active rc across a brain refresh."},{"path":"crates/spt-daemon/tests/control_stamp_lifetime.rs","description":"Verifies controller/viewer stamps clear on PTY reap and converge on broker truth. Useful for observing the primary mechanism: after a stale replay worker detaches, broker truth should show no current controller even though the original rc stream and serve loop remain open."},{"path":"docs/KNOWN-HAZARDS.md","description":"Binding related hazards include 6.7 broker/brain process isolation; 7.2 restart idempotency; 7.7 viewer isolation; 7.11 dropped attach pump; 7.12 off-drain controller writes; 7.19 no-ack input flood; and 7.21 one controller writer/epoch ordering. None currently states the stale-finished-stream replay invariant."},{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","description":"Defines the stable broker as PTY/socket owner beneath a freely restartable brain. This is why refresh/update can preserve the terminal data plane while losing and reconstructing per-stream controller workers."},{"path":"docs/adr/0031-server-side-screen-grid-render-repaint.md","description":"Defines cold PTY attach as a synthesized current-screen repaint rather than raw output-ring replay. It affects PTY OutputLog replay after control is reacquired, but does not preserve or classify the separate attach protocol’s seq-0 Request."},{"path":"docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","description":"Historical RCA for the old undrained brain-connection SharedSend self-deadlock. The current cursor-only daemon resume fixes that mechanism; the present investigation distinguishes it from production attach-worker reconstruction."},{"path":"docs/DEBUG-ROLLOUT.md","description":"Reiterates that a broker-touching candidate is refused while broker-held endpoints are live; safe field work should use brain-only refresh/update boundaries or an isolated test home, never force a broker-breaking apply over live PTYs."}],"architecture":"Ownership and call chain\n\n1. PTY data plane: `spt_term::PtySession` owns the real master reader/writer and child. `Broker::dispatch_spawn` registers a `HostedSession`, starts one PTY drain and one exit waiter, and stores `OutputLog`. This process and state survive a brain-only refresh/update.\n2. Operator/viewer connection: `spt rc` creates its own pump-mode `Brain`. For a local endpoint `establish_attach` resolves the broker session, mints a loopback connection and paired streams; remotely it dials QUIC. It writes `AttachRecord::Request`, subscribes its locally-initiated stream, forwards terminal Input/Resize, and renders Output records.\n3. Target controller/viewer connection: the daemon dispatcher claims the peer-initiated paired stream. `worker` peeks the opener, then `serve_attach` uses a fresh primary Brain connection to subscribe that stream and to subscribe the PTY session as Controller/Viewer. That one physical broker IPC connection therefore owns two logical roles. A separate `wire` Brain connection forwards session output to the stream, avoiding mutual IPC backpressure.\n4. Daemon brain lifecycle: refresh/update ends all target-side dispatcher threads and their broker IPC connections, but not the broker’s PTY sessions, NetHost connections, or `StreamEntry` rows. The replacement brain’s heartbeat starts a fresh dispatcher. Its empty local `claimed` set attempts to re-serve all peer-initiated stream rows from cursor 0.\n5. Broker lifecycle: physical Brain-IPC disconnect cleanup is pointer-conditional; old cleanup cannot unseat a genuinely newer controller/subscriber. Controller writers also carry an epoch so a superseded writer cannot continue an initial replay. These protections handle late physical cleanup and byte ordering, but deliberately permit a new same-origin worker to silently retake the controller.\n\nPrimary causal candidate — stale finished-stream takeover\n\n`StreamLog::finish` retains the stream row forever. On brain replacement, all historical finished peer attach streams are indistinguishable from active rows to `run_dispatch_loop`: each has `initiated_locally=false` and `next_seq>0`. Their buffered Request and EOF are replayed. For a stale control stream with the same stable origin identity as the current local operator, `resolve_subscribe` takes the `same_identity` branch and calls `become_controller`; this silently replaces the newly restored active worker and intentionally emits no `Displaced`. The stale worker then reads its buffered EOF and calls `detach_session`; because it is now the current controller, the unsubscribe clears the slot. The legitimate active worker remains alive, retains `role=Controller` in its local state, and its operator stream remains unfinished, but the broker no longer routes PTY output to it. No EOF, error, or kick reaches the existing rc frontend. Since that rc already rendered earlier output, its first-event stall guard is permanently disarmed. A fresh rc creates a new stream and a new worker that takes the free slot, matching the observed recovery.\n\nWhy only 4 of 5 [INFERENCE]: stream reclaim is per row and concurrent. Endpoints differ in historical finished-stream count, whether old Request records remain retained, and which worker performs the final same-origin take. A session with no usable stale stream, or whose active worker attaches after all stale EOF workers finish, remains live; one with a stale worker attaching last is left controllerless. This mechanism predicts correlation with attach/detach history and nondeterministic HashMap/thread ordering, not with PTY survival.\n\nAdjacent causal candidate — opener eviction plus one-shot claim\n\nOrdinary `StreamLog` retains 4096 transport read chunks, not logical Attach records. A sufficiently old/input-active control stream can evict its seq-0 Request. The fresh dispatcher still calls `peek_first_line(stream,0)`. `Brain::net_stream_subscribe` seeds cursor 0, the broker replays only from its surviving ring floor, and `Brain::read_event` rejects the first seq as a forward gap. `worker` returns `Failed(\"stream X gap: got seq N want 0\")`. Because `claimed.insert(stream_id)` happened before spawning the worker, that stream is never retried in the generation. Detach/reattach creates a new stream with a retained opener and therefore succeeds. This predicts failure by accumulated operator-side transport chunks even without historical detached streams.\n\nFive falsifiable hypotheses, ordered\n\nH1 — stale finished attach rows replay and logically steal/clear control. Confirm with logs showing, after `BRAIN_NET_CONSUMERS_UP`, multiple `stream-sub-attach` records for old finished stream ids, then `controller-replaced` from the active controller to a stale worker, followed by `session-detach was_controller=true`; the final session snapshot has no controller while its PTY output high-water advances and the active operator stream is unfinished. Falsify if finished stream rows are absent or never process a Request/Control after refresh.\n\nH2 — the active attach opener rolled out of the 4096-chunk stream ring. Confirm `NetStreamInfo.next_seq > 4096` and dispatcher `DISPATCH:<id>:Failed(\"stream <id> gap: got seq <floor> want 0\")`, with no subsequent worker attempt for that id. Falsify if seq 0/Request is retained and classification/serve completes.\n\nH3 — another transient per-stream worker failure becomes permanent because claim is one-shot. Any temporary broker-connect, access, session-resolution, or replay error after `claimed.insert` abandons the stream until the next brain generation. Confirm one `DISPATCH:<id>:Failed(...)`, no retry despite the row remaining active, and immediate success when the same operation is manually retried/new stream opened. Falsify if no worker failure occurs or a later retry is observed.\n\nH4 — a half-open old physical Brain connection black-holes controller writes. This is lower probability in the current tree because `BrokerConn` has a bounded watchdog, physical poison/retire, stall eviction, pointer-conditional detach, and same-origin takeover. Confirm with `CONN_WRITE_POISONED`, writer deadline, delayed `transport-close`, and controller slot held by that same old conn until eviction. Falsify with prompt old-conn close plus the stale logical replace/detach sequence. This mechanism is already substantially covered by `brain_decouple` and `conn_blackhole_lifecycle`.\n\nH5 — the target controller falls irrecoverably behind the PTY OutputLog during the brain gap. Confirm the explicit `ControllerIrrecoverablyBehind` truncation path and a finished target send side; the operator should then see EOF and enter its severed reconnect class, so a truly silent indefinitely-open freeze would falsify it. This is less consistent with the reported symptom than H1/H2.\n\nSafe deterministic feedback loop proposal\n\nA. Minimal deterministic stale-take test, real broker/NetHost/PTY but no machine daemon: use a temp `SPT_HOME`, loopback-only NetHost, and a ticker/echo PTY. Create a finished same-origin attach stream containing Request+EOF and keep its broker row. Establish a separate active same-origin controller stream, wait until its operator sees a marker, and barrier on broker controller ownership. Then deliberately re-serve the finished stream after the active barrier. Assert, before any teardown: (1) the stale worker resolves Controller without Displaced; (2) its EOF path returns Detached; (3) broker controller truth becomes empty; (4) the active operator stream remains unfinished/open; (5) PTY/ring high-water continues; (6) the active operator receives no post-event marker within a bounded window; and (7) a newly-created attach stream receives that marker and becomes controller. This ordering makes the red state deterministic rather than relying on thread scheduling.\n\nB. Deterministic opener-eviction arm: give a test NetHost a tiny ordinary stream-ring cap or send more than the real 4096 independently-read chunks after the Request. Restart only the dispatcher generation and assert the first replay is a forward gap, the worker fails once, the row remains, and no retry occurs. The desired future contract should make the attach opener/classification durable independently of the evicting payload ring or otherwise expose an explicit retained floor/retry outcome; the test should assert behavior, not source text.\n\nC. Production-topology capstone: extend `daemon_refresh_e2e.rs` under its isolated temp home. Bring up five mock ticker endpoints. For four, intentionally create and cleanly finish one or more preliminary rc streams; leave the fifth without stale history. Start five persistent `spt rc` children with piped stdin held open, prove each sees a pre-refresh tick, run the real `spt daemon refresh` with a deadline, verify the broker and PTY PIDs remain unchanged, then require every same rc process to see a post-generation tick and round-trip a unique input marker without detach/reattach. On failure, capture brain stderr plus stream table (`stream_id`, `finished`, `initiated_locally`, `next_seq`) and session controller identity/connection id. Reap only test-scoped children. Do not invoke production `spt update`, mutate real endpoints, or use a shared `SPT_HOME`.\n\nD. Diagnostics that discriminate without guesswork: correlate `BRAIN_UP`/`BRAIN_NET_CONSUMERS_UP`, `DISPATCH:<stream>`, `CONN_LIFECYCLE` (`stream-sub-attach`, `controller-attach`, `controller-replaced`, `session-detach`, `transport-close`), `CONN_WRITE_POISONED`/`RETIRED`, stream `finished/next_seq`, session controller truth, and PTY `next_seq`. H1 yields clean logical replace/detach without a poisoned conn; H2 yields a stream cursor gap and no controller attach; H4 yields deadline poison/physical retirement. The existing `BRAIN_RESUMED` line is not evidence that attach streams were or were not re-served—it only reports PTY cursor reconstruction."},"status":"success"},"isError":false,"timestamp":1784196280881}}
{"type":"message","id":"cd5c7fc3","parentId":"0877158d","timestamp":"2026-07-16T10:04:40.900Z","message":{"role":"assistant","content":[],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"aborted","errorMessage":"Request was aborted","errorId":134221824,"timestamp":1784196280897}}
{"type":"custom","customType":"session_exit","data":{"reason":"dispose","kind":"normal","recordedAt":"2026-07-16T10:11:40.917Z"},"id":"7234621b","parentId":"cd5c7fc3","timestamp":"2026-07-16T10:11:40.917Z"}
{"type":"custom_message","customType":"irc:incoming","content":"<irc>\nIncoming IRC message from agent `PtyPathRca`:\n\nI’m tracing crates/spt-daemon, spt-term, spt PTY+RC/session concurrency end-to-end. Please send any prior exact findings/locations on shared locks, queues, timeouts, viewer stalls.\nIf a response is expected, reply with the `irc` tool (`op: \"send\"`, `to: \"PtyPathRca\"`) — you may finish your current step first. Nobody replies on your behalf.\n</irc>","display":true,"details":{"id":"1532a8cd759c5f0c","from":"PtyPathRca","message":"I’m tracing crates/spt-daemon, spt-term, spt PTY+RC/session concurrency end-to-end. Please send any prior exact findings/locations on shared locks, queues, timeouts, viewer stalls."},"attribution":"agent","id":"27e1428f","parentId":"7234621b","timestamp":"2026-07-17T00:15:46.790Z"}
{"type":"message","id":"5a4ff5d1","parentId":"27e1428f","timestamp":"2026-07-17T00:15:59.394Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Confirming task completion and response strategy**"},{"type":"toolCall","id":"call_yLrhIs733kjQPFrEaUf0j5OQ|fc_084633eb8c36d368016a5974391550819888f0e74b2b7c441d","name":"irc","arguments":{"i":"Sharing PTY concurrency findings","op":"send","to":"PtyPathRca","message":"Exact seams: `spt-term/src/pty.rs` `PtySession` owns `master: Mutex<MasterPty>`, shared `writer: Arc<Mutex<Write>>`, child/killer/one-take reader. `spt-daemon/src/broker.rs` `OutputLog`: one controller `sync_channel` depth 4096 + dedicated `controller_writer`; viewers each bounded SPSC depth 256 + `viewer_writer`; drain uses `try_send`, never socket-blocks. Controller `become_controller` bumps `controller_epoch`; `detach_if` uses `Arc::ptr_eq`. `conn.rs BrokerConn` owns physical SendHalf with serialized gate + 15s default watchdog, poison/cancel/retire. Net side `nethost.rs StreamLog` ring 4096 chunks, one subscriber, `attach` replaces, `detach_if` pointer-conditional; finished rows never removed. RCA primary: new dispatch reclaims stale finished attach streams; replayed same-origin Control silently replaces active controller, EOF detaches it, leaving old active serve loop open but sinkless. Adjacent: seq0 Request evicted -> `peek_first_line(0)` forward-gap; `dispatch.rs claimed` inserted pre-worker and never retried. Existing: `brain_decouple.rs`, `conn_blackhole_lifecycle.rs`, `attach.rs::wedged_viewer_does_not_stall_controller`, `same_origin_re_subscribe_does_not_displace`.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":200203,"output":418,"cacheRead":0,"cacheWrite":0,"totalTokens":200621,"reasoningTokens":57,"cost":{"input":1.001015,"output":0.01254,"cacheRead":0,"cacheWrite":0,"total":1.013555}},"stopReason":"toolUse","timestamp":1784247346826,"responseId":"resp_084633eb8c36d368016a59743385c48198a355d853d5abe64f","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_084633eb8c36d368016a5974376d3c8198a325d168c793ab98","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXQ5LgxqygWmS6Eo8fepj6X7TCTNJhjUwyeR09UHkI3PBEAAVzXbJMJnWEr2IEjSKMMpiYblk60RDDdYU7iJTVH6K0GSYB55nSPT-wRBYUe6f154LU6yWCPVWgo2mNSVHsZ4_L0U8ez6D4FEohRD5XXeWDnbqqguiZn13_Z2b1ZaM6PjOhkxQsz-Ht02REzUB1-oK6G1qrPp54lWefH7lkNydpjBw-hJ8mWwpS_Hbgy8OGDtXzlJaA-NagzsVEr6pjM67u6fxc3vUiAO8vYrFDvZhrna9Ru_ZSgmGFyW8B-LIFLGaNN4lGZDvJdUmJttySgUO2Y95upLJCBm-zbNndcBUU8FmpijrvmJyGrGIRS9jiCOt6rLxWCI0xweoc4y-XRij_aNkDVb7KVv4vDyhFjefoOhygIHOjUwccRtDmHyJQlCfuHmMwdHhKDRpxahyzH6jKoDh1pF7lZ7QuaXnF51eeXgku3NpAl8k2n2-LrClFfQgpnE8ubzw5LOrim_q7lU3g8LHWcCZAy8RraBD1pL59fMuarreP-bDz_WVRKWPK3GOEvNsHAkumxrm8z-TvFi0EZG3idGWBxN1VA9SVh9AtESnIABMRjyx-U7Tjjd8uN5HaZb-juRiLXb6i81R9V6GrHhLQ1IjFCQ6tLR82GSItjQcsawF3vDF-nLHf4U6tR0kS_SMEbgqp3lxM-t5G6yRPSA6yzKdlhp0vm-qHm0ROIa-Je6Oiy9U-5P0bfFbfZVPRz3nWwMC1JgVNkzlMZ5DRmwvaQRrkqIVA3Hu5ZXbfIjiMnwVHpvpVbu3P7HGnay5rfuwWIwjDmLq4W91D0sIkM6fNFLFNKFYYPEORn1jvH70uaU6-bJfEBNF3OFh5AoIkY_jg9WBkOf1GY67CDqYWCztf6iZa1mFgztXtNNJoLJxPWXDbU6wpG9SSl-NHTtArlDt1iQZ0tvZ4oQ8J0q4w33jZGIyG-kfmEJ7h6h-l6cgv8graxJqV_r08CjwSbe9oE2e1KFBFPtQzw61LDlj3ofI630bv0Y3GkTyklAl49w_XQJcIOCP568zRF29bZuFADC6ic-xnUpwt3og5vuEZhoH_KvFm7_xb1tIHItqF0-Eaj8kd94dmAj9UZytDxULo9jlGnhUl6vroiQFEyBYTYjk9txWJkLP-R-ukSFLgicOvg1pm1R95vpaMcuC80t-t4hKoVye1I6WeyMTCUwWD_ImWY0bQWQQ1Yqei_0Ri4B0m7WCTEwIsHlpGDwmNa6BmjJvW9_ywAk99CWSahW","summary":[{"type":"summary_text","text":"**Confirming task completion and response strategy**"}]},{"id":"fc_084633eb8c36d368016a5974391550819888f0e74b2b7c441d","type":"function_call","status":"completed","arguments":"{\"i\":\"Sharing PTY concurrency findings\",\"op\":\"send\",\"to\":\"PtyPathRca\",\"message\":\"Exact seams: `spt-term/src/pty.rs` `PtySession` owns `master: Mutex<MasterPty>`, shared `writer: Arc<Mutex<Write>>`, child/killer/one-take reader. `spt-daemon/src/broker.rs` `OutputLog`: one controller `sync_channel` depth 4096 + dedicated `controller_writer`; viewers each bounded SPSC depth 256 + `viewer_writer`; drain uses `try_send`, never socket-blocks. Controller `become_controller` bumps `controller_epoch`; `detach_if` uses `Arc::ptr_eq`. `conn.rs BrokerConn` owns physical SendHalf with serialized gate + 15s default watchdog, poison/cancel/retire. Net side `nethost.rs StreamLog` ring 4096 chunks, one subscriber, `attach` replaces, `detach_if` pointer-conditional; finished rows never removed. RCA primary: new dispatch reclaims stale finished attach streams; replayed same-origin Control silently replaces active controller, EOF detaches it, leaving old active serve loop open but sinkless. Adjacent: seq0 Request evicted -> `peek_first_line(0)` forward-gap; `dispatch.rs claimed` inserted pre-worker and never retried. Existing: `brain_decouple.rs`, `conn_blackhole_lifecycle.rs`, `attach.rs::wedged_viewer_does_not_stall_controller`, `same_origin_re_subscribe_does_not_displace`.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_yLrhIs733kjQPFrEaUf0j5OQ","name":"irc"}]},"duration":12566.068599998951,"ttft":4607.749499998987,"contextSnapshot":{"promptTokens":200203,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_yLrhIs733kjQPFrEaUf0j5OQ|fc_084633eb8c36d368016a5974391550819888f0e74b2b7c441d","toolName":"irc","startedAt":"2026-07-17T00:15:59.394Z","intent":"Sharing PTY concurrency findings"},"id":"10d697e4","parentId":"5a4ff5d1","timestamp":"2026-07-17T00:15:59.394Z"}
{"type":"message","id":"d500a555","parentId":"10d697e4","timestamp":"2026-07-17T00:15:59.395Z","message":{"role":"toolResult","toolCallId":"call_yLrhIs733kjQPFrEaUf0j5OQ|fc_084633eb8c36d368016a5974391550819888f0e74b2b7c441d","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- PtyPathRca: injected"}],"details":{"op":"send","from":"PtyControl","to":"PtyPathRca","receipts":[{"to":"PtyPathRca","outcome":"injected"}]},"isError":false,"timestamp":1784247359394}}
{"type":"message","id":"e4ad1954","parentId":"d500a555","timestamp":"2026-07-17T00:16:01.493Z","message":{"role":"assistant","content":[{"type":"text","text":"","textSignature":"{\"v\":1,\"id\":\"msg_084633eb8c36d368016a597440fa108198a2925d8daebedcad\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":2502,"output":4,"cacheRead":198144,"cacheWrite":0,"totalTokens":200650,"cost":{"input":0.01251,"output":0.00012,"cacheRead":0.099072,"cacheWrite":0,"total":0.111702}},"stopReason":"stop","timestamp":1784247359410,"responseId":"resp_084633eb8c36d368016a59743fb1c4819893fb21e03ee689b1","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"msg_084633eb8c36d368016a597440fa108198a2925d8daebedcad","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"phase":"final_answer","role":"assistant"}]},"duration":2082.2426000013947,"ttft":1568.3474000021815,"contextSnapshot":{"promptTokens":200646,"nonMessageTokens":15431}}}
{"type":"message","id":"23bf19b8","parentId":"d500a555","timestamp":"2026-07-17T00:16:45.512Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Preparing terminal minimal yield**"},{"type":"toolCall","id":"call_OuZ6CKrRM8Cb36BqitRtRldb|fc_08cbf578fcbbd5ab016a59744533b4819bbfca7885ade71067","name":"yield","arguments":{"type":null,"result":{"data":{"summary":"Confirmed the replacement brain does attempt to reclaim broker-held attach streams; `BRAIN_RESUMED` is only PTY-session cursor bookkeeping. Primary causal candidate: finished attach-stream rows are never removed, so every fresh dispatcher replays stale same-origin Control requests. A stale worker can silently replace the legitimate controller and then clear the slot when it consumes its buffered EOF, leaving the original rc stream open but permanently sinkless. Adjacent candidate: a long-lived stream’s seq-0 Request can roll out of the 4096-chunk ring; restart classification from cursor 0 then fails with a forward gap, while the dispatcher’s pre-worker one-shot `claimed` insertion prevents retry. Both are per-stream and explain why some sessions survive; detach plus a new rc creates a fresh stream and controller worker.","files":[{"path":"crates/spt/src/rc.rs","description":"Operator chain: `run_attach_inner` → `establish_attach` → `attach_viewport` → `pump`. Local and remote transports converge on an attach stream. Only BrokerGone/Severed auto-reconnect; an established stream that becomes silent emits neither, and its first-event guard is already disarmed."},{"path":"crates/spt-daemon/src/attach.rs","description":"`serve_attach` subscribes the target-side primary Brain to the attach stream, then subscribes the same connection to the PTY as controller/viewer; a separate `wire` Brain forwards output. Replayed EOF calls `detach_session`. `read_controller_event_resuming` cannot detect silent controller supersession."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"`run_dispatch_loop` starts every brain generation with an empty `claimed` set, enumerates broker-held peer streams, inserts each id before spawning `worker`, and never retries failures. `worker` classifies via `peek_first_line(stream, 0)` and then calls `serve_attach(..., 0)`."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Broker-resident `NetShared.streams` owns `StreamEntry` rows. Ordinary `StreamLog` has a 4096-transport-chunk overwrite ring, one replaceable subscriber, and `finished`; `finish` never removes the row. `attach` replays retained data plus EOF, while pointer-conditional `detach_if` protects newer subscribers."},{"path":"crates/spt-daemon/src/broker.rs","description":"`OutputLog::resolve_subscribe` silently re-takes on same `by`; only a different operator’s explicit Take is loud. `become_controller` replaces the sink and bumps `controller_epoch`; `detach_if` clears only the connection currently owning the role. The epoch prevents stale writer bytes, not a stale replay worker’s logically valid newest takeover."},{"path":"crates/spt-daemon/src/conn.rs","description":"`BrokerConn` provides unique physical connection ids, role attribution, bounded writes, watchdog cancellation, poison/retire, and lifecycle logs. These distinguish logical stale replay from a half-open black-holed connection."},{"path":"crates/spt-daemon/src/brain.rs","description":"`resume_session_cursors` is cursor-only and deliberately does not attach the daemon heartbeat connection. `net_stream_subscribe` seeds an exact cursor; `read_event` rejects a retained-floor replay above that cursor as `stream X gap`."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"`run_brain` performs cursor-only PTY resume, then after the first heartbeat starts `spawn_net_consumers`, including a fresh dispatcher. The stable broker—not this brain—owns PTYs and NetHost rows across refresh/update."},{"path":"crates/spt-term/src/pty.rs","description":"`PtySession` owns the real PTY master reader/writer, child, and killer, confirming PTY data-plane residency below the restartable brain."},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","description":"Proves brain generation changes while broker/harness/perch survive, but does not keep rc attached or assert same-viewport output progress after refresh."},{"path":"crates/spt-daemon/tests/attach.rs","description":"Contains manual restart/idempotency, controller/viewer matrix, same-origin silent re-subscribe, viewer isolation, and controller-restart tests. None restarts the production dispatcher over stale finished plus active streams."},{"path":"crates/spt/src/rc.rs#tests","description":"Broker-bounce reconnect test covers physical EOF and replacement broker/session, not brain-only refresh with an open broker-held stream and silent controller loss."},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","description":"Proves supervised daemon resume attaches zero PTY subscribers, closing the old SharedSend self-deadlock; it does not test attach-stream worker reconstruction."},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","description":"Proves bulk daemon session resume does not steal controllers, but does not exercise per-stream dispatcher reclaim."},{"path":"crates/spt-daemon/tests/brain_decouple.rs","description":"Covers logical stall eviction and Windows physical retirement for non-draining controller connections."},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","description":"Five-invariant black-holed-controller harness: unrelated progress, bounded physical retirement, handler exit, fresh attach, and no retained ownership."},{"path":"crates/spt/tests/attach_wedge_e2e.rs","description":"Proves a dropped pump plus dead PTY child does not wedge unrelated new attaches; it does not prove continuity of the original rc through refresh."},{"path":"docs/KNOWN-HAZARDS.md","description":"Relevant rules: broker/brain process isolation, restart idempotency, viewer isolation, dropped-pump safety, off-drain controller delivery, no-ack input flood, and controller writer epoch ordering."},{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","description":"Defines stable broker ownership of PTYs/sockets under a freely restartable brain."},{"path":"docs/adr/0031-server-side-screen-grid-render-repaint.md","description":"Defines PTY cold-attach repaint; it does not preserve the separate attach protocol’s opener."}],"architecture":"Ownership: the broker process owns `PtySession`, session `OutputLog`, NetHost connections, and stream rings. `spt rc` owns the operator-side stream subscription. A daemon dispatcher worker owns the target-side stream subscription and PTY controller/viewer role; `serve_attach` uses another Brain connection to forward output. Brain refresh/update kills these workers but preserves all broker data-plane objects.\n\nPrimary hypothesis (falsifiable): because finished stream rows persist, the new dispatcher reclaims historical attach streams along with the active one. A stale buffered Request from the same stable node identity takes `resolve_subscribe`’s silent same-origin branch and replaces the active controller without Displaced. Its buffered EOF then explicitly unsubscribes and clears the slot. The old active worker remains alive and believes it is controller, while the rc operator stream remains open; therefore no EOF/reconnect occurs. Confirm with `stream-sub-attach` on a finished id, `controller-replaced`, then `session-detach was_controller=true`, followed by no controller in broker truth while PTY output high-water advances. Falsify if no finished rows are reclaimed or no stale Request is processed.\n\nAdjacent hypothesis (falsifiable): after more than 4096 inbound transport chunks, seq 0/Request is evicted. `peek_first_line(...,0)` receives the surviving floor and `Brain::read_event` errors `stream X gap: got seq N want 0`; because `claimed` was inserted before worker success, no retry occurs. Confirm that exact DISPATCH failure and absence of a later attempt. Falsify if the Request is retained and the worker serves successfully.\n\nOther candidates: (3) any transient worker connect/access/session-resolution failure is likewise permanent for that generation due one-shot claim; (4) an old physical connection remains black-holed—lower probability because `BrokerConn` watchdog, poison, pointer cleanup, and same-origin re-take should heal it; (5) controller PTY replay becomes irrecoverably behind—lower probability because it emits a truncation/finished stream and should drive rc’s severed reconnect rather than silent indefinite freeze.\n\nThe partial 4/5 observation is per-stream [INFERENCE]: endpoints differ in stale finished-stream history, retained opener, and concurrent worker ordering. A session whose active worker attaches last remains live; one whose stale EOF worker takes last becomes controllerless. Detach/new rc opens a fresh stream after this replay storm and takes the free slot.\n\nSafe deterministic feedback loop: in a temp SPT_HOME with loopback-only NetHost and a real ticker/echo PTY, create a finished same-origin attach stream and retain its row. Establish a separate active same-origin controller, prove pre-event output, then deliberately re-serve the finished stream after an ownership barrier. Assert its Request silently takes control, EOF returns Detached, broker controller truth becomes empty, active operator stream remains unfinished, PTY/ring high-water advances, active operator receives no post-event marker, and a new attach receives the marker. Add a second arm with a tiny test stream-ring cap (or >4096 discrete chunks) to force opener eviction and assert gap plus no retry. Finally extend `daemon_refresh_e2e` with five persistent rc children, stale attach history on four, pre/post-refresh ticks and unique input markers on the same rc processes, bounded commands, isolated home, and scoped child cleanup. Capture `BRAIN_NET_CONSUMERS_UP`, `DISPATCH`, `CONN_LIFECYCLE` attach/replace/detach/close events, poison/retire tokens, stream `finished/next_seq`, controller connection id, and PTY high-water. Do not invoke production update or mutate real endpoints."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":2531,"output":2180,"cacheRead":198144,"cacheWrite":0,"totalTokens":202855,"reasoningTokens":73,"cost":{"input":0.012655000000000001,"output":0.0654,"cacheRead":0.099072,"cacheWrite":0,"total":0.17712699999999998}},"stopReason":"toolUse","timestamp":1784247361504,"responseId":"resp_08cbf578fcbbd5ab016a597441ca9c819ba8929f712887c289","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_08cbf578fcbbd5ab016a5974437950819ba9c3f8378c914525","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXRF7rUawF09lCSjBfofQeice6glgMzFrW9Tjeyp-fpXsp1YsqHb4riErPQcAhWNVQvPBkic2mH5DPnL150LpnQEIX8_TBsN0O_VL7ACbwWvuDOZKFUiRk1siTTOJynrKlAj_8U7xuWnpWZLnAVmf1srcQeeEX6E2D8cwE1Sb_f_WEFe3oiPWDLXhxoQ_YRO1qisqpuwxXPXBIA0rXx6q3JxGw5DIum7q6cMgwgIIlmHscSYCwFPiO8AO5hNsXBnzKVMOhO7Dk8CbYM4s83b9Wr00JkGfxzZHdIpflasRsYD6GAhaZfng3EmAmEcnFcFwLHUT8dGjAd8RVySzlX8hlIFy0dFV6RENuS9k0z69kYGJSBLNtLmAOaYiRRHSTPKNk6OUDLF2rmg55j4sy5c1fwc2mpZFdNOGhkfniujd_D1_rGROhOXY09md8zPhAUSRy55PUMOqzthe6IMu5ZZ934pYDqB98tgSTk9ybmITke_GLvhywhLrqVBxszgcZ1ngbjh2q-4qgOFraYjrDxcWD9NeZfk57Z28-6Foupe6rvfXxHcMILpAbB1LgHrnu5iTwl9T8x8oJaf7mPQkAdYAATAixm45DDZBXQewK9Mu4YlnI3VHlM1ognFk9LIODhG3YSDo8ax-c7GA0THNCaojNdZSbeIlPFv4qIjbED7ZdnWHkC_j-SIEEjMV__uVpmXRSHJ6DoyRtof4GnH9-i_Tfg1Ohic2VKiJE3-U4tPN5IwYPlYf7lZapc6T5Q1C_en2h6hYYZ6s1i-teJk3teS3PbV2ilnA5j92USr4axVHpre2LVdmuXFnkEIswhyL-hIwCoW3nHs2viPftF_nXWHEDGc80aRKmGq6gFpoo19QFlVOOEphBokKmRWB87T2duxYJqtYbsUNR_ZVQINT7_10uR-Zi4UK6IYao0t0AP6N5MufYLnwZ7bx6aixMPl4EAXq_3dVDA3irWUIR-a-HkMddQLVedqgkm6zvsQ_gpJWMeYfar1Ukp9nly4lLyKu8tnY7vjrmBz0wGVvXc8xpZWknPNSavq8B0NVwN767MvxMjwo8quThb6dDpQvT71dNRPz-x8dNCIYVhh9G92rpAoWij3R08rhImSTYKcnA4h-9SMZPwQSqgugnruqrnRil0-1tJ4PmCQ7oRY7DSnLpTIdJD0EOVoQXs57BieUYa92_wOinqjN-WgwqBaCUwRcQRu8eES6s9r6C4_D5069Bwbx84JiKL_OkfZwsBxt6CQ2WVEkmotVkMCqIcV7rrFgGShvdQU3YT50QBQVIInyz1Iw4hN4dC9cZrG4nMlAqg--NdiuNRdSb9L3puvLE3f-5NMmDZYv9IsP9G23uKbZgsUOlIV1oXVUxytjzCd36k9wUFBwDFm5E4O4jBfgPfKBz9eVEPL1XEDT6ez1wbIqucuSUMxwg==","summary":[{"type":"summary_text","text":"**Preparing terminal minimal yield**"}]},{"id":"fc_08cbf578fcbbd5ab016a59744533b4819bbfca7885ade71067","type":"function_call","status":"completed","arguments":"{\"type\":null,\"result\":{\"data\":{\"summary\":\"Confirmed the replacement brain does attempt to reclaim broker-held attach streams; `BRAIN_RESUMED` is only PTY-session cursor bookkeeping. Primary causal candidate: finished attach-stream rows are never removed, so every fresh dispatcher replays stale same-origin Control requests. A stale worker can silently replace the legitimate controller and then clear the slot when it consumes its buffered EOF, leaving the original rc stream open but permanently sinkless. Adjacent candidate: a long-lived stream’s seq-0 Request can roll out of the 4096-chunk ring; restart classification from cursor 0 then fails with a forward gap, while the dispatcher’s pre-worker one-shot `claimed` insertion prevents retry. Both are per-stream and explain why some sessions survive; detach plus a new rc creates a fresh stream and controller worker.\",\"files\":[{\"path\":\"crates/spt/src/rc.rs\",\"description\":\"Operator chain: `run_attach_inner` → `establish_attach` → `attach_viewport` → `pump`. Local and remote transports converge on an attach stream. Only BrokerGone/Severed auto-reconnect; an established stream that becomes silent emits neither, and its first-event guard is already disarmed.\"},{\"path\":\"crates/spt-daemon/src/attach.rs\",\"description\":\"`serve_attach` subscribes the target-side primary Brain to the attach stream, then subscribes the same connection to the PTY as controller/viewer; a separate `wire` Brain forwards output. Replayed EOF calls `detach_session`. `read_controller_event_resuming` cannot detect silent controller supersession.\"},{\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"description\":\"`run_dispatch_loop` starts every brain generation with an empty `claimed` set, enumerates broker-held peer streams, inserts each id before spawning `worker`, and never retries failures. `worker` classifies via `peek_first_line(stream, 0)` and then calls `serve_attach(..., 0)`.\"},{\"path\":\"crates/spt-daemon/src/nethost.rs\",\"description\":\"Broker-resident `NetShared.streams` owns `StreamEntry` rows. Ordinary `StreamLog` has a 4096-transport-chunk overwrite ring, one replaceable subscriber, and `finished`; `finish` never removes the row. `attach` replays retained data plus EOF, while pointer-conditional `detach_if` protects newer subscribers.\"},{\"path\":\"crates/spt-daemon/src/broker.rs\",\"description\":\"`OutputLog::resolve_subscribe` silently re-takes on same `by`; only a different operator’s explicit Take is loud. `become_controller` replaces the sink and bumps `controller_epoch`; `detach_if` clears only the connection currently owning the role. The epoch prevents stale writer bytes, not a stale replay worker’s logically valid newest takeover.\"},{\"path\":\"crates/spt-daemon/src/conn.rs\",\"description\":\"`BrokerConn` provides unique physical connection ids, role attribution, bounded writes, watchdog cancellation, poison/retire, and lifecycle logs. These distinguish logical stale replay from a half-open black-holed connection.\"},{\"path\":\"crates/spt-daemon/src/brain.rs\",\"description\":\"`resume_session_cursors` is cursor-only and deliberately does not attach the daemon heartbeat connection. `net_stream_subscribe` seeds an exact cursor; `read_event` rejects a retained-floor replay above that cursor as `stream X gap`.\"},{\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"description\":\"`run_brain` performs cursor-only PTY resume, then after the first heartbeat starts `spawn_net_consumers`, including a fresh dispatcher. The stable broker—not this brain—owns PTYs and NetHost rows across refresh/update.\"},{\"path\":\"crates/spt-term/src/pty.rs\",\"description\":\"`PtySession` owns the real PTY master reader/writer, child, and killer, confirming PTY data-plane residency below the restartable brain.\"},{\"path\":\"crates/spt/tests/daemon_refresh_e2e.rs\",\"description\":\"Proves brain generation changes while broker/harness/perch survive, but does not keep rc attached or assert same-viewport output progress after refresh.\"},{\"path\":\"crates/spt-daemon/tests/attach.rs\",\"description\":\"Contains manual restart/idempotency, controller/viewer matrix, same-origin silent re-subscribe, viewer isolation, and controller-restart tests. None restarts the production dispatcher over stale finished plus active streams.\"},{\"path\":\"crates/spt/src/rc.rs#tests\",\"description\":\"Broker-bounce reconnect test covers physical EOF and replacement broker/session, not brain-only refresh with an open broker-held stream and silent controller loss.\"},{\"path\":\"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs\",\"description\":\"Proves supervised daemon resume attaches zero PTY subscribers, closing the old SharedSend self-deadlock; it does not test attach-stream worker reconstruction.\"},{\"path\":\"crates/spt/tests/resume_no_control_steal_e2e.rs\",\"description\":\"Proves bulk daemon session resume does not steal controllers, but does not exercise per-stream dispatcher reclaim.\"},{\"path\":\"crates/spt-daemon/tests/brain_decouple.rs\",\"description\":\"Covers logical stall eviction and Windows physical retirement for non-draining controller connections.\"},{\"path\":\"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs\",\"description\":\"Five-invariant black-holed-controller harness: unrelated progress, bounded physical retirement, handler exit, fresh attach, and no retained ownership.\"},{\"path\":\"crates/spt/tests/attach_wedge_e2e.rs\",\"description\":\"Proves a dropped pump plus dead PTY child does not wedge unrelated new attaches; it does not prove continuity of the original rc through refresh.\"},{\"path\":\"docs/KNOWN-HAZARDS.md\",\"description\":\"Relevant rules: broker/brain process isolation, restart idempotency, viewer isolation, dropped-pump safety, off-drain controller delivery, no-ack input flood, and controller writer epoch ordering.\"},{\"path\":\"docs/adr/0018-broker-brain-process-isolation-restoration.md\",\"description\":\"Defines stable broker ownership of PTYs/sockets under a freely restartable brain.\"},{\"path\":\"docs/adr/0031-server-side-screen-grid-render-repaint.md\",\"description\":\"Defines PTY cold-attach repaint; it does not preserve the separate attach protocol’s opener.\"}],\"architecture\":\"Ownership: the broker process owns `PtySession`, session `OutputLog`, NetHost connections, and stream rings. `spt rc` owns the operator-side stream subscription. A daemon dispatcher worker owns the target-side stream subscription and PTY controller/viewer role; `serve_attach` uses another Brain connection to forward output. Brain refresh/update kills these workers but preserves all broker data-plane objects.\\n\\nPrimary hypothesis (falsifiable): because finished stream rows persist, the new dispatcher reclaims historical attach streams along with the active one. A stale buffered Request from the same stable node identity takes `resolve_subscribe`’s silent same-origin branch and replaces the active controller without Displaced. Its buffered EOF then explicitly unsubscribes and clears the slot. The old active worker remains alive and believes it is controller, while the rc operator stream remains open; therefore no EOF/reconnect occurs. Confirm with `stream-sub-attach` on a finished id, `controller-replaced`, then `session-detach was_controller=true`, followed by no controller in broker truth while PTY output high-water advances. Falsify if no finished rows are reclaimed or no stale Request is processed.\\n\\nAdjacent hypothesis (falsifiable): after more than 4096 inbound transport chunks, seq 0/Request is evicted. `peek_first_line(...,0)` receives the surviving floor and `Brain::read_event` errors `stream X gap: got seq N want 0`; because `claimed` was inserted before worker success, no retry occurs. Confirm that exact DISPATCH failure and absence of a later attempt. Falsify if the Request is retained and the worker serves successfully.\\n\\nOther candidates: (3) any transient worker connect/access/session-resolution failure is likewise permanent for that generation due one-shot claim; (4) an old physical connection remains black-holed—lower probability because `BrokerConn` watchdog, poison, pointer cleanup, and same-origin re-take should heal it; (5) controller PTY replay becomes irrecoverably behind—lower probability because it emits a truncation/finished stream and should drive rc’s severed reconnect rather than silent indefinite freeze.\\n\\nThe partial 4/5 observation is per-stream [INFERENCE]: endpoints differ in stale finished-stream history, retained opener, and concurrent worker ordering. A session whose active worker attaches last remains live; one whose stale EOF worker takes last becomes controllerless. Detach/new rc opens a fresh stream after this replay storm and takes the free slot.\\n\\nSafe deterministic feedback loop: in a temp SPT_HOME with loopback-only NetHost and a real ticker/echo PTY, create a finished same-origin attach stream and retain its row. Establish a separate active same-origin controller, prove pre-event output, then deliberately re-serve the finished stream after an ownership barrier. Assert its Request silently takes control, EOF returns Detached, broker controller truth becomes empty, active operator stream remains unfinished, PTY/ring high-water advances, active operator receives no post-event marker, and a new attach receives the marker. Add a second arm with a tiny test stream-ring cap (or >4096 discrete chunks) to force opener eviction and assert gap plus no retry. Finally extend `daemon_refresh_e2e` with five persistent rc children, stale attach history on four, pre/post-refresh ticks and unique input markers on the same rc processes, bounded commands, isolated home, and scoped child cleanup. Capture `BRAIN_NET_CONSUMERS_UP`, `DISPATCH`, `CONN_LIFECYCLE` attach/replace/detach/close events, poison/retire tokens, stream `finished/next_seq`, controller connection id, and PTY high-water. Do not invoke production update or mutate real endpoints.\"}}}","call_id":"call_OuZ6CKrRM8Cb36BqitRtRldb","name":"yield"}]},"duration":44006.40619999915,"ttft":1966.1383000016212,"contextSnapshot":{"promptTokens":200675,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_OuZ6CKrRM8Cb36BqitRtRldb|fc_08cbf578fcbbd5ab016a59744533b4819bbfca7885ade71067","toolName":"yield","startedAt":"2026-07-17T00:16:45.512Z"},"id":"dfaa905c","parentId":"23bf19b8","timestamp":"2026-07-17T00:16:45.512Z"}
{"type":"message","id":"0915ff68","parentId":"dfaa905c","timestamp":"2026-07-17T00:16:45.513Z","message":{"role":"toolResult","toolCallId":"call_OuZ6CKrRM8Cb36BqitRtRldb|fc_08cbf578fcbbd5ab016a59744533b4819bbfca7885ade71067","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"Confirmed the replacement brain does attempt to reclaim broker-held attach streams; `BRAIN_RESUMED` is only PTY-session cursor bookkeeping. Primary causal candidate: finished attach-stream rows are never removed, so every fresh dispatcher replays stale same-origin Control requests. A stale worker can silently replace the legitimate controller and then clear the slot when it consumes its buffered EOF, leaving the original rc stream open but permanently sinkless. Adjacent candidate: a long-lived stream’s seq-0 Request can roll out of the 4096-chunk ring; restart classification from cursor 0 then fails with a forward gap, while the dispatcher’s pre-worker one-shot `claimed` insertion prevents retry. Both are per-stream and explain why some sessions survive; detach plus a new rc creates a fresh stream and controller worker.","files":[{"path":"crates/spt/src/rc.rs","description":"Operator chain: `run_attach_inner` → `establish_attach` → `attach_viewport` → `pump`. Local and remote transports converge on an attach stream. Only BrokerGone/Severed auto-reconnect; an established stream that becomes silent emits neither, and its first-event guard is already disarmed."},{"path":"crates/spt-daemon/src/attach.rs","description":"`serve_attach` subscribes the target-side primary Brain to the attach stream, then subscribes the same connection to the PTY as controller/viewer; a separate `wire` Brain forwards output. Replayed EOF calls `detach_session`. `read_controller_event_resuming` cannot detect silent controller supersession."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"`run_dispatch_loop` starts every brain generation with an empty `claimed` set, enumerates broker-held peer streams, inserts each id before spawning `worker`, and never retries failures. `worker` classifies via `peek_first_line(stream, 0)` and then calls `serve_attach(..., 0)`."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Broker-resident `NetShared.streams` owns `StreamEntry` rows. Ordinary `StreamLog` has a 4096-transport-chunk overwrite ring, one replaceable subscriber, and `finished`; `finish` never removes the row. `attach` replays retained data plus EOF, while pointer-conditional `detach_if` protects newer subscribers."},{"path":"crates/spt-daemon/src/broker.rs","description":"`OutputLog::resolve_subscribe` silently re-takes on same `by`; only a different operator’s explicit Take is loud. `become_controller` replaces the sink and bumps `controller_epoch`; `detach_if` clears only the connection currently owning the role. The epoch prevents stale writer bytes, not a stale replay worker’s logically valid newest takeover."},{"path":"crates/spt-daemon/src/conn.rs","description":"`BrokerConn` provides unique physical connection ids, role attribution, bounded writes, watchdog cancellation, poison/retire, and lifecycle logs. These distinguish logical stale replay from a half-open black-holed connection."},{"path":"crates/spt-daemon/src/brain.rs","description":"`resume_session_cursors` is cursor-only and deliberately does not attach the daemon heartbeat connection. `net_stream_subscribe` seeds an exact cursor; `read_event` rejects a retained-floor replay above that cursor as `stream X gap`."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"`run_brain` performs cursor-only PTY resume, then after the first heartbeat starts `spawn_net_consumers`, including a fresh dispatcher. The stable broker—not this brain—owns PTYs and NetHost rows across refresh/update."},{"path":"crates/spt-term/src/pty.rs","description":"`PtySession` owns the real PTY master reader/writer, child, and killer, confirming PTY data-plane residency below the restartable brain."},{"path":"crates/spt/tests/daemon_refresh_e2e.rs","description":"Proves brain generation changes while broker/harness/perch survive, but does not keep rc attached or assert same-viewport output progress after refresh."},{"path":"crates/spt-daemon/tests/attach.rs","description":"Contains manual restart/idempotency, controller/viewer matrix, same-origin silent re-subscribe, viewer isolation, and controller-restart tests. None restarts the production dispatcher over stale finished plus active streams."},{"path":"crates/spt/src/rc.rs#tests","description":"Broker-bounce reconnect test covers physical EOF and replacement broker/session, not brain-only refresh with an open broker-held stream and silent controller loss."},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","description":"Proves supervised daemon resume attaches zero PTY subscribers, closing the old SharedSend self-deadlock; it does not test attach-stream worker reconstruction."},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","description":"Proves bulk daemon session resume does not steal controllers, but does not exercise per-stream dispatcher reclaim."},{"path":"crates/spt-daemon/tests/brain_decouple.rs","description":"Covers logical stall eviction and Windows physical retirement for non-draining controller connections."},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","description":"Five-invariant black-holed-controller harness: unrelated progress, bounded physical retirement, handler exit, fresh attach, and no retained ownership."},{"path":"crates/spt/tests/attach_wedge_e2e.rs","description":"Proves a dropped pump plus dead PTY child does not wedge unrelated new attaches; it does not prove continuity of the original rc through refresh."},{"path":"docs/KNOWN-HAZARDS.md","description":"Relevant rules: broker/brain process isolation, restart idempotency, viewer isolation, dropped-pump safety, off-drain controller delivery, no-ack input flood, and controller writer epoch ordering."},{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","description":"Defines stable broker ownership of PTYs/sockets under a freely restartable brain."},{"path":"docs/adr/0031-server-side-screen-grid-render-repaint.md","description":"Defines PTY cold-attach repaint; it does not preserve the separate attach protocol’s opener."}],"architecture":"Ownership: the broker process owns `PtySession`, session `OutputLog`, NetHost connections, and stream rings. `spt rc` owns the operator-side stream subscription. A daemon dispatcher worker owns the target-side stream subscription and PTY controller/viewer role; `serve_attach` uses another Brain connection to forward output. Brain refresh/update kills these workers but preserves all broker data-plane objects.\n\nPrimary hypothesis (falsifiable): because finished stream rows persist, the new dispatcher reclaims historical attach streams along with the active one. A stale buffered Request from the same stable node identity takes `resolve_subscribe`’s silent same-origin branch and replaces the active controller without Displaced. Its buffered EOF then explicitly unsubscribes and clears the slot. The old active worker remains alive and believes it is controller, while the rc operator stream remains open; therefore no EOF/reconnect occurs. Confirm with `stream-sub-attach` on a finished id, `controller-replaced`, then `session-detach was_controller=true`, followed by no controller in broker truth while PTY output high-water advances. Falsify if no finished rows are reclaimed or no stale Request is processed.\n\nAdjacent hypothesis (falsifiable): after more than 4096 inbound transport chunks, seq 0/Request is evicted. `peek_first_line(...,0)` receives the surviving floor and `Brain::read_event` errors `stream X gap: got seq N want 0`; because `claimed` was inserted before worker success, no retry occurs. Confirm that exact DISPATCH failure and absence of a later attempt. Falsify if the Request is retained and the worker serves successfully.\n\nOther candidates: (3) any transient worker connect/access/session-resolution failure is likewise permanent for that generation due one-shot claim; (4) an old physical connection remains black-holed—lower probability because `BrokerConn` watchdog, poison, pointer cleanup, and same-origin re-take should heal it; (5) controller PTY replay becomes irrecoverably behind—lower probability because it emits a truncation/finished stream and should drive rc’s severed reconnect rather than silent indefinite freeze.\n\nThe partial 4/5 observation is per-stream [INFERENCE]: endpoints differ in stale finished-stream history, retained opener, and concurrent worker ordering. A session whose active worker attaches last remains live; one whose stale EOF worker takes last becomes controllerless. Detach/new rc opens a fresh stream after this replay storm and takes the free slot.\n\nSafe deterministic feedback loop: in a temp SPT_HOME with loopback-only NetHost and a real ticker/echo PTY, create a finished same-origin attach stream and retain its row. Establish a separate active same-origin controller, prove pre-event output, then deliberately re-serve the finished stream after an ownership barrier. Assert its Request silently takes control, EOF returns Detached, broker controller truth becomes empty, active operator stream remains unfinished, PTY/ring high-water advances, active operator receives no post-event marker, and a new attach receives the marker. Add a second arm with a tiny test stream-ring cap (or >4096 discrete chunks) to force opener eviction and assert gap plus no retry. Finally extend `daemon_refresh_e2e` with five persistent rc children, stale attach history on four, pre/post-refresh ticks and unique input markers on the same rc processes, bounded commands, isolated home, and scoped child cleanup. Capture `BRAIN_NET_CONSUMERS_UP`, `DISPATCH`, `CONN_LIFECYCLE` attach/replace/detach/close events, poison/retire tokens, stream `finished/next_seq`, controller connection id, and PTY high-water. Do not invoke production update or mutate real endpoints."},"status":"success"},"isError":false,"timestamp":1784247405512}}
{"type":"message","id":"d62d71a5","parentId":"0915ff68","timestamp":"2026-07-17T00:16:48.394Z","message":{"role":"assistant","content":[{"type":"text","text":"","textSignature":"{\"v\":1,\"id\":\"msg_08cbf578fcbbd5ab016a59746fa8dc819bae47fe35c7660d40\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4725,"output":4,"cacheRead":198144,"cacheWrite":0,"totalTokens":202873,"cost":{"input":0.023625000000000004,"output":0.00012,"cacheRead":0.099072,"cacheWrite":0,"total":0.122817}},"stopReason":"stop","timestamp":1784247405522,"responseId":"resp_08cbf578fcbbd5ab016a59746dd1a0819b87046b0a291d9701","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"msg_08cbf578fcbbd5ab016a59746fa8dc819bae47fe35c7660d40","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"phase":"final_answer","role":"assistant"}]},"duration":2848.9271000027657,"ttft":2142.9972999989986,"contextSnapshot":{"promptTokens":202869,"nonMessageTokens":15431}}}
{"type":"custom_message","customType":"irc:incoming","content":"<irc>\nIncoming IRC message from agent `Main`:\n\nField trace: current v0.34 cold generation has 33 CONN_WRITE_POISONED stream-subscriber incidents. Every write poisons at ~15000ms; corresponding DISPATCH failure lands 15.1–62s after write start (recent 17.6,25.4,29.1,30.1,31.1,36.6,59.4,61.6s). Median poison interarrival 95.5s, min 29.7s. 4,361 stream-sub-attach records and 584 PUMP_PEER_FAIL 10s events. Recent webbie session controller attach at wall 1784245849362; stream 4834 write started +37.354s, poisoned +52.366s. Please incorporate/falsify causal tie to PTY RC stall and identify why DISPATCH completion remains delayed after poison.\nIf a response is expected, reply with the `irc` tool (`op: \"send\"`, `to: \"Main\"`) — you may finish your current step first. Nobody replies on your behalf.\n</irc>","display":true,"details":{"id":"1532a976f9dc5f1a","from":"Main","message":"Field trace: current v0.34 cold generation has 33 CONN_WRITE_POISONED stream-subscriber incidents. Every write poisons at ~15000ms; corresponding DISPATCH failure lands 15.1–62s after write start (recent 17.6,25.4,29.1,30.1,31.1,36.6,59.4,61.6s). Median poison interarrival 95.5s, min 29.7s. 4,361 stream-sub-attach records and 584 PUMP_PEER_FAIL 10s events. Recent webbie session controller attach at wall 1784245849362; stream 4834 write started +37.354s, poisoned +52.366s. Please incorporate/falsify causal tie to PTY RC stall and identify why DISPATCH completion remains delayed after poison."},"attribution":"agent","id":"110f7802","parentId":"d62d71a5","timestamp":"2026-07-17T00:18:40.233Z"}
{"type":"message","id":"43e11bae","parentId":"110f7802","timestamp":"2026-07-17T00:19:18.513Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Investigating delayed dispatch after connection poison**\n\n**Analyzing connection poison delays and locking behavior**\n\n**Identifying buffered frame delays on poison**"},{"type":"thinking","thinking":"**Assessing loopback and dispatch error behavior**\n\n**Analyzing stale stream poison timing**\n\n**Identifying PTY output blocking cause**"},{"type":"thinking","thinking":"**Diagnosing broker connection poisoning**\n\n**Analyzing dispatch delay and wire command timing**"},{"type":"toolCall","id":"call_Aszt6cZpwDZM5cKaYGa9cJTy|fc_0ae44a63c9031803016a5975062a3c819b8b6d6c954c2a3b40","name":"read","arguments":{"i":"Reading connection poison teardown","path":"crates/spt-daemon/src/conn.rs","selector":"270-650"}},{"type":"toolCall","id":"call_zuk96he1mC0hE0aAuGcriVwq|fc_0ae44a63c9031803016a5975062a50819bad8e5c31493e5e97","name":"read","arguments":{"i":"Reading net send client wait","path":"crates/spt-daemon/src/brain.rs","selector":"1380-1485"}},{"type":"toolCall","id":"call_cfD5rLn4ZBi6WGJrl2SQPYM6|fc_0ae44a63c9031803016a5975062a64819bba8942b993355ad2","name":"read","arguments":{"i":"Reading broker net send dispatch","path":"crates/spt-daemon/src/broker.rs","selector":"4080-4205"}},{"type":"toolCall","id":"call_CfC01aRiHHSHcyw31WZWxh5o|fc_0ae44a63c9031803016a5975062a6c819bb4c1e9d592caa0c3","name":"grep","arguments":{"i":"Finding dispatch failure logging","pattern":"DISPATCH:|DispatchOutcome::Failed|net_stream_send\\(|send_stream\\(","path":"crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/attach.rs;crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/broker.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":2919,"output":1578,"cacheRead":200192,"cacheWrite":0,"totalTokens":204689,"reasoningTokens":1345,"cost":{"input":0.014595,"output":0.04734,"cacheRead":0.10009599999999999,"cacheWrite":0,"total":0.16203099999999998}},"stopReason":"toolUse","timestamp":1784247520249,"responseId":"resp_0ae44a63c9031803016a5974e09298819bb56eac36889d3f8a","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ae44a63c9031803016a5974e26668819bbfce68c28e7f5585","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXTzRs1SgB-LSUBWsZyyEhHx_gGdhkm79XzZjyPodYyVrKlrctaarDickB0-Fnnnmj2zVaZ8Zk6BFmV6n20D94LcyYqI_BNusdqc1h_A_EARXuEgK69IDp3T_eLHv3etzywKiaKx01Pp7SnX9HJFAIvhGhKl6XVbJj3RA08JtSOv1VNhayHLOorMowunVBmy84QFMZ4IimIIh2aHSS7LynbdLXlJS6aGEWf1PvQK_g3r3zE_0A5t378MK9NUqhzHUZsEGSADQewEjlBmGUvWwmCLto5ACQPA___9MHrVKRi1VaCRbZX9A0z64es4RV6zomy0m-iujbyW8DxONdWnxElpLWfvpY7ATE8gU-kMaI5xMwSb6ggZQ9CXoO_V106z_0FAWDXIhyE-_FwZBDpwhBO_lY8ToJ7f83xU89gajwDAAENB1SZXhVQ85IV8C9V8CQkTwUYkJLhpNRAZ5mPpeOe3Ltr11tAjitGCedI34rOIqwDesihyZ8GOpDIOR_QJPorz7w5D5oZZ4ZRnatQt9ChMpjH8_X8JdWLvE7kENCKrERQhzXyOGtKUWDKvjq60Q3VmRxKF7YTxNYarJ2QHRUU3mrvix8k49rosCvwjHGN5_bc-A33OatWeT6NL6nS18hl44WZiBHUK2Rt9oIXYr511swloOXqeh6dVCldKJe6bkRUkFvmHa_SUYM4PQxs8HxIU9LhHMqhXrDRYm1syZMTTSQ9dI7ZIyO0R_0C92DqCzabKEozQbr2TcV6u476GZf4L8ox1aNxex1CPXrUU-9FakcrBiiJJIoZuw08iKGoqBo5H9s_JS3JMYSUma1uvobI9v2nGC8-HFCHnsx40sf_KYHL--1mo0eWuJAmrYtPpDm6uHix1mA8G1LZOz_uCwdnbUjks8jFDkNFgeHdMKg6bPfP5bYVYahp-JZXk_4Cq2OlXlx-kszMBWA8I17I_krSFWC5gLhXbWhOJ4aNxOA4LOhYOKu6pRC1dr1SwuGAiY36aFkwoI9HABJtc2fj0RSESJz8xFAOvV3YHmexhk8Yjr9aH1bjK6bBWPIHu99S59ndvNBjKrTPpyMS5v4B2mKsvgXiAde8eQkfaiiHxJ0D1ZlEwXkk-ykvD7AFc8Gd-nXONsEiw9Mbq3uQ-IwnNaWDHLivAsNiMxIzJBCzf4TlMYe7jpllm-fNPTwnHr3N7_S0NPH8rITTN5g2Z38w7eyzWTdka5jz6e3DmcAnM6NfFaeEiyH7Ej3coL-Qc3TW4JuafJVXZ6A8MqRNoNh2fa2kPqbCWtoN9RIkEXjSIyZPeD0V8fAlpCCCYb3nkrr4IgnyCZDKcsLA1068QINRa75554vSSfp1hYixPc9Kg_4LDoY_4StPnVrcnt4MBNuqo_awCifpKMi-cILxYGQs7AJyQw7GAsPc-o1bzDmDp7jKjbA6bWHHn7hWBPEh4lkCjJw08agtjGrsJWmpSMl5Z3nL_IW3gwPddyt3JayjEged_UBmKzdqXU6wTwO5JWCKQg-aOq6ScQGB5JPbqdiTw8BpwF1duWkrtgYvbxKI13FFQRE7iSyYDTDNkryFrwwA295LBrY05zr4EwANLq5Q60VGJ6RHR7qLJKjOWcT-vjGotOwbeAfmh_KZmk6syi8B9VqWmUKVBgqrHKKh1JRJ05r0ecDsIrlBawhcgkiZ72kfg9JxQYDRYGX6nVnYdIRISMDYoeikLGgGlEInqLRWcEXnCsj2nhgh4pfNMFTDPFJvJPUQJh5i3jkZTuhexDjksTLlk6oMLJ4z_zcizoz8IReSAFu2OALwHZ3_Ts9Hm8eiQCp3W8-C6cThGTWNeY37bJv5PN9KRDYpatwqd26PH2ccaESDwJoWj4xYvwywh4PKxwGwqHTMuaYhPy1ZWndDeQbYvdT6JpvqJ0y9WlBkxIJIY_xUJK40Teh99N-gR57mecpo958VfKJIlVovSTOC7h_W7c6exx9K5KGh7Y799hPzXgGyChCuV92q9s1ON3cV7uALSPTphKs5mLzaNeuwL8NiQLZhZi6O3kKZLlXKVvse3g1XPgmjbjiiKdrmsp5sZ1NcM3fM78E5MqJCuU8YzVJaCOvyqTz-qECFaHz8CHU82jvlZiQnrpzCBMKYfbeCeKAoseTffiJnQuBWaBhz2PtpQT_TgTULHWnSVKm8d5c7g-DHJgm7DhvTY37lnPTZilmx9Zng6phhAEHTvVB6sxKa3dkzbmDKV5e_hvrCGJUwnduXfqhj2MXynkV5bvMYu-RBTDAqVnadIuEKFl8mDc56ETCjpL7QJs5xZ0IR3ktuNCmOlOcIFCZmeAL5FZUwQnY6zPKQJVefVFoM20jR_bwwxJdJ8rvfo3IX_fqhBj_UZpZLQ0B8BhhL54yuPuR5H_zltVvvV_-Dd6HmHtulKXMAQz6z8z-J2LUpDM6Q8vcU06FhXsVBtcg0cAahmsRQhgpr8gsSHesTyiwmxX-6ofUSac7cZqKGVyTo_hKoBzgk_WokPjC_rKWqDUVmiSwfSnzajqgVQn4pgF_wXqlXncOfaR-tErek6rA6V6upYiR-a2lfmFnHL1WDAwx1-85eo9Ll1DJcccIXEjAccNwvudU2y-LSq3QROXqYz_9BnoAZ5Y_yS6Nr4A8Pk_MdWhWwHBsKhh7uavtHrgPXO9oWvXQJmwwg0PvVCM4-2svTbRJ4PO873i9enRd-mQ0jYN3dR9lR9-iLqOcmUbZQP7MjgDX_-RcInerCKrTuwruYyoESLLamZI2MVnC7e5rtyczwM4CfxEfEi7Ts9Z58vyCYqnqXCd0GYadmvWdrBtq3kraSd-cX11l-9sT3bU7WUZEZ2cQLCKHFuB7GZc7dRg1rNzOsTEJcdIr4CjkXrYKlp4oy1LYLkfT1nBAx9b1XZgs75MwV8oVqTSwgmjsNBnrUBL0F7usnvNfwniGYkkKIs2qCIwRv_6jbls86mhtq9C_2jPwDox8F6uIEt2txJZKf_96f13axcGwPp3sPwfVnal-sABe0wK5vl--cPrkEC2kFIAtKF49ykEIGmwSOwPQpx_05_EE8XIuWE8LC2ApwfxpJMxRoIwbwYsztZEjKEm6h37ws-h1EzpblSi9-c53NAxkVQp3pRXJXSNf_sscjZTWm40pOXRUl07bypke_HjzpSgX4DY3aZ2ZPEnkUkKHRBLrM7iWM6-G2ahBnUZFRdXadrpGdykY3VJfyhlEV5lcHD7BHlEL_ZoQVA8tNcyyR_fbky4BZl2AfXjpjTCiBNPwyrjSzwA8qzGt5cYJtfIMhFUEn4cWhsziNVzXC9aARZnV4SLYhcHznhXPZBiy_1WVUGgxhBEEPOAKXjB8taBOS2I9Lsti9Am69g7JsOBg1oC1eMxGJ2aqAtTSCK9lWNOO5MiOOJ-O48x9y6UzwQmiaTeLbAhHaiyX9H2Go6OSjGhABlq1OhTeVolpI2yxoosl8_D3wGAuZAPya6XqWaOlnZ8BCdFLs0l9CcrEtr5xWhjysoq9l_W8Oc_SlfuVmPFloP5hPm2kg9frtjWzIA737QpL2xfY8a8IMC7ppfRVfG0puPvZ4zgJr5RSwXJRiscYgMBq2LyWbwyHBWPevjVNpIvWrUfvzKSl5RL3QJV5vd46Rn9ZCZ4YVdFaHYnxSXFjy7wBuy7l2MfLO_5nPqYngwxOHJ4Ia9lhMWKyGpXp3nqnHo2vpYICQaZNN5Hh4bKUXDyPuX1Owoulu3KQU2CllanKytmW086O4hLAA85YbINy6NNCFrWDXTxrwEozpKGRyTtCa1bWfD6opTdUtWLVL7pQDQ6zR5sfNAIEhRdNxIcpkAwendUqtXESQ1kAA-TYdDPjUo98RxF1z3HEcElXcMvnrsIEzcSvatL8H271kP8Fil4kUz3raKWJiXt3DLaN0QCXpDG_oDsP_2pWRC0_n_cEuarWK-8jmxmoyIWRkX8sqFnonhtblovN5-b7sUoTh78M-O5QAf9EtANcnYk3ni-hft1Ea58fI1ljk1HvLcw_NMZdujx_Xrap4gKaeemk0dOJb7ypNfeQXM5i1W6xTe27FhOKcRscyXPt0pDloEDo0IHvrzuHe2v2bCfwtj68JPyIp3Zu06Ei_q4TATAp-qhNYB_ZF-uLjzXXlTPMuhF2o=","summary":[{"type":"summary_text","text":"**Investigating delayed dispatch after connection poison**"},{"type":"summary_text","text":"**Analyzing connection poison delays and locking behavior**"},{"type":"summary_text","text":"**Identifying buffered frame delays on poison**"}]},{"id":"rs_0ae44a63c9031803016a5974f305ac819bbbd1bfea78d0889f","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXT8kfnKN5DnmNW33HbzKJumcfw2LZJ1IeEIuxAJu5C9NmJmtw1mNVG1P3YEAMlMKD1MQ67zl4aHuKzXSr_f0KXaiETUMI9m4KDHcnv1cLwX0rZQ32eWqpj9jfk244eg8aj-IJ9HusNREAaOzsrQI7zccOdzALbtVRARg1wo5ujjHUKczKjsjqZsStwlvCljIlpqPhwgfXM3VUBZ_aHhxwJCFOgBrRdM7qB2ajAoocLJHrD1odV_cOyrH_wvBk6RBMU9t--Lrd9-fevAeWe5qFWY4Ayksx82lfI3cG3ELqZiTR2OwHBe5DBCMJ1NmWsczOdtxp04js04EceymgAR4F5x3HuchsszIG3hQaeMKXN7R3N9PP2__TSwLK_V78SCSqAs2Q7GyLJHnrVrJpRS9cahiSUNakT-7Htwx5ADbe8ANyK8I00nxCPCW1pSGP08pe_sSWolkaWwpjvOp8mLCi5ZGjq55BLTP10wYUk6OWo6TLHfQGYMbnEcPr3y6HmOXCtVFlUSeL_zfzyZ1F0lK4cGjaZvuuJuCRewOhM9KSqmBagNTVEnuuBgTYce29V44u3eiLD9UiT1_ZmpEhvSgc1AdZra-NB9FLb_GgtwAySrhNOZLx972l4svLyLUQZPoKODFMGcG24up3J0u-bwnvKKROo-G75F9PXzSBxI7L29_9JVmkVmSMXxe2_-MUHkBZbu1NEV1HUdP8vv6FA7-Y76aNn9tXXKrLkaicoPuCEVWvgbmKKK-hCRpXFo0tauGceCs6F_DrewxgK91bHIQGfhpdLobfL7J3u-VPQsHNIypuaA4qvqivh4REC89CFD55_Bc_2WqH0teNuInusyPM209fd82c0HhlIEoo_jzhBPtG5dduJ1WBw5hZzicdo7PNW8dMuXAxtLPSjayRBpd6RFH2NAUsqotKdSwWRF8EqwB9euM7oyvbF9zIJXmyByZ_LKRXteTUrVJwgfy339F14ZIvUUZXzIIEfSKtsFXpFEQbxROyII_iSoNrEtyfBfUo-iqW5x7YZGTWJ1EJFRvnhTPdnEco-pmOREKj-Y_JpWQTfiPKsgNvxuUtmM6_VoLad4XHjMEa_4jJLsO4DUv5RrfvwM68EwoilStcEybjXtPiydXMZwgL33F1J3SRbDp1f6WMfdvP8wqnPxcOqCriv4gqdlGeUWWAiaKLq0M2ssSkLSbcaC-2vT7PodninT0pBvdpz2PeebZHmLgYrFOC2fDT6w231pbls8qkwOWiMVWWAk5Xy8bsJOyBTjkWlGj1qYp26DvAg4fL449FnXxNlk1J8ZOb48oxbfUHLelTRUw_vAaAd0xc0AxLzHBOe_lHkbwPC-DIAWADEiA6zBgG45FFK0FKRU-nks_LZpMaUUg85Bd_UX04y_qf1wEqG-_kFWjY7QWvUqHTYurSymhk81sjWkXULmDA9-ovgvHWIOhvoc5tBIgDz33hxZ9u-cnFzgPgu8xlu-gugKplynR42bXKCP1UkKpCZuoTmPSgaGslMJnNMLVdU6QzteCJvPdiB5TtCAP1VS0t80ox8YqkfGDBuQG4-TaOAcAwhe5KgSgJ27TiOh2bpmJY6jwP-cNQxWZsrctM1uoJ2dawcvMZijRQ3PquO6OosU4ecGkaB_-jGaPNnE-BP3Ma4p2y_gVXbZJnEis_a5IFmHjodQUjGqEYrIQd9nSLNhkRR_9NerVtRxIoETiVkKInMKTmzFudutR76YvYdwuSUAWoQLdc5vg_LN1vAXMdX2BwRVeVjY94mkypJBRJNmrVesh3xMQCsJMistURBZwOjyoQodZYXo2iTgzNsTVYGS63oQreBilLkXBaTsTdm1UoL6yMYnmacuItO4et3_8UZ6kkuFjREp0SD76tF_7sXIx29smI9MqE5mHR3o6nMQVdW_0UZO4Ah2MsqjvgvOQ96QSqaYYx0V7ddYr0BvKKQdRxyhbKZ8jyT1eME8-tT6QKF9C-aN3kS8KlgJQ91tt6l_mK5-maNa2cnCQrnn1kbEhGRFOCEal-fRCs0X_pKYliX7PSm__9jT5Nf1PQtIWOCTvbEbpvYZgbCUeYMk-8vOJLPXyvWrHNazOgh5_NjlTBEq-on6Aj6Ja-Qv_sIrA9KB2_LoJ3ODQgdzzAR7XwS73RCksDeQhIqiK-ypCzmasGceEU6HftmyQzlMFR0snjPnaeG2wqrL3OV4mK2C_Vc50KJkodbj_efMRIpK4QYRAfCwxMvvdeRB4n9HQtX-cw3fJIDXYMLL01lrpRloAruSxmp0o8B7QAgW1I35oGw92jAdNpjaBwfG6sNjtWhjygKGAfqYhHPcrGWYY9Znp12tKEtGixz1wZVAXZipeU62WWr7uJXvrbNC0crEBu0JOpZIHOt6qykoeBmGrHLb7tM6FKHFmGiOQId1qR3qPwVGkDVXZOhgneFvlqibYPf2sCPf76QRPeWtA9Z4C0Wte8s5IQzm6hEsOCCINM7Du7XQtCyVCM1kaiAX4H0xbCJ93L4xSGajyTZTn5nOW6cyTdRduQ_OqwwJwAuBYfiSQz4B6eGcyMlz7P7b0kH4Y3n25kUfN39a8kwJqVSS7NkHuxHdjfI7kYtIC9oWDRdj7DFHgX8fXEDUnmtQdSH0AlgfhRbEsVlEsP1wRTwrPhk7M_H2yylF1N750CZRW69trPqOUlpyh0pZWxW10eicoiflx_TwumIMgmbex_xO0mUKMhkkzCTk55UXT9FsPBEjMJ7sdZfNQtwwUGQPgi975LQjA50oJbf59pFFgSrItefSlYSXiyy-jlbeDQ2KYPTuH-fFb8N4ZNii1ERAyK0ro351lxo2O7Xn3BaPZ7q2PKhQSCqcKatgLaApBODlt_g_I2CfyOe1pSFt6KW59oOuyen_EoEAMr-lNmsJ3F0BOvOSoQTFYaTCa9fnl1iLW-d60E1qYPwK0uMAnUIzBWigWr0PybwMdyA6e6Bq-hO5_ZgRTz1cekMqdML2jfUkoeyCmlQjBiUoqNp2zTINcrRqYtaJ0XjOCyTzwdq1kfGwMDzoSmbGjyxHsNnkMM0l5U4ByW-4c3rUh1eSmLt9kaPXaPuPbCJKbZWaph3WBwWS2plnVCCg5x7o_C17w3kUObk9FOnwRvHQA8MBWwpMGXNKNLeNeIoogFi4vjeXdk87NCLScCIAkcbFm96tfoUfxLYM6P0wvxo3in1GEj_c7GvQIf44ThHaJgLKbOk_n4P6FK275dri-a3Q7u--ifcIX4LrjKzLEe9kMVCFjhkKyeusISTMBMRGmFQbv_kmL_kh6THUsAgsaKwL2fS-63eDUMnhkEu0rnS2bbbVb2f8ZBS8EjKLnYceNfNZBKJ2weW9EpVyxc_-HJQSTjX8F9ZGrh2hMQB3N5hML5RYXOFQIHzt61lJcf6yOHQXOxeEepVY2q4pgySSRacJADHC7SJE4Evxy4A281-y5dJR9KPzqvYjp8DyAGfI-J1T4muQeLrYXv_Zg2oGzQM8xONZX0m8w7tGixbb4luXbKnQtci7P55FtKIwMiPuJnwvHcIIU91ZsXmhL6pGvUS2vIDLBVY3hSO3iMY1KHrqfDOLC7bg24pO0JkBa-_oBEBN3pFUILY_qV65B28WqT0Pw958fijDVK8cRHtkuCk6k8DRDQVGXGEdIAI4CLM8bcPilJTEj8Dbp0-gSjUEmLy6bbGarhZ_eCo80LnqgSsMjRkuZH89KwMExoZ2nsiQEzzzS8vEu_qkShTtyIKWHm8-A66gulKZWvFe_zFCpJHZtoL-tILYpKxojpYOONVPCvRs2Q0N_ksmgD7wUPF6OiF9ocQ6_lCb5cGYMt5yglMjU3e_Bvul1pQF-hAyMEJ9U0enaJMcjObLcJXXPVcdfHWgq5JecbOmsEpdJL9CrIaqjrUOCGBi6kVpsHJXA6Vy_Lzf5Y_6g39qEmbBzEJRJcu2DPfdGDGeLr0P3VjZOXkU6qh_en1v2zcikFJ2b9He2E523I2ySf6myWSaTPwmZJSrm8UpfdfiGRvovMjDmoISCK2FOSERI2I_oIRtQsaMsp9g-6n0A8Db99ByhKm113tDpm0rxZpbg1CeKEfeynwmLwBO4UY6tTKo-n2cmg6bnDv82gBkhZX5hli5fHNs6demDBX8kn0aAyxNTsCzIt4hNsLj6lZYGGNKd2hv6ek0bawHOPKC3fIgVA9827ltcDJyzj-5uJxmOdeX5zRO7JeH5NmtgNMGdAUwYeQMpcK9eu6YNv3NHw==","summary":[{"type":"summary_text","text":"**Assessing loopback and dispatch error behavior**"},{"type":"summary_text","text":"**Analyzing stale stream poison timing**"},{"type":"summary_text","text":"**Identifying PTY output blocking cause**"}]},{"id":"rs_0ae44a63c9031803016a5974fcc914819bbab0641e5ec8678b","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXUGqH8wwRY6NmI4cZllFM8b8oqKCYbuj2b58y1CP6iCs8kUaTwtZ6BgzBEbFp5s-aFNj-dlS06xBs5J0NYF0FicTyVWuS8NtCnNqQEz9PIybaReljAeTzoVAuHIEgmATdpiWf_xzfwoZuk_x70YCLvYD7X4ZaQKSlCNs7KLaCb7UC2TEajygZX1_CK8mguUmpjtZNRnJeIPPqxGYbMKbvfNC_8spyWbQ3Ho07eKM5fUGFFq0PDJ-FY9O-jAdvuG7snYq8ZkLDpuNz42HNroQD_x1Q-ByY5OoW9xdXblbqbS1LkkpBSfq1aPLhq_sm0luK2jSrTSbeCu_0CJPURtyWaWzKDgDpGFLRBuOGXSXsUvU-0bnOocZTy_GCD35DuXE_366YV8fVfl1XcJ0aJLFaOntptMekrC9PzgllV_2qCV6Oz5SU4PJAZ0wUZ-14LU9BmJMDjSgaKr1FdDK5ohB9v9vQuNCRNk8NGzIXpDOtV_wiPmdJyVNpoAeNaoyMaLPvGjHLi5hFwmfJw828kgHNXMc8B3PMAeLKoccl3yUo3fNQZ_OLNmrMgQepBOYPfVbktusiGVv6ShVz4GgHp2mA744t7Ovj9aVX3Ip0TN-YRuya88-16TjFK_y4f6nrRO5wmE1p2u388oHgxlocdJqT4T4X6xOiwhzziXvjTQDeK-e5UFOjwTaKHftuIlsvRW5xLy_sUMviybp-C-R1yxdroLPpi79rHcF8UXuBhAvqPtfxLdefEkJkzLGGNyUvBiIEnXJq-OQjDi5JAUzk40yifD_3Vx9QVzxi0nzpNY3u7zG64TgNMqlaqhcCKueKuTQcuYYGGun1Nc2hqMI-7ykmCerXdD1bXk8UUnDdU9ba9LCYkBBY6kV2cQAd8Cc-Fhoiz3EnschSzERcWAscySctlG2rRzDL5M6fVBx6vJ-7qsyldAsjcpErlGOLKsSMF6dMJRJsNIwnVvQ9JFdPW-CGDQ_OM4wDCG41rpUtJ0ta8c2vbccaMikKe8DtkXfLPjbilekUN-4YCVPFA23d1D19KdbUAumeeS9MmhUtUlH2AVH_A8Q7F2dTGeufJl-_2sBkOdoawSvyDhx3O7gXlZrFy0Gw9Cyso_BcuxUKyXMZLTSoyBajlVKBCdx5aGaG1l8ki1_PLSs8-m1e0vhiRdoLc3heLDAneNkgEqN3CWYtIxUJeCV9rzhF4ZBREwR2rvfwLXTxQyIAH2cmvXHDkyN6Jh990-ULpT_iBHsmgk9SsJTcgSf5IS3ZgZHVo81NtpzbvUZEMnD_i0Ni6solORpK8nMzVYbuwGIqQ_bXoQK1d5vJJJpTkM_xaJ3L9RfX1PM9p9ZSly8ez1aDtiUs9A0AnJwsV8uYoMQB0OCyLjqxUwjBF8tnw3Zwl7e0dTfmHMuVzGH9RpcZBNQgQ7niK6IGQTJHbuOpqbjVStvefZb33rZDvROJDEdnZsI81iuaRQly720ntpPzk8uNCr8PabnmeNDW8wM8XKothyNmi4_PY49BUemDEw-sCCUNIcfg2xIR3XLL3MdBlrLk2MNRVIWX7asO0JvOFLaPaVKj0vUFrrNEI3exaF68VnoBv_Y8H4vhEkCqLyuoPsxT-xkc56YtLk2qPb9oAvaCdGqdRSfWJPsag5UjpT3o2Ws0vkyXAulfhQfPWj8FyhApOhjUuBoF6tQluG1wII6AbxfjOQRwAVRGq6TJhVTIbQJXZfPMDKA6W6WFQppJMTEKfJYfmKKUSwN82PxaKNv0e8sxdTygPgvp-8MMfcoSHOJCUowOU3ULV2oxv4bjO2hwxTr63UsXRTDXXd05jIkHEADGfGYSnl3H0ib4DQExTfx0tyMPzao97ZzaPQhv1iFZ08Zp4MU9pwAeoCfpBygVDpDtV8Yeqo4iiS18uG-K8fbmqy_qiVFcjo1_00BM7H7GyYQznliamsjwVkoGANRzI24Fkvvj_1yuyoUPvsud_aE9lmosoLdZyDEdhfh5WvPJ8_x_LzSZ1q6ZDD2Kv62wuQBr65xjLLjcvsmfFw6tvmy2d33aeWoAJ2I9boac5cwro3ysw-Ht8XW6GfcqacSmS9PXdQewn9YF44DsYfIntQ5Mbr0emowdDGItEubjXyi1thBgq4AXF-N1b9OzESVB40KhCu_zlKITuiAk9z5jFd364OX5g-N47n1vFNrAdJkjpFYtsz9aecdRNQxKFVL57Q2E7dIpy6ZC_DNMlIeVhdz9yzEJDMlj-HBe5ONYrOEuw444G6xGPtYoi_xeGtznXABOdky3IpBIdpCjlRl1lS9ZgDRc1pMqNgrjOuOFPYzN1cM_7cBLrxOxsnvAn-4urft6NTBVO8ke3CdjTk2ZjDoQPLnzxgm2poLD7Ar-jkx5jxFlag6K83dF1HU1Siq3KbSjIkgU7bC39h38eh915_9Xp8l9mZp-Qf6KdD57LYhv_DffvV-8CUP53QXXKiuoekhCRPLnPVHXJr9KLK8Q7nCkyoBAadlmZyVY8R5JMFoXgXyRHNkfbXquORjF5Ov9a_5QNL4S8J5g_8FKNelCw8Azp_v-FXxQzXJ89F9FaFdIHDQZ4JfLei2LTMdT57g20M5ZKIKevo33IQmAbK7NY7_1JEb1vQ-PXWc9zELpMr1HJjZbrDmCEXw3QfDtmheNZ9rgpeMmBSm_MPQHB3waNNlXNanRhUh7r2","summary":[{"type":"summary_text","text":"**Diagnosing broker connection poisoning**"},{"type":"summary_text","text":"**Analyzing dispatch delay and wire command timing**"}]},{"id":"fc_0ae44a63c9031803016a5975062a3c819b8b6d6c954c2a3b40","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading connection poison teardown\",\"path\":\"crates/spt-daemon/src/conn.rs\",\"selector\":\"270-650\"}","call_id":"call_Aszt6cZpwDZM5cKaYGa9cJTy","name":"read"},{"id":"fc_0ae44a63c9031803016a5975062a50819bad8e5c31493e5e97","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading net send client wait\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":\"1380-1485\"}","call_id":"call_zuk96he1mC0hE0aAuGcriVwq","name":"read"},{"id":"fc_0ae44a63c9031803016a5975062a64819bba8942b993355ad2","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker net send dispatch\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"4080-4205\"}","call_id":"call_cfD5rLn4ZBi6WGJrl2SQPYM6","name":"read"},{"id":"fc_0ae44a63c9031803016a5975062a6c819bb4c1e9d592caa0c3","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding dispatch failure logging\",\"pattern\":\"DISPATCH:|DispatchOutcome::Failed|net_stream_send\\\\(|send_stream\\\\(\",\"path\":\"crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/attach.rs;crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/broker.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_CfC01aRiHHSHcyw31WZWxh5o","name":"grep"}]},"duration":38262.6745999977,"ttft":2201.472199998796,"contextSnapshot":{"promptTokens":203111,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Aszt6cZpwDZM5cKaYGa9cJTy|fc_0ae44a63c9031803016a5975062a3c819b8b6d6c954c2a3b40","toolName":"read","startedAt":"2026-07-17T00:19:18.514Z","args":{"path":"crates/spt-daemon/src/conn.rs"},"intent":"Reading connection poison teardown"},"id":"318351b7","parentId":"43e11bae","timestamp":"2026-07-17T00:19:18.514Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_zuk96he1mC0hE0aAuGcriVwq|fc_0ae44a63c9031803016a5975062a50819bad8e5c31493e5e97","toolName":"read","startedAt":"2026-07-17T00:19:18.514Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Reading net send client wait"},"id":"c2ef494f","parentId":"318351b7","timestamp":"2026-07-17T00:19:18.514Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_cfD5rLn4ZBi6WGJrl2SQPYM6|fc_0ae44a63c9031803016a5975062a64819bba8942b993355ad2","toolName":"read","startedAt":"2026-07-17T00:19:18.514Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker net send dispatch"},"id":"beb96d79","parentId":"c2ef494f","timestamp":"2026-07-17T00:19:18.514Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_CfC01aRiHHSHcyw31WZWxh5o|fc_0ae44a63c9031803016a5975062a6c819bb4c1e9d592caa0c3","toolName":"grep","startedAt":"2026-07-17T00:19:18.514Z","args":{"path":"crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/attach.rs;crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/broker.rs"},"intent":"Finding dispatch failure logging"},"id":"e6bdcaab","parentId":"beb96d79","timestamp":"2026-07-17T00:19:18.514Z"}
{"type":"message","id":"f3f43ecf","parentId":"e6bdcaab","timestamp":"2026-07-17T00:19:18.524Z","message":{"role":"toolResult","toolCallId":"call_Aszt6cZpwDZM5cKaYGa9cJTy|fc_0ae44a63c9031803016a5975062a3c819b8b6d6c954c2a3b40","toolName":"read","content":[{"type":"text","text":"impl Inner {\n…\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    fn lifecycle(&self, event: &str, extra: &str) {\n        let sep = if extra.is_empty() { \"\" } else { \" \" };\n        eprintln!(\n            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\n            self.attribution()\n        );\n    }\n\n    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\n    /// the halves share one OS object) and latch the poison. Idempotent and\n    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\n    /// no-op, and re-cancel closes the race where a write was issued a beat\n    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\n    /// `cause` (the organic caller's write error) rides the retirement record\n    /// when the retirement is NOT deadline-driven.\n    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\n        if !self.poisoned.swap(true, Ordering::AcqRel) {\n            // First poison of this conn: loud, once. The token is class-split\n            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n            eprintln!(\n                \"{}\",\n                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n            );\n        }\n        #[cfg(windows)]\n        // SAFETY: scalar kernel32 call on a handle whose owning half is kept\n        // alive until Drop (gate / writer stack / graveyard — see `raw`).\n        unsafe {\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\n        }\n        #[cfg(unix)]\n        // SAFETY: scalar shutdown(2) on an fd alive until Drop. SHUT_RDWR\n        // fails the blocked write (EPIPE), EOFs the parked read, and fails\n        // every future op — on Unix this one call IS the whole teardown.\n        unsafe {\n            let _ = libc::shutdown(self.raw, libc::SHUT_RDWR);\n        }\n        // Wake gate-waiters so they observe the poison instead of sleeping\n        // out their full deadline, and the watchdog so it re-evaluates.\n        self.gate_cv.notify_all();\n        self.dog_cv.notify_all();\n    }\n\n    /// Sever the physical connection (exactly once), AFTER the canceled op\n    /// reported completion: on Windows `DisconnectNamedPipe` forces the\n    /// client off (its next access fails), makes a re-issued server-side\n    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\n    /// new read in after the cancel), and defuses the drop-limbo flush; the\n    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\n    /// `shutdown` in [`poison_and_cancel`] already did all of this.\n    // On Unix the cfg(windows) teardown strips out and the idempotence\n    // guard's early `return` becomes the function tail — needless_return\n    // there, load-bearing here. Restructuring (e.g. `if !swapped { … }`)\n    // would leave Unix an empty if-body (`clippy::needless_if`) instead.\n    #[cfg_attr(unix, allow(clippy::needless_return))]\n    fn sever(&self) {\n        if self.severed.swap(true, Ordering::AcqRel) {\n            return;\n        }\n        #[cfg(windows)]\n        // SAFETY: as in poison_and_cancel — handle outlives the watchdog.\n        unsafe {\n            let _ = win::DisconnectNamedPipe(self.raw);\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\n        }\n    }\n\n    /// The watchdog body: park until a write is in flight, then sleep to its\n    /// absolute deadline. If the op is still in flight at the deadline,\n    /// poison + cancel, re-canceling periodically until the op reports\n    /// completion (the writer clears `inflight` the instant `write_frame`\n    /// returns — that return IS the buffer-release handshake), then sever.\n    fn watchdog(&self) {\n        let mut d = recover(&self.dog);\n        loop {\n            // Park until armed (or shutdown).\n            let (op, deadline) = loop {\n                if d.shutdown {\n                    return;\n                }\n                if let Some(armed) = d.inflight {\n                    break armed;\n                }\n                d = self\n                    .dog_cv\n                    .wait(d)\n                    .unwrap_or_else(|p| p.into_inner());\n            };\n            // Sleep toward the deadline while THIS op stays in flight.\n            let fired = loop {\n                if d.shutdown {\n                    return;\n                }\n                match d.inflight {\n                    Some((cur, _)) if cur == op => {}\n                    // Completed (or a newer op armed) before the deadline.\n                    _ => break false,\n                }\n                let now = Instant::now();\n                if now >= deadline {\n                    break true;\n                }\n                let (g, _) = self\n                    .dog_cv\n                    .wait_timeout(d, deadline - now)\n                    .unwrap_or_else(|p| p.into_inner());\n                d = g;\n            };\n            if !fired {\n                continue;\n            }\n            // Deadline: abort out of band, then wait for the canceled op to\n            // report completion before severing (re-cancel each slice — a\n            // cancel that raced the WriteFileEx issuance reaches nothing, so\n            // one shot is not enough). Latch timed_out FIRST so the aborted\n            // writer observes its failure as a DEADLINE retirement (wedge),\n            // not an organic conn death.\n            drop(d);\n            self.timed_out.store(true, Ordering::Release);\n            self.poison_and_cancel(None);\n            d = recover(&self.dog);\n            while matches!(d.inflight, Some((cur, _)) if cur == op) && !d.shutdown {\n                let (g, _) = self\n                    .dog_cv\n                    .wait_timeout(d, Duration::from_millis(100))\n                    .unwrap_or_else(|p| p.into_inner());\n                d = g;\n                drop(d);\n                self.poison_and_cancel(None);\n                d = recover(&self.dog);\n            }\n            if d.shutdown {\n                return;\n            }\n            drop(d);\n            self.sever();\n            d = recover(&self.dog);\n        }\n    }\n}\n\n/// The shared, serialized, BOUNDED send side of one physical broker\n/// connection — what [`crate::broker::SharedSend`] now aliases. Owns the\n/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\npub(crate) struct BrokerConn {\n    inner: Arc<Inner>,\n    /// Joined on drop — after `shutdown` is raised — so the raw handle the\n    /// watchdog aborts through strictly outlives every raw use.\n    dog: Option<JoinHandle<()>>,\n}\n\nimpl BrokerConn {\n    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\n    /// deadline (gate-wait + OS write completion) — the broker passes its\n    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\n        let raw = raw_of(&half);\n        let inner = Arc::new(Inner {\n            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n            label: Mutex::new(String::new()),\n            first_write_logged: AtomicBool::new(false),\n            gate: Mutex::new(Gate { half: Some(half) }),\n            gate_cv: Condvar::new(),\n            dog: Mutex::new(Dog {\n                inflight: None,\n                shutdown: false,\n            }),\n            dog_cv: Condvar::new(),\n            poisoned: AtomicBool::new(false),\n            timed_out: AtomicBool::new(false),\n            severed: AtomicBool::new(false),\n            graveyard: Mutex::new(None),\n            op_seq: AtomicU64::new(0),\n            bound,\n            raw,\n        });\n        let dog_inner = Arc::clone(&inner);\n        let dog = thread::Builder::new()\n            .name(\"conn-watchdog\".into())\n            .spawn(move || dog_inner.watchdog())\n            .ok();\n        BrokerConn { inner, dog }\n    }\n\n    /// The stable per-physical-conn id — what lifecycle records key on\n    /// (`Arc::ptr_eq` identity does not survive a log line).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn id(&self) -> u64 {\n        self.inner.id\n    }\n\n    /// Accrete an attribution fact onto this conn's label (subscriber role,\n    /// endpoint/session where known — e.g. `role=brain`,\n    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn describe(&self, fact: &str) {\n        let mut l = recover(&self.inner.label);\n        if l.ends_with('…') {\n            return;\n        }\n        if l.len() + fact.len() + 1 > LABEL_CAP {\n            l.push('…');\n            return;\n        }\n        if !l.is_empty() {\n            l.push(' ');\n        }\n        l.push_str(fact);\n    }\n\n    /// Emit a bounded per-conn lifecycle record through this conn's shared\n    /// attribution format — the broker's sink machinery uses it for\n    /// `writer-exit`, attach/replace, and detach events (leg d).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\n        self.inner.lifecycle(event, extra);\n    }\n\n    /// Bounded framed write. See [`Self::write_gated`].\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n        self.write_gated(env, || true)\n    }\n\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\n    /// serialized gate is acquired and BEFORE any byte is written — the same\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\n    /// superseded controller writer still bails atomically with respect to\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\n    pub(crate) fn write_gated(\n        &self,\n        env: &Envelope,\n        pre: impl FnOnce() -> bool,\n    ) -> io::Result<ConnWrite> {\n        let inner = &*self.inner;\n        let deadline = Instant::now() + inner.bound;\n\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\n        let mut g = recover(&inner.gate);\n        loop {\n            if inner.poisoned.load(Ordering::Acquire) {\n                return Err(poisoned_err());\n            }\n            if g.half.is_some() {\n                break;\n            }\n            let now = Instant::now();\n            if now >= deadline {\n                // The write we queued behind is stuck past OUR whole bound:\n                // that op is the wedge. Abort it out of band and fail — the\n                // watchdog's own deadline (armed at that op's entry) is also\n                // in flight; both paths converge on the same idempotent\n                // poison/cancel.\n                drop(g);\n                inner.timed_out.store(true, Ordering::Release);\n                inner.poison_and_cancel(None);\n                return Err(timeout_err());\n            }\n            let (g2, _) = inner\n                .gate_cv\n                .wait_timeout(g, deadline - now)\n                .unwrap_or_else(|p| p.into_inner());\n            g = g2;\n        }\n\n        if !pre() {\n            return Ok(ConnWrite::Superseded);\n        }\n\n        // Once-per-conn `write-start` lifecycle record (leg d): marks the conn's\n        // first committed write, so a conn that later retires can be correlated\n        // to when it went active (fresh-carrier churn shows as open→start→retire\n        // triplets with fresh ids).\n        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\n            inner.lifecycle(\"write-start\", \"\");\n        }\n\n        // ── Check out the half + arm the watchdog, then write OUT of locks. ──\n        let mut half = g.half.take().expect(\"gate half present at checkout\");\n        let op = inner.op_seq.fetch_add(1, Ordering::Relaxed) + 1;\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = Some((op, deadline));\n            inner.dog_cv.notify_all();\n        }\n        drop(g);\n\n        let res = write_frame(&mut half, env);\n\n        // Completion handshake: `write_frame` has returned, so the OS op —\n        // canceled or not — reported completion and the frame buffer is\n        // released. Clear the arm BEFORE deciding health, so the watchdog\n        // never severs while the op is formally in flight.\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = None;\n            inner.dog_cv.notify_all();\n        }\n\n        let mut g = recover(&inner.gate);\n        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\n            g.half = Some(half);\n            drop(g);\n            inner.gate_cv.notify_all();\n            return Ok(ConnWrite::Done);\n        }\n\n        // ── Retire: poison the whole conn, abort the read side so\n        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\n        // graveyard (its handle must outlive any late raw abort; never\n        // re-gated, never reused — a timed-out length-prefixed frame may be\n        // partially written). ──\n        drop(g);\n        inner.poison_and_cancel(res.as_ref().err());\n        inner.sever();\n        *recover(&inner.graveyard) = Some(half);\n        inner.gate_cv.notify_all();\n        // A DEADLINE retirement (the watchdog aborted OUR op, or the conn was\n        // already deadline-poisoned) surfaces as TimedOut — the caller can\n        // count the wedge (W2 stall-evict tally). An organic I/O failure\n        // (peer death) keeps its own error.\n        if inner.timed_out.load(Ordering::Acquire) {\n            return Err(timeout_err());\n        }\n        Err(match res {\n            Err(e) => e,\n            Ok(()) => poisoned_err(),\n        })\n    }\n}\n\n/// TEST-ONLY determinism lever: holds the serialized write gate (the half is\n/// checked out) so every writer parks AT THE GATE until the pin drops — the\n/// round-4 analog of pinning the old `Mutex<SendHalf>` across a registration\n/// window (the P1c epoch-gate keystone unit relies on it).\n#[cfg(test)]\npub(crate) struct TestGatePin {\n    inner: Arc<Inner>,\n    half: Option<SendHalf>,\n}\n\n#[cfg(test)]\nimpl Drop for TestGatePin {\n    fn drop(&mut self) {\n        let mut g = recover(&self.inner.gate);\n        g.half = self.half.take();\n        drop(g);\n        self.inner.gate_cv.notify_all();\n    }\n}\n\n#[cfg(test)]\nimpl BrokerConn {\n    /// Check the half out of the gate without arming the watchdog — writers\n    /// queue behind the gate (bounded by their own deadlines) until the\n    /// returned pin drops. Test-only; never a production path.\n    pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {\n        let mut g = recover(&self.inner.gate);\n        let half = g.half.take().expect(\"pin: gate half present\");\n        TestGatePin {\n            inner: Arc::clone(&self.inner),\n            half: Some(half),\n        }\n    }\n}\n\nimpl Drop for BrokerConn {\n    fn drop(&mut self) {\n        // Once-per-conn `transport-close` lifecycle record (leg d): the handle\n        // is about to close — the terminal record of this conn id's life, with\n        // how it ended (organic EOF vs poisoned vs deadline-poisoned).\n        self.inner.lifecycle(\n            \"transport-close\",\n            &format!(\n                \"poisoned={} timed_out={}\",\n                self.inner.poisoned.load(Ordering::Acquire),\n                self.inner.timed_out.load(Ordering::Acquire)\n            ),\n        );\n        {\n            let mut d = recover(&self.inner.dog);\n…\n        }\n…\n    }\n}\n\n[Showing lines 269-653 of 815. Use :654 to continue]"}],"details":{"truncation":{"content":"    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    fn lifecycle(&self, event: &str, extra: &str) {\r\n        let sep = if extra.is_empty() { \"\" } else { \" \" };\r\n        eprintln!(\r\n            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\r\n            self.attribution()\r\n        );\r\n    }\r\n\r\n    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\r\n    /// the halves share one OS object) and latch the poison. Idempotent and\r\n    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\r\n    /// no-op, and re-cancel closes the race where a write was issued a beat\r\n    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\r\n    /// `cause` (the organic caller's write error) rides the retirement record\r\n    /// when the retirement is NOT deadline-driven.\r\n    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\r\n        if !self.poisoned.swap(true, Ordering::AcqRel) {\r\n            // First poison of this conn: loud, once. The token is class-split\r\n            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\r\n            // observable), organic fast-fail → CONN_WRITE_RETIRED.\r\n            eprintln!(\r\n                \"{}\",\r\n                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\r\n            );\r\n        }\r\n        #[cfg(windows)]\r\n        // SAFETY: scalar kernel32 call on a handle whose owning half is kept\r\n        // alive until Drop (gate / writer stack / graveyard — see `raw`).\r\n        unsafe {\r\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\r\n        }\r\n        #[cfg(unix)]\r\n        // SAFETY: scalar shutdown(2) on an fd alive until Drop. SHUT_RDWR\r\n        // fails the blocked write (EPIPE), EOFs the parked read, and fails\r\n        // every future op — on Unix this one call IS the whole teardown.\r\n        unsafe {\r\n            let _ = libc::shutdown(self.raw, libc::SHUT_RDWR);\r\n        }\r\n        // Wake gate-waiters so they observe the poison instead of sleeping\r\n        // out their full deadline, and the watchdog so it re-evaluates.\r\n        self.gate_cv.notify_all();\r\n        self.dog_cv.notify_all();\r\n    }\r\n\r\n    /// Sever the physical connection (exactly once), AFTER the canceled op\r\n    /// reported completion: on Windows `DisconnectNamedPipe` forces the\r\n    /// client off (its next access fails), makes a re-issued server-side\r\n    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\r\n    /// new read in after the cancel), and defuses the drop-limbo flush; the\r\n    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\r\n    /// `shutdown` in [`poison_and_cancel`] already did all of this.\r\n    // On Unix the cfg(windows) teardown strips out and the idempotence\r\n    // guard's early `return` becomes the function tail — needless_return\r\n    // there, load-bearing here. Restructuring (e.g. `if !swapped { … }`)\r\n    // would leave Unix an empty if-body (`clippy::needless_if`) instead.\r\n    #[cfg_attr(unix, allow(clippy::needless_return))]\r\n    fn sever(&self) {\r\n        if self.severed.swap(true, Ordering::AcqRel) {\r\n            return;\r\n        }\r\n        #[cfg(windows)]\r\n        // SAFETY: as in poison_and_cancel — handle outlives the watchdog.\r\n        unsafe {\r\n            let _ = win::DisconnectNamedPipe(self.raw);\r\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\r\n        }\r\n    }\r\n\r\n    /// The watchdog body: park until a write is in flight, then sleep to its\r\n    /// absolute deadline. If the op is still in flight at the deadline,\r\n    /// poison + cancel, re-canceling periodically until the op reports\r\n    /// completion (the writer clears `inflight` the instant `write_frame`\r\n    /// returns — that return IS the buffer-release handshake), then sever.\r\n    fn watchdog(&self) {\r\n        let mut d = recover(&self.dog);\r\n        loop {\r\n            // Park until armed (or shutdown).\r\n            let (op, deadline) = loop {\r\n                if d.shutdown {\r\n                    return;\r\n                }\r\n                if let Some(armed) = d.inflight {\r\n                    break armed;\r\n                }\r\n                d = self\r\n                    .dog_cv\r\n                    .wait(d)\r\n                    .unwrap_or_else(|p| p.into_inner());\r\n            };\r\n            // Sleep toward the deadline while THIS op stays in flight.\r\n            let fired = loop {\r\n                if d.shutdown {\r\n                    return;\r\n                }\r\n                match d.inflight {\r\n                    Some((cur, _)) if cur == op => {}\r\n                    // Completed (or a newer op armed) before the deadline.\r\n                    _ => break false,\r\n                }\r\n                let now = Instant::now();\r\n                if now >= deadline {\r\n                    break true;\r\n                }\r\n                let (g, _) = self\r\n                    .dog_cv\r\n                    .wait_timeout(d, deadline - now)\r\n                    .unwrap_or_else(|p| p.into_inner());\r\n                d = g;\r\n            };\r\n            if !fired {\r\n                continue;\r\n            }\r\n            // Deadline: abort out of band, then wait for the canceled op to\r\n            // report completion before severing (re-cancel each slice — a\r\n            // cancel that raced the WriteFileEx issuance reaches nothing, so\r\n            // one shot is not enough). Latch timed_out FIRST so the aborted\r\n            // writer observes its failure as a DEADLINE retirement (wedge),\r\n            // not an organic conn death.\r\n            drop(d);\r\n            self.timed_out.store(true, Ordering::Release);\r\n            self.poison_and_cancel(None);\r\n            d = recover(&self.dog);\r\n            while matches!(d.inflight, Some((cur, _)) if cur == op) && !d.shutdown {\r\n                let (g, _) = self\r\n                    .dog_cv\r\n                    .wait_timeout(d, Duration::from_millis(100))\r\n                    .unwrap_or_else(|p| p.into_inner());\r\n                d = g;\r\n                drop(d);\r\n                self.poison_and_cancel(None);\r\n                d = recover(&self.dog);\r\n            }\r\n            if d.shutdown {\r\n                return;\r\n            }\r\n            drop(d);\r\n            self.sever();\r\n            d = recover(&self.dog);\r\n        }\r\n    }\r\n}\r\n\r\n/// The shared, serialized, BOUNDED send side of one physical broker\r\n/// connection — what [`crate::broker::SharedSend`] now aliases. Owns the\r\n/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\r\npub(crate) struct BrokerConn {\r\n    inner: Arc<Inner>,\r\n    /// Joined on drop — after `shutdown` is raised — so the raw handle the\r\n    /// watchdog aborts through strictly outlives every raw use.\r\n    dog: Option<JoinHandle<()>>,\r\n}\r\n\r\nimpl BrokerConn {\r\n    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\r\n    /// deadline (gate-wait + OS write completion) — the broker passes its\r\n    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\r\n    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\r\n        let raw = raw_of(&half);\r\n        let inner = Arc::new(Inner {\r\n            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\r\n            label: Mutex::new(String::new()),\r\n            first_write_logged: AtomicBool::new(false),\r\n            gate: Mutex::new(Gate { half: Some(half) }),\r\n            gate_cv: Condvar::new(),\r\n            dog: Mutex::new(Dog {\r\n                inflight: None,\r\n                shutdown: false,\r\n            }),\r\n            dog_cv: Condvar::new(),\r\n            poisoned: AtomicBool::new(false),\r\n            timed_out: AtomicBool::new(false),\r\n            severed: AtomicBool::new(false),\r\n            graveyard: Mutex::new(None),\r\n            op_seq: AtomicU64::new(0),\r\n            bound,\r\n            raw,\r\n        });\r\n        let dog_inner = Arc::clone(&inner);\r\n        let dog = thread::Builder::new()\r\n            .name(\"conn-watchdog\".into())\r\n            .spawn(move || dog_inner.watchdog())\r\n            .ok();\r\n        BrokerConn { inner, dog }\r\n    }\r\n\r\n    /// The stable per-physical-conn id — what lifecycle records key on\r\n    /// (`Arc::ptr_eq` identity does not survive a log line).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn id(&self) -> u64 {\r\n        self.inner.id\r\n    }\r\n\r\n    /// Accrete an attribution fact onto this conn's label (subscriber role,\r\n    /// endpoint/session where known — e.g. `role=brain`,\r\n    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\r\n    /// and ride EVERY subsequent lifecycle record for this conn; growth is\r\n    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn describe(&self, fact: &str) {\r\n        let mut l = recover(&self.inner.label);\r\n        if l.ends_with('…') {\r\n            return;\r\n        }\r\n        if l.len() + fact.len() + 1 > LABEL_CAP {\r\n            l.push('…');\r\n            return;\r\n        }\r\n        if !l.is_empty() {\r\n            l.push(' ');\r\n        }\r\n        l.push_str(fact);\r\n    }\r\n\r\n    /// Emit a bounded per-conn lifecycle record through this conn's shared\r\n    /// attribution format — the broker's sink machinery uses it for\r\n    /// `writer-exit`, attach/replace, and detach events (leg d).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\r\n        self.inner.lifecycle(event, extra);\r\n    }\r\n\r\n    /// Bounded framed write. See [`Self::write_gated`].\r\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\r\n        self.write_gated(env, || true)\r\n    }\r\n\r\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\r\n    /// serialized gate is acquired and BEFORE any byte is written — the same\r\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\r\n    /// superseded controller writer still bails atomically with respect to\r\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\r\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\r\n    pub(crate) fn write_gated(\r\n        &self,\r\n        env: &Envelope,\r\n        pre: impl FnOnce() -> bool,\r\n    ) -> io::Result<ConnWrite> {\r\n        let inner = &*self.inner;\r\n        let deadline = Instant::now() + inner.bound;\r\n\r\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\r\n        let mut g = recover(&inner.gate);\r\n        loop {\r\n            if inner.poisoned.load(Ordering::Acquire) {\r\n                return Err(poisoned_err());\r\n            }\r\n            if g.half.is_some() {\r\n                break;\r\n            }\r\n            let now = Instant::now();\r\n            if now >= deadline {\r\n                // The write we queued behind is stuck past OUR whole bound:\r\n                // that op is the wedge. Abort it out of band and fail — the\r\n                // watchdog's own deadline (armed at that op's entry) is also\r\n                // in flight; both paths converge on the same idempotent\r\n                // poison/cancel.\r\n                drop(g);\r\n                inner.timed_out.store(true, Ordering::Release);\r\n                inner.poison_and_cancel(None);\r\n                return Err(timeout_err());\r\n            }\r\n            let (g2, _) = inner\r\n                .gate_cv\r\n                .wait_timeout(g, deadline - now)\r\n                .unwrap_or_else(|p| p.into_inner());\r\n            g = g2;\r\n        }\r\n\r\n        if !pre() {\r\n            return Ok(ConnWrite::Superseded);\r\n        }\r\n\r\n        // Once-per-conn `write-start` lifecycle record (leg d): marks the conn's\r\n        // first committed write, so a conn that later retires can be correlated\r\n        // to when it went active (fresh-carrier churn shows as open→start→retire\r\n        // triplets with fresh ids).\r\n        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\r\n            inner.lifecycle(\"write-start\", \"\");\r\n        }\r\n\r\n        // ── Check out the half + arm the watchdog, then write OUT of locks. ──\r\n        let mut half = g.half.take().expect(\"gate half present at checkout\");\r\n        let op = inner.op_seq.fetch_add(1, Ordering::Relaxed) + 1;\r\n        {\r\n            let mut d = recover(&inner.dog);\r\n            d.inflight = Some((op, deadline));\r\n            inner.dog_cv.notify_all();\r\n        }\r\n        drop(g);\r\n\r\n        let res = write_frame(&mut half, env);\r\n\r\n        // Completion handshake: `write_frame` has returned, so the OS op —\r\n        // canceled or not — reported completion and the frame buffer is\r\n        // released. Clear the arm BEFORE deciding health, so the watchdog\r\n        // never severs while the op is formally in flight.\r\n        {\r\n            let mut d = recover(&inner.dog);\r\n            d.inflight = None;\r\n            inner.dog_cv.notify_all();\r\n        }\r\n\r\n        let mut g = recover(&inner.gate);\r\n        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\r\n            g.half = Some(half);\r\n            drop(g);\r\n            inner.gate_cv.notify_all();\r\n            return Ok(ConnWrite::Done);\r\n        }\r\n\r\n        // ── Retire: poison the whole conn, abort the read side so\r\n        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\r\n        // graveyard (its handle must outlive any late raw abort; never\r\n        // re-gated, never reused — a timed-out length-prefixed frame may be\r\n        // partially written). ──\r\n        drop(g);\r\n        inner.poison_and_cancel(res.as_ref().err());\r\n        inner.sever();\r\n        *recover(&inner.graveyard) = Some(half);\r\n        inner.gate_cv.notify_all();\r\n        // A DEADLINE retirement (the watchdog aborted OUR op, or the conn was\r\n        // already deadline-poisoned) surfaces as TimedOut — the caller can\r\n        // count the wedge (W2 stall-evict tally). An organic I/O failure\r\n        // (peer death) keeps its own error.\r\n        if inner.timed_out.load(Ordering::Acquire) {\r\n            return Err(timeout_err());\r\n        }\r\n        Err(match res {\r\n            Err(e) => e,\r\n            Ok(()) => poisoned_err(),\r\n        })\r\n    }\r\n}\r\n\r\n/// TEST-ONLY determinism lever: holds the serialized write gate (the half is\r\n/// checked out) so every writer parks AT THE GATE until the pin drops — the\r\n/// round-4 analog of pinning the old `Mutex<SendHalf>` across a registration\r\n/// window (the P1c epoch-gate keystone unit relies on it).\r\n#[cfg(test)]\r\npub(crate) struct TestGatePin {\r\n    inner: Arc<Inner>,\r\n    half: Option<SendHalf>,\r\n}\r\n\r\n#[cfg(test)]\r\nimpl Drop for TestGatePin {\r\n    fn drop(&mut self) {\r\n        let mut g = recover(&self.inner.gate);\r\n        g.half = self.half.take();\r\n        drop(g);\r\n        self.inner.gate_cv.notify_all();\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nimpl BrokerConn {\r\n    /// Check the half out of the gate without arming the watchdog — writers\r\n    /// queue behind the gate (bounded by their own deadlines) until the\r\n    /// returned pin drops. Test-only; never a production path.\r\n    pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {\r\n        let mut g = recover(&self.inner.gate);\r\n        let half = g.half.take().expect(\"pin: gate half present\");\r\n        TestGatePin {\r\n            inner: Arc::clone(&self.inner),\r\n            half: Some(half),\r\n        }\r\n    }\r\n}\r\n\r\nimpl Drop for BrokerConn {\r\n    fn drop(&mut self) {\r\n        // Once-per-conn `transport-close` lifecycle record (leg d): the handle\r\n        // is about to close — the terminal record of this conn id's life, with\r\n        // how it ended (organic EOF vs poisoned vs deadline-poisoned).\r\n        self.inner.lifecycle(\r\n            \"transport-close\",\r\n            &format!(\r\n                \"poisoned={} timed_out={}\",\r\n                self.inner.poisoned.load(Ordering::Acquire),\r\n                self.inner.timed_out.load(Ordering::Acquire)\r\n            ),\r\n        );\r\n        {\r\n            let mut d = recover(&self.inner.dog);\r","truncated":true,"truncatedBy":"lines","totalLines":547,"totalBytes":16133,"outputLines":385,"outputBytes":16133,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"impl Inner {\n…\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    fn lifecycle(&self, event: &str, extra: &str) {\r\n        let sep = if extra.is_empty() { \"\" } else { \" \" };\r\n        eprintln!(\r\n            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\r\n            self.attribution()\r\n        );\r\n    }\r\n\r\n    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\r\n    /// the halves share one OS object) and latch the poison. Idempotent and\r\n    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\r\n    /// no-op, and re-cancel closes the race where a write was issued a beat\r\n    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\r\n    /// `cause` (the organic caller's write error) rides the retirement record\r\n    /// when the retirement is NOT deadline-driven.\r\n    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\r\n        if !self.poisoned.swap(true, Ordering::AcqRel) {\r\n            // First poison of this conn: loud, once. The token is class-split\r\n            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\r\n            // observable), organic fast-fail → CONN_WRITE_RETIRED.\r\n            eprintln!(\r\n                \"{}\",\r\n                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\r\n            );\r\n        }\r\n        #[cfg(windows)]\r\n        // SAFETY: scalar kernel32 call on a handle whose owning half is kept\r\n        // alive until Drop (gate / writer stack / graveyard — see `raw`).\r\n        unsafe {\r\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\r\n        }\r\n        #[cfg(unix)]\r\n        // SAFETY: scalar shutdown(2) on an fd alive until Drop. SHUT_RDWR\r\n        // fails the blocked write (EPIPE), EOFs the parked read, and fails\r\n        // every future op — on Unix this one call IS the whole teardown.\r\n        unsafe {\r\n            let _ = libc::shutdown(self.raw, libc::SHUT_RDWR);\r\n        }\r\n        // Wake gate-waiters so they observe the poison instead of sleeping\r\n        // out their full deadline, and the watchdog so it re-evaluates.\r\n        self.gate_cv.notify_all();\r\n        self.dog_cv.notify_all();\r\n    }\r\n\r\n    /// Sever the physical connection (exactly once), AFTER the canceled op\r\n    /// reported completion: on Windows `DisconnectNamedPipe` forces the\r\n    /// client off (its next access fails), makes a re-issued server-side\r\n    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\r\n    /// new read in after the cancel), and defuses the drop-limbo flush; the\r\n    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\r\n    /// `shutdown` in [`poison_and_cancel`] already did all of this.\r\n    // On Unix the cfg(windows) teardown strips out and the idempotence\r\n    // guard's early `return` becomes the function tail — needless_return\r\n    // there, load-bearing here. Restructuring (e.g. `if !swapped { … }`)\r\n    // would leave Unix an empty if-body (`clippy::needless_if`) instead.\r\n    #[cfg_attr(unix, allow(clippy::needless_return))]\r\n    fn sever(&self) {\r\n        if self.severed.swap(true, Ordering::AcqRel) {\r\n            return;\r\n        }\r\n        #[cfg(windows)]\r\n        // SAFETY: as in poison_and_cancel — handle outlives the watchdog.\r\n        unsafe {\r\n            let _ = win::DisconnectNamedPipe(self.raw);\r\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\r\n        }\r\n    }\r\n\r\n    /// The watchdog body: park until a write is in flight, then sleep to its\r\n    /// absolute deadline. If the op is still in flight at the deadline,\r\n    /// poison + cancel, re-canceling periodically until the op reports\r\n    /// completion (the writer clears `inflight` the instant `write_frame`\r\n    /// returns — that return IS the buffer-release handshake), then sever.\r\n    fn watchdog(&self) {\r\n        let mut d = recover(&self.dog);\r\n        loop {\r\n            // Park until armed (or shutdown).\r\n            let (op, deadline) = loop {\r\n                if d.shutdown {\r\n                    return;\r\n                }\r\n                if let Some(armed) = d.inflight {\r\n                    break armed;\r\n                }\r\n                d = self\r\n                    .dog_cv\r\n                    .wait(d)\r\n                    .unwrap_or_else(|p| p.into_inner());\r\n            };\r\n            // Sleep toward the deadline while THIS op stays in flight.\r\n            let fired = loop {\r\n                if d.shutdown {\r\n                    return;\r\n                }\r\n                match d.inflight {\r\n                    Some((cur, _)) if cur == op => {}\r\n                    // Completed (or a newer op armed) before the deadline.\r\n                    _ => break false,\r\n                }\r\n                let now = Instant::now();\r\n                if now >= deadline {\r\n                    break true;\r\n                }\r\n                let (g, _) = self\r\n                    .dog_cv\r\n                    .wait_timeout(d, deadline - now)\r\n                    .unwrap_or_else(|p| p.into_inner());\r\n                d = g;\r\n            };\r\n            if !fired {\r\n                continue;\r\n            }\r\n            // Deadline: abort out of band, then wait for the canceled op to\r\n            // report completion before severing (re-cancel each slice — a\r\n            // cancel that raced the WriteFileEx issuance reaches nothing, so\r\n            // one shot is not enough). Latch timed_out FIRST so the aborted\r\n            // writer observes its failure as a DEADLINE retirement (wedge),\r\n            // not an organic conn death.\r\n            drop(d);\r\n            self.timed_out.store(true, Ordering::Release);\r\n            self.poison_and_cancel(None);\r\n            d = recover(&self.dog);\r\n            while matches!(d.inflight, Some((cur, _)) if cur == op) && !d.shutdown {\r\n                let (g, _) = self\r\n                    .dog_cv\r\n                    .wait_timeout(d, Duration::from_millis(100))\r\n                    .unwrap_or_else(|p| p.into_inner());\r\n                d = g;\r\n                drop(d);\r\n                self.poison_and_cancel(None);\r\n                d = recover(&self.dog);\r\n            }\r\n            if d.shutdown {\r\n                return;\r\n            }\r\n            drop(d);\r\n            self.sever();\r\n            d = recover(&self.dog);\r\n        }\r\n    }\r\n}\r\n\r\n/// The shared, serialized, BOUNDED send side of one physical broker\r\n/// connection — what [`crate::broker::SharedSend`] now aliases. Owns the\r\n/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\r\npub(crate) struct BrokerConn {\r\n    inner: Arc<Inner>,\r\n    /// Joined on drop — after `shutdown` is raised — so the raw handle the\r\n    /// watchdog aborts through strictly outlives every raw use.\r\n    dog: Option<JoinHandle<()>>,\r\n}\r\n\r\nimpl BrokerConn {\r\n    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\r\n    /// deadline (gate-wait + OS write completion) — the broker passes its\r\n    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\r\n    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\r\n        let raw = raw_of(&half);\r\n        let inner = Arc::new(Inner {\r\n            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\r\n            label: Mutex::new(String::new()),\r\n            first_write_logged: AtomicBool::new(false),\r\n            gate: Mutex::new(Gate { half: Some(half) }),\r\n            gate_cv: Condvar::new(),\r\n            dog: Mutex::new(Dog {\r\n                inflight: None,\r\n                shutdown: false,\r\n            }),\r\n            dog_cv: Condvar::new(),\r\n            poisoned: AtomicBool::new(false),\r\n            timed_out: AtomicBool::new(false),\r\n            severed: AtomicBool::new(false),\r\n            graveyard: Mutex::new(None),\r\n            op_seq: AtomicU64::new(0),\r\n            bound,\r\n            raw,\r\n        });\r\n        let dog_inner = Arc::clone(&inner);\r\n        let dog = thread::Builder::new()\r\n            .name(\"conn-watchdog\".into())\r\n            .spawn(move || dog_inner.watchdog())\r\n            .ok();\r\n        BrokerConn { inner, dog }\r\n    }\r\n\r\n    /// The stable per-physical-conn id — what lifecycle records key on\r\n    /// (`Arc::ptr_eq` identity does not survive a log line).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn id(&self) -> u64 {\r\n        self.inner.id\r\n    }\r\n\r\n    /// Accrete an attribution fact onto this conn's label (subscriber role,\r\n    /// endpoint/session where known — e.g. `role=brain`,\r\n    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\r\n    /// and ride EVERY subsequent lifecycle record for this conn; growth is\r\n    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn describe(&self, fact: &str) {\r\n        let mut l = recover(&self.inner.label);\r\n        if l.ends_with('…') {\r\n            return;\r\n        }\r\n        if l.len() + fact.len() + 1 > LABEL_CAP {\r\n            l.push('…');\r\n            return;\r\n        }\r\n        if !l.is_empty() {\r\n            l.push(' ');\r\n        }\r\n        l.push_str(fact);\r\n    }\r\n\r\n    /// Emit a bounded per-conn lifecycle record through this conn's shared\r\n    /// attribution format — the broker's sink machinery uses it for\r\n    /// `writer-exit`, attach/replace, and detach events (leg d).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\r\n        self.inner.lifecycle(event, extra);\r\n    }\r\n\r\n    /// Bounded framed write. See [`Self::write_gated`].\r\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\r\n        self.write_gated(env, || true)\r\n    }\r\n\r\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\r\n    /// serialized gate is acquired and BEFORE any byte is written — the same\r\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\r\n    /// superseded controller writer still bails atomically with respect to\r\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\r\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\r\n    pub(crate) fn write_gated(\r\n        &self,\r\n        env: &Envelope,\r\n        pre: impl FnOnce() -> bool,\r\n    ) -> io::Result<ConnWrite> {\r\n        let inner = &*self.inner;\r\n        let deadline = Instant::now() + inner.bound;\r\n\r\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\r\n        let mut g = recover(&inner.gate);\r\n        loop {\r\n            if inner.poisoned.load(Ordering::Acquire) {\r\n                return Err(poisoned_err());\r\n            }\r\n            if g.half.is_some() {\r\n                break;\r\n            }\r\n            let now = Instant::now();\r\n            if now >= deadline {\r\n                // The write we queued behind is stuck past OUR whole bound:\r\n                // that op is the wedge. Abort it out of band and fail — the\r\n                // watchdog's own deadline (armed at that op's entry) is also\r\n                // in flight; both paths converge on the same idempotent\r\n                // poison/cancel.\r\n                drop(g);\r\n                inner.timed_out.store(true, Ordering::Release);\r\n                inner.poison_and_cancel(None);\r\n                return Err(timeout_err());\r\n            }\r\n            let (g2, _) = inner\r\n                .gate_cv\r\n                .wait_timeout(g, deadline - now)\r\n                .unwrap_or_else(|p| p.into_inner());\r\n            g = g2;\r\n        }\r\n\r\n        if !pre() {\r\n            return Ok(ConnWrite::Superseded);\r\n        }\r\n\r\n        // Once-per-conn `write-start` lifecycle record (leg d): marks the conn's\r\n        // first committed write, so a conn that later retires can be correlated\r\n        // to when it went active (fresh-carrier churn shows as open→start→retire\r\n        // triplets with fresh ids).\r\n        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\r\n            inner.lifecycle(\"write-start\", \"\");\r\n        }\r\n\r\n        // ── Check out the half + arm the watchdog, then write OUT of locks. ──\r\n        let mut half = g.half.take().expect(\"gate half present at checkout\");\r\n        let op = inner.op_seq.fetch_add(1, Ordering::Relaxed) + 1;\r\n        {\r\n            let mut d = recover(&inner.dog);\r\n            d.inflight = Some((op, deadline));\r\n            inner.dog_cv.notify_all();\r\n        }\r\n        drop(g);\r\n\r\n        let res = write_frame(&mut half, env);\r\n\r\n        // Completion handshake: `write_frame` has returned, so the OS op —\r\n        // canceled or not — reported completion and the frame buffer is\r\n        // released. Clear the arm BEFORE deciding health, so the watchdog\r\n        // never severs while the op is formally in flight.\r\n        {\r\n            let mut d = recover(&inner.dog);\r\n            d.inflight = None;\r\n            inner.dog_cv.notify_all();\r\n        }\r\n\r\n        let mut g = recover(&inner.gate);\r\n        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\r\n            g.half = Some(half);\r\n            drop(g);\r\n            inner.gate_cv.notify_all();\r\n            return Ok(ConnWrite::Done);\r\n        }\r\n\r\n        // ── Retire: poison the whole conn, abort the read side so\r\n        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\r\n        // graveyard (its handle must outlive any late raw abort; never\r\n        // re-gated, never reused — a timed-out length-prefixed frame may be\r\n        // partially written). ──\r\n        drop(g);\r\n        inner.poison_and_cancel(res.as_ref().err());\r\n        inner.sever();\r\n        *recover(&inner.graveyard) = Some(half);\r\n        inner.gate_cv.notify_all();\r\n        // A DEADLINE retirement (the watchdog aborted OUR op, or the conn was\r\n        // already deadline-poisoned) surfaces as TimedOut — the caller can\r\n        // count the wedge (W2 stall-evict tally). An organic I/O failure\r\n        // (peer death) keeps its own error.\r\n        if inner.timed_out.load(Ordering::Acquire) {\r\n            return Err(timeout_err());\r\n        }\r\n        Err(match res {\r\n            Err(e) => e,\r\n            Ok(()) => poisoned_err(),\r\n        })\r\n    }\r\n}\r\n\r\n/// TEST-ONLY determinism lever: holds the serialized write gate (the half is\r\n/// checked out) so every writer parks AT THE GATE until the pin drops — the\r\n/// round-4 analog of pinning the old `Mutex<SendHalf>` across a registration\r\n/// window (the P1c epoch-gate keystone unit relies on it).\r\n#[cfg(test)]\r\npub(crate) struct TestGatePin {\r\n    inner: Arc<Inner>,\r\n    half: Option<SendHalf>,\r\n}\r\n\r\n#[cfg(test)]\r\nimpl Drop for TestGatePin {\r\n    fn drop(&mut self) {\r\n        let mut g = recover(&self.inner.gate);\r\n        g.half = self.half.take();\r\n        drop(g);\r\n        self.inner.gate_cv.notify_all();\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nimpl BrokerConn {\r\n    /// Check the half out of the gate without arming the watchdog — writers\r\n    /// queue behind the gate (bounded by their own deadlines) until the\r\n    /// returned pin drops. Test-only; never a production path.\r\n    pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {\r\n        let mut g = recover(&self.inner.gate);\r\n        let half = g.half.take().expect(\"pin: gate half present\");\r\n        TestGatePin {\r\n            inner: Arc::clone(&self.inner),\r\n            half: Some(half),\r\n        }\r\n    }\r\n}\r\n\r\nimpl Drop for BrokerConn {\r\n    fn drop(&mut self) {\r\n        // Once-per-conn `transport-close` lifecycle record (leg d): the handle\r\n        // is about to close — the terminal record of this conn id's life, with\r\n        // how it ended (organic EOF vs poisoned vs deadline-poisoned).\r\n        self.inner.lifecycle(\r\n            \"transport-close\",\r\n            &format!(\r\n                \"poisoned={} timed_out={}\",\r\n                self.inner.poisoned.load(Ordering::Acquire),\r\n                self.inner.timed_out.load(Ordering::Acquire)\r\n            ),\r\n        );\r\n        {\r\n            let mut d = recover(&self.inner.dog);\r\n…\n        }\n…\n    }\n}","startLine":219,"lineNumbers":[219,null,269,270,271,272,273,274,275,276,277,278,279,280,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,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,null,656,null,662,663]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\conn.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":815,"totalBytes":16133,"outputLines":385,"outputBytes":16133,"shownRange":{"start":269,"end":653},"nextOffset":654}}},"isError":false,"timestamp":1784247558524}}
{"type":"message","id":"fc7e5d57","parentId":"f3f43ecf","timestamp":"2026-07-17T00:19:18.532Z","message":{"role":"toolResult","toolCallId":"call_CfC01aRiHHSHcyw31WZWxh5o|fc_0ae44a63c9031803016a5975062a6c819bb4c1e9d592caa0c3","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## dispatch.rs\n 217|        DispatchOutcome::Served(_) | DispatchOutcome::Unknown => ClaimState::Terminal,\n*218|        DispatchOutcome::Failed(_) => {\n 219|            let attempts = attempts + 1;\n 220|            if attempts >= MAX_DISPATCH_ATTEMPTS {\n 221|                ClaimState::Terminal\n...\n 238|/// request and await the reply on the same stream (sync.rs `request_sync` and\n*239|/// propagate.rs `fetch_update` both `net_stream_send(.., finish=true)` on the\n 240|/// opener), so every pull that arrives while the brain is down reads\n 241|/// `finished=true` while genuinely PENDING — filtering it breaks context sync\n 242|/// across exactly the refresh window this milestone fixes.\n...\n 353|                } else {\n*354|                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n 355|                }\n 356|                let mut c = claims_done.lock().unwrap();\n 357|                c.insert(\n...\n 467|    if take_injected_transient() {\n*468|        return DispatchOutcome::Failed(\n 469|            \"injected transient worker-start failure (test valve)\".into(),\n 470|        );\n 471|    }\n...\n 474|        Ok(None) => return DispatchOutcome::Unknown,\n*475|        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 476|    };\n 477|    let family = classify_first_line(&line);\n 478|\n...\n 492|        Ok(b) => b,\n*493|        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 494|    };\n 495|\n 496|    match family {\n...\n 499|                Ok(cs) => cs,\n*500|                Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 501|            };\n 502|            let policy = SyncPolicy::load();\n 503|            match serve_sync(\n...\n 512|                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*513|                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 514|            }\n 515|        }\n 516|        StreamFamily::Update => {\n...\n 520|                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*521|                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 522|            }\n 523|        }\n 524|        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n...\n 588|                    Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*589|                    Err(e) => DispatchOutcome::Failed(e.to_string()),\n 590|                },\n 591|                None => {\n 592|                    // Refuse politely: finish our send side, claim stays.\n*593|                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n 594|                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n 595|                }\n 596|            }\n...\n 613|                        Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*614|                        Err(e) => DispatchOutcome::Failed(e.to_string()),\n 615|                    }\n 616|                }\n 617|                None => {\n*618|                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n 619|                    DispatchOutcome::Served(\"xfer refused: no endpoint named\".into())\n 620|                }\n 621|            }\n...\n 631|                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*632|                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 633|            }\n 634|        }\n 635|        StreamFamily::ShellLink => {\n...\n 642|                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*643|                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 644|            }\n 645|        }\n 646|        StreamFamily::ServeProbe => {\n...\n 652|                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*653|                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 654|            }\n 655|        }\n 656|        StreamFamily::Unknown => DispatchOutcome::Unknown,\n...\n 681|    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n*682|        return DispatchOutcome::Failed(e.to_string());\n 683|    }\n 684|    let mut decoder = RegistryDecoder::new();\n 685|    let mut merged = 0usize;\n...\n 733|            }\n*734|            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n 735|            Ok(_) => {}\n*736|            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 737|        }\n 738|    }\n 739|}\n...\n 755|        Ok(s) => s,\n*756|        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 757|    };\n 758|    let policy = NotifPolicy::load();\n 759|    // This serving node's identity for the presence resolution: the broker's\n## attach.rs\n 244|    if access_check(endpoint, origin_node, InboundClass::Unsolicited) == AccessDecision::Deny {\n*245|        wire.net_stream_send(stream_id, &[], None, true)?;\n 246|        return Ok(Some(AttachServeOutcome::Refused));\n 247|    }\n 248|    let sid = remote_session.unwrap_or(session_id);\n...\n 399|                        });\n*400|                        let _ = wire.net_stream_send(stream_id, &line, None, true);\n 401|                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n 402|                        return Err(e);\n 403|                    }\n...\n 517|                        // renders the busy guidance client-side from driven_by.\n*518|                        wire.net_stream_send(stream_id, &[], None, true)?;\n 519|                        return Ok(AttachServeOutcome::Refused);\n 520|                    }\n 521|                }\n...\n 525|                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\n*526|                wire.net_stream_send(stream_id, &line, None, false)?;\n 527|            }\n 528|            // This controller was kicked by a different operator's `--take`.\n 529|            BrokerEvent::Displaced { by, .. } if attached => {\n 530|                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\n*531|                wire.net_stream_send(stream_id, &line, None, true)?;\n 532|                // No dormant edge: the endpoint stays driven by the taker (whose\n 533|                // own serve loop already fired the wake).\n 534|                return Ok(AttachServeOutcome::Displaced);\n...\n 560|            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n*561|                wire.net_stream_send(stream_id, &[], None, true)?;\n 562|                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\n 563|                // info.json, carrying the current driven_by through), THEN release\n 564|                // the role over IPC so the broker's driven_by=None is the LAST\n...\n 585|                // the record, and the operator's render cursor is the dedup.\n*586|                wire.net_stream_send(stream_id, &line, None, false)?;\n 587|            }\n 588|            BrokerEvent::Exit { code, .. } if attached => {\n 589|                let line = ndjson::encode_line(&AttachRecord::Exit { code });\n*590|                wire.net_stream_send(stream_id, &line, None, true)?;\n 591|                if role == Some(ServeRole::Controller) {\n 592|                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n 593|                }\n...\n 652|    });\n*653|    brain.net_stream_send(opened.stream_id, &line, None, false)?;\n 654|    Ok(opened.stream_id)\n 655|}\n 656|\n...\n 669|    let line = ndjson::encode_line(&AttachRecord::Resize { rows, cols });\n*670|    brain.net_stream_send(stream_id, &line, None, false)?;\n 671|    Ok(())\n 672|}\n 673|\n...\n 688|    });\n*689|    brain.net_stream_send(stream_id, &line, None, false)?;\n 690|    Ok(())\n 691|}\n## brain.rs\n 1400|    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n*1401|    pub fn net_stream_send(\n 1402|        &mut self,\n 1403|        stream_id: u64,\n 1404|        bytes: &[u8],\n## broker.rs\n 3091|                KIND_NET_STREAM_SEND => {\n*3092|                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\n 3093|                        send_error(&send, &msg);\n 3094|                    }\n 3095|                }\n...\n 4261|    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n*4262|    fn dispatch_net_stream_send(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4263|        let req: NetStreamSendReq = serde_json::from_value(env.payload)\n 4264|            .map_err(|e| format!(\"bad net-stream-send payload: {e}\"))?;\n 4265|        let bytes =\n...\n 4269|            None => host\n*4270|                .send_stream(req.stream_id, &bytes, req.finish)\n 4271|                .map_err(|e| e.to_string()),\n 4272|            Some(op) => {\n 4273|                let minter = req.minter.unwrap_or(Minter::Legacy);\n...\n 4281|                    .apply_once(key, EffectKind::NetSend, || {\n*4282|                        host.send_stream(req.stream_id, &bytes, req.finish)\n 4283|                    })\n 4284|                    .map_err(|e| format!(\"net-stream-send failed: {e}\"))?;\n 4285|                let frame = Envelope::new("}],"details":{"scopePath":"crates/spt-daemon/src/dispatch.rs, crates/spt-daemon/src/attach.rs, crates/spt-daemon/src/brain.rs, crates/spt-daemon/src/broker.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":36,"fileCount":4,"files":["crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/dispatch.rs","count":20},{"path":"crates/spt-daemon/src/attach.rs","count":11},{"path":"crates/spt-daemon/src/brain.rs","count":1},{"path":"crates/spt-daemon/src/broker.rs","count":4}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## dispatch.rs\n 217│        DispatchOutcome::Served(_) | DispatchOutcome::Unknown => ClaimState::Terminal,\n*218│        DispatchOutcome::Failed(_) => {\n 219│            let attempts = attempts + 1;\n 220│            if attempts >= MAX_DISPATCH_ATTEMPTS {\n 221│                ClaimState::Terminal\n    │...\n 238│/// request and await the reply on the same stream (sync.rs `request_sync` and\n*239│/// propagate.rs `fetch_update` both `net_stream_send(.., finish=true)` on the\n 240│/// opener), so every pull that arrives while the brain is down reads\n 241│/// `finished=true` while genuinely PENDING — filtering it breaks context sync\n 242│/// across exactly the refresh window this milestone fixes.\n    │...\n 353│                } else {\n*354│                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n 355│                }\n 356│                let mut c = claims_done.lock().unwrap();\n 357│                c.insert(\n    │...\n 467│    if take_injected_transient() {\n*468│        return DispatchOutcome::Failed(\n 469│            \"injected transient worker-start failure (test valve)\".into(),\n 470│        );\n 471│    }\n    │...\n 474│        Ok(None) => return DispatchOutcome::Unknown,\n*475│        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 476│    };\n 477│    let family = classify_first_line(&line);\n 478│\n    │...\n 492│        Ok(b) => b,\n*493│        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 494│    };\n 495│\n 496│    match family {\n    │...\n 499│                Ok(cs) => cs,\n*500│                Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 501│            };\n 502│            let policy = SyncPolicy::load();\n 503│            match serve_sync(\n    │...\n 512│                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*513│                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 514│            }\n 515│        }\n 516│        StreamFamily::Update => {\n    │...\n 520│                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*521│                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 522│            }\n 523│        }\n 524│        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n    │...\n 588│                    Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*589│                    Err(e) => DispatchOutcome::Failed(e.to_string()),\n 590│                },\n 591│                None => {\n 592│                    // Refuse politely: finish our send side, claim stays.\n*593│                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n 594│                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n 595│                }\n 596│            }\n    │...\n 613│                        Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*614│                        Err(e) => DispatchOutcome::Failed(e.to_string()),\n 615│                    }\n 616│                }\n 617│                None => {\n*618│                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n 619│                    DispatchOutcome::Served(\"xfer refused: no endpoint named\".into())\n 620│                }\n 621│            }\n    │...\n 631│                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*632│                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 633│            }\n 634│        }\n 635│        StreamFamily::ShellLink => {\n    │...\n 642│                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*643│                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 644│            }\n 645│        }\n 646│        StreamFamily::ServeProbe => {\n    │...\n 652│                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n*653│                Err(e) => DispatchOutcome::Failed(e.to_string()),\n 654│            }\n 655│        }\n 656│        StreamFamily::Unknown => DispatchOutcome::Unknown,\n    │...\n 681│    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n*682│        return DispatchOutcome::Failed(e.to_string());\n 683│    }\n 684│    let mut decoder = RegistryDecoder::new();\n 685│    let mut merged = 0usize;\n    │...\n 733│            }\n*734│            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n 735│            Ok(_) => {}\n*736│            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 737│        }\n 738│    }\n 739│}\n    │...\n 755│        Ok(s) => s,\n*756│        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n 757│    };\n 758│    let policy = NotifPolicy::load();\n 759│    // This serving node's identity for the presence resolution: the broker's\n## attach.rs\n 244│    if access_check(endpoint, origin_node, InboundClass::Unsolicited) == AccessDecision::Deny {\n*245│        wire.net_stream_send(stream_id, &[], None, true)?;\n 246│        return Ok(Some(AttachServeOutcome::Refused));\n 247│    }\n 248│    let sid = remote_session.unwrap_or(session_id);\n    │...\n 399│                        });\n*400│                        let _ = wire.net_stream_send(stream_id, &line, None, true);\n 401│                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n 402│                        return Err(e);\n 403│                    }\n    │...\n 517│                        // renders the busy guidance client-side from driven_by.\n*518│                        wire.net_stream_send(stream_id, &[], None, true)?;\n 519│                        return Ok(AttachServeOutcome::Refused);\n 520│                    }\n 521│                }\n    │...\n 525│                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\n*526│                wire.net_stream_send(stream_id, &line, None, false)?;\n 527│            }\n 528│            // This controller was kicked by a different operator's `--take`.\n 529│            BrokerEvent::Displaced { by, .. } if attached => {\n 530│                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\n*531│                wire.net_stream_send(stream_id, &line, None, true)?;\n 532│                // No dormant edge: the endpoint stays driven by the taker (whose\n 533│                // own serve loop already fired the wake).\n 534│                return Ok(AttachServeOutcome::Displaced);\n    │...\n 560│            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n*561│                wire.net_stream_send(stream_id, &[], None, true)?;\n 562│                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\n 563│                // info.json, carrying the current driven_by through), THEN release\n 564│                // the role over IPC so the broker's driven_by=None is the LAST\n    │...\n 585│                // the record, and the operator's render cursor is the dedup.\n*586│                wire.net_stream_send(stream_id, &line, None, false)?;\n 587│            }\n 588│            BrokerEvent::Exit { code, .. } if attached => {\n 589│                let line = ndjson::encode_line(&AttachRecord::Exit { code });\n*590│                wire.net_stream_send(stream_id, &line, None, true)?;\n 591│                if role == Some(ServeRole::Controller) {\n 592│                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n 593│                }\n    │...\n 652│    });\n*653│    brain.net_stream_send(opened.stream_id, &line, None, false)?;\n 654│    Ok(opened.stream_id)\n 655│}\n 656│\n    │...\n 669│    let line = ndjson::encode_line(&AttachRecord::Resize { rows, cols });\n*670│    brain.net_stream_send(stream_id, &line, None, false)?;\n 671│    Ok(())\n 672│}\n 673│\n    │...\n 688│    });\n*689│    brain.net_stream_send(stream_id, &line, None, false)?;\n 690│    Ok(())\n 691│}\n## brain.rs\n 1400│    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n*1401│    pub fn net_stream_send(\n 1402│        &mut self,\n 1403│        stream_id: u64,\n 1404│        bytes: &[u8],\n## broker.rs\n 3091│                KIND_NET_STREAM_SEND => {\n*3092│                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\n 3093│                        send_error(&send, &msg);\n 3094│                    }\n 3095│                }\n     │...\n 4261│    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n*4262│    fn dispatch_net_stream_send(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4263│        let req: NetStreamSendReq = serde_json::from_value(env.payload)\n 4264│            .map_err(|e| format!(\"bad net-stream-send payload: {e}\"))?;\n 4265│        let bytes =\n     │...\n 4269│            None => host\n*4270│                .send_stream(req.stream_id, &bytes, req.finish)\n 4271│                .map_err(|e| e.to_string()),\n 4272│            Some(op) => {\n 4273│                let minter = req.minter.unwrap_or(Minter::Legacy);\n     │...\n 4281│                    .apply_once(key, EffectKind::NetSend, || {\n*4282│                        host.send_stream(req.stream_id, &bytes, req.finish)\n 4283│                    })\n 4284│                    .map_err(|e| format!(\"net-stream-send failed: {e}\"))?;\n 4285│                let frame = Envelope::new("},"isError":false,"timestamp":1784247558532}}
{"type":"message","id":"87148c3c","parentId":"fc7e5d57","timestamp":"2026-07-17T00:19:18.547Z","message":{"role":"toolResult","toolCallId":"call_zuk96he1mC0hE0aAuGcriVwq|fc_0ae44a63c9031803016a5975062a50819bad8e5c31493e5e97","toolName":"read","content":[{"type":"text","text":"    pub fn net_open_stream(\n…\n    ) -> io::Result<NetStreamOpened> {\n        self.send(\n…\n            serde_json::to_value(NetStreamOpenReq {\n                conn_id,\n                op_id: op.map(|o| o.seq),\n                minter: op.map(|o| o.minter),\n            })\n            .expect(\"NetStreamOpenReq serializes\"),\n        )?;\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::NetStreamOpened(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Write bytes to a held stream over the broker (D4b). With `op_id` the\n    /// write is exactly-once and this blocks for the `net-sent` ack (returned);\n    /// without one it is a direct fire-and-forget write (`None` returned).\n    /// `finish` closes the send side after the write.\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    pub fn net_stream_send(\n        &mut self,\n        stream_id: u64,\n        bytes: &[u8],\n        op: Option<MintedOp>,\n        finish: bool,\n    ) -> io::Result<Option<NetSent>> {\n        self.send(\n            KIND_NET_STREAM_SEND,\n            serde_json::to_value(NetStreamSendReq {\n                stream_id,\n                data_b64: encode_bytes(bytes),\n                op_id: op.map(|o| o.seq),\n                minter: op.map(|o| o.minter),\n                finish,\n            })\n            .expect(\"NetStreamSendReq serializes\"),\n        )?;\n        if op.is_none() {\n            return Ok(None);\n        }\n        let want_seq = op.map(|o| o.seq);\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::NetSent(ev) if Some(ev.op_id) == want_seq => return Ok(Some(ev)),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// List the streams the broker holds (D4b) — how a re-attaching brain\n    /// finds streams (incl. peer-initiated ones) that arrived while it was down.\n    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {\n        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::NetStreams(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A).\n    /// The dispatcher calls this when a serve loop runs to completion, so no\n    /// future dispatcher generation rediscovers and replays the stream\n    /// (KNOWN-HAZARDS 7.41). Best-effort at the caller: an older broker\n    /// answers `error` (KH-2.3 N-1 window — retirement then waits for the\n    /// broker to cycle onto the new binary).\n    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n    pub fn net_stream_retire(&mut self, stream_id: u64) -> io::Result<bool> {\n        self.send(\n            KIND_NET_STREAM_RETIRE,\n            serde_json::to_value(NetStreamRetireReq { stream_id })\n                .expect(\"NetStreamRetireReq serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_RETIRED => {\n                    let reply: NetStreamRetired = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(reply.removed);\n                }\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// A stream's pinned opener line (ADR-0038 B) — the restart-durable\n    /// classification fact, read instead of peeking ring seq 0 (which a\n    /// bounded ring evicts under traffic). `Ok(None)` = the broker holds the\n    /// stream but no opener is pinned (no newline yet / capture gave up).\n    /// `Err` = no such stream, or an older broker without the verb (KH-2.3 —\n    /// the dispatcher falls back to the ring peek).\n    // [impl->REQ-STREAM-OPENER-DURABLE]\n    pub fn net_stream_opener(&mut self, stream_id: u64) -> io::Result<Option<Vec<u8>>> {\n        self.send(\n            KIND_NET_STREAM_OPENER,\n            serde_json::to_value(NetStreamOpenerReq { stream_id })\n                .expect(\"NetStreamOpenerReq serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_OPENER_REPLY => {\n                    let reply: NetStreamOpenerReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n…\n                }\n…\n            }\n        }\n    }\n\n[Showing lines 1379-1488 of 2236. Use :1489 to continue]"}],"details":{"truncation":{"content":"            serde_json::to_value(NetStreamOpenReq {\r\n                conn_id,\r\n                op_id: op.map(|o| o.seq),\r\n                minter: op.map(|o| o.minter),\r\n            })\r\n            .expect(\"NetStreamOpenReq serializes\"),\r\n        )?;\r\n        let deadline = self.call_deadline();\r\n        loop {\r\n            match self.read_event_until(deadline)? {\r\n                BrokerEvent::NetStreamOpened(ev) => return Ok(ev),\r\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\r\n                _ => continue,\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Write bytes to a held stream over the broker (D4b). With `op_id` the\r\n    /// write is exactly-once and this blocks for the `net-sent` ack (returned);\r\n    /// without one it is a direct fire-and-forget write (`None` returned).\r\n    /// `finish` closes the send side after the write.\r\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\r\n    pub fn net_stream_send(\r\n        &mut self,\r\n        stream_id: u64,\r\n        bytes: &[u8],\r\n        op: Option<MintedOp>,\r\n        finish: bool,\r\n    ) -> io::Result<Option<NetSent>> {\r\n        self.send(\r\n            KIND_NET_STREAM_SEND,\r\n            serde_json::to_value(NetStreamSendReq {\r\n                stream_id,\r\n                data_b64: encode_bytes(bytes),\r\n                op_id: op.map(|o| o.seq),\r\n                minter: op.map(|o| o.minter),\r\n                finish,\r\n            })\r\n            .expect(\"NetStreamSendReq serializes\"),\r\n        )?;\r\n        if op.is_none() {\r\n            return Ok(None);\r\n        }\r\n        let want_seq = op.map(|o| o.seq);\r\n        let deadline = self.call_deadline();\r\n        loop {\r\n            match self.read_event_until(deadline)? {\r\n                BrokerEvent::NetSent(ev) if Some(ev.op_id) == want_seq => return Ok(Some(ev)),\r\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\r\n                _ => continue,\r\n            }\r\n        }\r\n    }\r\n\r\n    /// List the streams the broker holds (D4b) — how a re-attaching brain\r\n    /// finds streams (incl. peer-initiated ones) that arrived while it was down.\r\n    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {\r\n        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;\r\n        loop {\r\n            match self.read_event()? {\r\n                BrokerEvent::NetStreams(ev) => return Ok(ev),\r\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\r\n                _ => continue,\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A).\r\n    /// The dispatcher calls this when a serve loop runs to completion, so no\r\n    /// future dispatcher generation rediscovers and replays the stream\r\n    /// (KNOWN-HAZARDS 7.41). Best-effort at the caller: an older broker\r\n    /// answers `error` (KH-2.3 N-1 window — retirement then waits for the\r\n    /// broker to cycle onto the new binary).\r\n    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\r\n    pub fn net_stream_retire(&mut self, stream_id: u64) -> io::Result<bool> {\r\n        self.send(\r\n            KIND_NET_STREAM_RETIRE,\r\n            serde_json::to_value(NetStreamRetireReq { stream_id })\r\n                .expect(\"NetStreamRetireReq serializes\"),\r\n        )?;\r\n        loop {\r\n            match self.read_event()? {\r\n                BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_RETIRED => {\r\n                    let reply: NetStreamRetired = serde_json::from_value(env.payload)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    return Ok(reply.removed);\r\n                }\r\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\r\n                _ => continue,\r\n            }\r\n        }\r\n    }\r\n\r\n    /// A stream's pinned opener line (ADR-0038 B) — the restart-durable\r\n    /// classification fact, read instead of peeking ring seq 0 (which a\r\n    /// bounded ring evicts under traffic). `Ok(None)` = the broker holds the\r\n    /// stream but no opener is pinned (no newline yet / capture gave up).\r\n    /// `Err` = no such stream, or an older broker without the verb (KH-2.3 —\r\n    /// the dispatcher falls back to the ring peek).\r\n    // [impl->REQ-STREAM-OPENER-DURABLE]\r\n    pub fn net_stream_opener(&mut self, stream_id: u64) -> io::Result<Option<Vec<u8>>> {\r\n        self.send(\r\n            KIND_NET_STREAM_OPENER,\r\n            serde_json::to_value(NetStreamOpenerReq { stream_id })\r\n                .expect(\"NetStreamOpenerReq serializes\"),\r\n        )?;\r\n        loop {\r\n            match self.read_event()? {\r\n                BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_OPENER_REPLY => {\r\n                    let reply: NetStreamOpenerReply = serde_json::from_value(env.payload)\r","truncated":true,"truncatedBy":"lines","totalLines":858,"totalBytes":4850,"outputLines":110,"outputBytes":4850,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    pub fn net_open_stream(\n…\n    ) -> io::Result<NetStreamOpened> {\n        self.send(\n…\n            serde_json::to_value(NetStreamOpenReq {\r\n                conn_id,\r\n                op_id: op.map(|o| o.seq),\r\n                minter: op.map(|o| o.minter),\r\n            })\r\n            .expect(\"NetStreamOpenReq serializes\"),\r\n        )?;\r\n        let deadline = self.call_deadline();\r\n        loop {\r\n            match self.read_event_until(deadline)? {\r\n                BrokerEvent::NetStreamOpened(ev) => return Ok(ev),\r\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\r\n                _ => continue,\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Write bytes to a held stream over the broker (D4b). With `op_id` the\r\n    /// write is exactly-once and this blocks for the `net-sent` ack (returned);\r\n    /// without one it is a direct fire-and-forget write (`None` returned).\r\n    /// `finish` closes the send side after the write.\r\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\r\n    pub fn net_stream_send(\r\n        &mut self,\r\n        stream_id: u64,\r\n        bytes: &[u8],\r\n        op: Option<MintedOp>,\r\n        finish: bool,\r\n    ) -> io::Result<Option<NetSent>> {\r\n        self.send(\r\n            KIND_NET_STREAM_SEND,\r\n            serde_json::to_value(NetStreamSendReq {\r\n                stream_id,\r\n                data_b64: encode_bytes(bytes),\r\n                op_id: op.map(|o| o.seq),\r\n                minter: op.map(|o| o.minter),\r\n                finish,\r\n            })\r\n            .expect(\"NetStreamSendReq serializes\"),\r\n        )?;\r\n        if op.is_none() {\r\n            return Ok(None);\r\n        }\r\n        let want_seq = op.map(|o| o.seq);\r\n        let deadline = self.call_deadline();\r\n        loop {\r\n            match self.read_event_until(deadline)? {\r\n                BrokerEvent::NetSent(ev) if Some(ev.op_id) == want_seq => return Ok(Some(ev)),\r\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\r\n                _ => continue,\r\n            }\r\n        }\r\n    }\r\n\r\n    /// List the streams the broker holds (D4b) — how a re-attaching brain\r\n    /// finds streams (incl. peer-initiated ones) that arrived while it was down.\r\n    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {\r\n        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;\r\n        loop {\r\n            match self.read_event()? {\r\n                BrokerEvent::NetStreams(ev) => return Ok(ev),\r\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\r\n                _ => continue,\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A).\r\n    /// The dispatcher calls this when a serve loop runs to completion, so no\r\n    /// future dispatcher generation rediscovers and replays the stream\r\n    /// (KNOWN-HAZARDS 7.41). Best-effort at the caller: an older broker\r\n    /// answers `error` (KH-2.3 N-1 window — retirement then waits for the\r\n    /// broker to cycle onto the new binary).\r\n    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\r\n    pub fn net_stream_retire(&mut self, stream_id: u64) -> io::Result<bool> {\r\n        self.send(\r\n            KIND_NET_STREAM_RETIRE,\r\n            serde_json::to_value(NetStreamRetireReq { stream_id })\r\n                .expect(\"NetStreamRetireReq serializes\"),\r\n        )?;\r\n        loop {\r\n            match self.read_event()? {\r\n                BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_RETIRED => {\r\n                    let reply: NetStreamRetired = serde_json::from_value(env.payload)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                    return Ok(reply.removed);\r\n                }\r\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\r\n                _ => continue,\r\n            }\r\n        }\r\n    }\r\n\r\n    /// A stream's pinned opener line (ADR-0038 B) — the restart-durable\r\n    /// classification fact, read instead of peeking ring seq 0 (which a\r\n    /// bounded ring evicts under traffic). `Ok(None)` = the broker holds the\r\n    /// stream but no opener is pinned (no newline yet / capture gave up).\r\n    /// `Err` = no such stream, or an older broker without the verb (KH-2.3 —\r\n    /// the dispatcher falls back to the ring peek).\r\n    // [impl->REQ-STREAM-OPENER-DURABLE]\r\n    pub fn net_stream_opener(&mut self, stream_id: u64) -> io::Result<Option<Vec<u8>>> {\r\n        self.send(\r\n            KIND_NET_STREAM_OPENER,\r\n            serde_json::to_value(NetStreamOpenerReq { stream_id })\r\n                .expect(\"NetStreamOpenerReq serializes\"),\r\n        )?;\r\n        loop {\r\n            match self.read_event()? {\r\n                BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_OPENER_REPLY => {\r\n                    let reply: NetStreamOpenerReply = serde_json::from_value(env.payload)\r\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n…\n                }\n…\n            }\n        }\n    }","startLine":1372,"lineNumbers":[1372,null,1376,1377,null,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,null,1496,null,1499,1500,1501]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2236,"totalBytes":4850,"outputLines":110,"outputBytes":4850,"shownRange":{"start":1379,"end":1488},"nextOffset":1489}}},"isError":false,"timestamp":1784247558547}}
{"type":"message","id":"c1fcf099","parentId":"87148c3c","timestamp":"2026-07-17T00:19:18.601Z","message":{"role":"toolResult","toolCallId":"call_cfD5rLn4ZBi6WGJrl2SQPYM6|fc_0ae44a63c9031803016a5975062a64819bba8942b993355ad2","toolName":"read","content":[{"type":"text","text":"    /// from abandoned earlier attempts are swept here (no separate reaper — pairing\n    /// is rare and the map is tiny).\n    // [impl->REQ-JOIN-TWO-PHASE]\n    fn dispatch_pair_meet(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: PairMeetReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad pair-meet payload: {e}\"))?;\n        let host = self\n            .net\n            .get()\n            .ok_or(\"net disabled: this broker was bound without a network host\")?;\n        match host.pair_meet(&req.subnet, req.verbose) {\n            Ok(addr) => {\n                // The canonical lowercase-hex name (== node_id_hex everywhere),\n                // derived from the met address's proven id.\n                let member_hex = spt_proto::identity::PublicKey::from_bytes(addr.id.as_bytes())\n                    .map(|p| p.to_hex())\n                    .unwrap_or_default();\n                let session_id = self.next_id.fetch_add(1, Ordering::Relaxed);\n                {\n                    let mut holds = recover(&self.pair_holds);\n                    let now = Instant::now();\n                    holds.retain(|_, h| h.deadline > now); // sweep abandoned holds\n                    holds.insert(\n                        session_id,\n                        PairHold {\n                            subnet: req.subnet.clone(),\n                            addr,\n                            deadline: now + PAIR_CODE_WAIT,\n                        },\n                    );\n                }\n                let frame = Envelope::new(\n                    KIND_MET_MEMBER,\n                    serde_json::to_value(MetMember {\n                        session_id,\n                        member_hex,\n                    })\n                    .expect(\"MetMember serializes\"),\n                );\n                send_frame(send, &frame);\n            }\n            // A meet failure reuses the classified pair-joined reply shape (ok:\n            // false) — the CLI renders it before ever prompting for a code (the\n            // \"dead subnet must not make the user fetch a code\" path).\n            Err(fail) => send_frame(send, &pair_fail_frame(&fail)),\n        }\n        Ok(())\n    }\n\n    /// Phase 2 of the two-phase join (REQ-JOIN-TWO-PHASE): submit the code for a\n    /// held meet session. Dials the HELD address (no re-search) and runs the\n    /// SPAKE2 ceremony. A wrong-code retry re-submits against the same session id\n    /// (the hold is KEPT — ceremony only); success or any other terminal verdict\n    /// (and an expired/unknown session) drops the hold.\n    // [impl->REQ-JOIN-TWO-PHASE]\n    fn dispatch_pair_code_submit(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: PairCodeSubmit = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad pair-code-submit payload: {e}\"))?;\n        // Read the held address FIRST, enforcing the wait-for-code timeout — a\n        // missing or expired hold is a clean failure that needs no net (the CLI\n        // re-runs the meet).\n        let held = {\n            let mut holds = recover(&self.pair_holds);\n            match holds.get(&req.session_id) {\n                Some(h) if hold_is_live(h.deadline, Instant::now()) => {\n                    Some((h.subnet.clone(), h.addr.clone()))\n                }\n                Some(_) => {\n                    holds.remove(&req.session_id);\n                    None\n                }\n                None => None,\n            }\n        };\n        let Some((subnet, addr)) = held else {\n            let reply = PairJoinReply {\n                ok: false,\n                class: Some(\"other\".to_string()),\n                retry_after_secs: None,\n                detail: \"the meet expired (the 5-minute wait-for-code window lapsed) \\\n                         — re-run `spt subnet join`\"\n                    .to_string(),\n            };\n            send_frame(\n                send,\n                &Envelope::new(\n                    KIND_PAIR_JOINED,\n                    serde_json::to_value(reply).expect(\"PairJoinReply serializes\"),\n                ),\n            );\n            return Ok(());\n        };\n        let host = self\n            .net\n            .get()\n            .ok_or(\"net disabled: this broker was bound without a network host\")?;\n        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {\n            Ok(outcome) => {\n                recover(&self.pair_holds).remove(&req.session_id);\n                Envelope::new(\n                    KIND_PAIR_JOINED,\n                    serde_json::to_value(PairJoinReply {\n                        ok: true,\n                        class: None,\n                        retry_after_secs: None,\n                        detail: outcome,\n                    })\n                    .expect(\"PairJoinReply serializes\"),\n                )\n            }\n            Err(fail) => {\n                // Keep the hold ONLY for a wrong-code retry (re-runs the ceremony\n                // against the same held address); every other verdict is terminal.\n                if !fail_keeps_hold(&fail) {\n                    recover(&self.pair_holds).remove(&req.session_id);\n                }\n                pair_fail_frame(&fail)\n            }\n        };\n        send_frame(send, &frame);\n        Ok(())\n    }\n\n    /// Open a bidi stream on a held connection (D4b). Journaled exactly-once\n    /// under the same discipline as [`Self::dispatch_net_dial`]: the minted\n    /// `stream_id` is recorded inside the `apply_once` closure, and a deduped\n    /// replay is answered with the stream the first apply opened.\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: NetStreamOpenReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad net-stream-open payload: {e}\"))?;\n…\n    }\n\n[Showing lines 4079-4208 of 6063. Use :4209 to continue]"}],"details":{"truncation":{"content":"    /// from abandoned earlier attempts are swept here (no separate reaper — pairing\r\n    /// is rare and the map is tiny).\r\n    // [impl->REQ-JOIN-TWO-PHASE]\r\n    fn dispatch_pair_meet(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: PairMeetReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad pair-meet payload: {e}\"))?;\r\n        let host = self\r\n            .net\r\n            .get()\r\n            .ok_or(\"net disabled: this broker was bound without a network host\")?;\r\n        match host.pair_meet(&req.subnet, req.verbose) {\r\n            Ok(addr) => {\r\n                // The canonical lowercase-hex name (== node_id_hex everywhere),\r\n                // derived from the met address's proven id.\r\n                let member_hex = spt_proto::identity::PublicKey::from_bytes(addr.id.as_bytes())\r\n                    .map(|p| p.to_hex())\r\n                    .unwrap_or_default();\r\n                let session_id = self.next_id.fetch_add(1, Ordering::Relaxed);\r\n                {\r\n                    let mut holds = recover(&self.pair_holds);\r\n                    let now = Instant::now();\r\n                    holds.retain(|_, h| h.deadline > now); // sweep abandoned holds\r\n                    holds.insert(\r\n                        session_id,\r\n                        PairHold {\r\n                            subnet: req.subnet.clone(),\r\n                            addr,\r\n                            deadline: now + PAIR_CODE_WAIT,\r\n                        },\r\n                    );\r\n                }\r\n                let frame = Envelope::new(\r\n                    KIND_MET_MEMBER,\r\n                    serde_json::to_value(MetMember {\r\n                        session_id,\r\n                        member_hex,\r\n                    })\r\n                    .expect(\"MetMember serializes\"),\r\n                );\r\n                send_frame(send, &frame);\r\n            }\r\n            // A meet failure reuses the classified pair-joined reply shape (ok:\r\n            // false) — the CLI renders it before ever prompting for a code (the\r\n            // \"dead subnet must not make the user fetch a code\" path).\r\n            Err(fail) => send_frame(send, &pair_fail_frame(&fail)),\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Phase 2 of the two-phase join (REQ-JOIN-TWO-PHASE): submit the code for a\r\n    /// held meet session. Dials the HELD address (no re-search) and runs the\r\n    /// SPAKE2 ceremony. A wrong-code retry re-submits against the same session id\r\n    /// (the hold is KEPT — ceremony only); success or any other terminal verdict\r\n    /// (and an expired/unknown session) drops the hold.\r\n    // [impl->REQ-JOIN-TWO-PHASE]\r\n    fn dispatch_pair_code_submit(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: PairCodeSubmit = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad pair-code-submit payload: {e}\"))?;\r\n        // Read the held address FIRST, enforcing the wait-for-code timeout — a\r\n        // missing or expired hold is a clean failure that needs no net (the CLI\r\n        // re-runs the meet).\r\n        let held = {\r\n            let mut holds = recover(&self.pair_holds);\r\n            match holds.get(&req.session_id) {\r\n                Some(h) if hold_is_live(h.deadline, Instant::now()) => {\r\n                    Some((h.subnet.clone(), h.addr.clone()))\r\n                }\r\n                Some(_) => {\r\n                    holds.remove(&req.session_id);\r\n                    None\r\n                }\r\n                None => None,\r\n            }\r\n        };\r\n        let Some((subnet, addr)) = held else {\r\n            let reply = PairJoinReply {\r\n                ok: false,\r\n                class: Some(\"other\".to_string()),\r\n                retry_after_secs: None,\r\n                detail: \"the meet expired (the 5-minute wait-for-code window lapsed) \\\r\n                         — re-run `spt subnet join`\"\r\n                    .to_string(),\r\n            };\r\n            send_frame(\r\n                send,\r\n                &Envelope::new(\r\n                    KIND_PAIR_JOINED,\r\n                    serde_json::to_value(reply).expect(\"PairJoinReply serializes\"),\r\n                ),\r\n            );\r\n            return Ok(());\r\n        };\r\n        let host = self\r\n            .net\r\n            .get()\r\n            .ok_or(\"net disabled: this broker was bound without a network host\")?;\r\n        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {\r\n            Ok(outcome) => {\r\n                recover(&self.pair_holds).remove(&req.session_id);\r\n                Envelope::new(\r\n                    KIND_PAIR_JOINED,\r\n                    serde_json::to_value(PairJoinReply {\r\n                        ok: true,\r\n                        class: None,\r\n                        retry_after_secs: None,\r\n                        detail: outcome,\r\n                    })\r\n                    .expect(\"PairJoinReply serializes\"),\r\n                )\r\n            }\r\n            Err(fail) => {\r\n                // Keep the hold ONLY for a wrong-code retry (re-runs the ceremony\r\n                // against the same held address); every other verdict is terminal.\r\n                if !fail_keeps_hold(&fail) {\r\n                    recover(&self.pair_holds).remove(&req.session_id);\r\n                }\r\n                pair_fail_frame(&fail)\r\n            }\r\n        };\r\n        send_frame(send, &frame);\r\n        Ok(())\r\n    }\r\n\r\n    /// Open a bidi stream on a held connection (D4b). Journaled exactly-once\r\n    /// under the same discipline as [`Self::dispatch_net_dial`]: the minted\r\n    /// `stream_id` is recorded inside the `apply_once` closure, and a deduped\r\n    /// replay is answered with the stream the first apply opened.\r\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\r\n    fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: NetStreamOpenReq = serde_json::from_value(env.payload)\r","truncated":true,"truncatedBy":"lines","totalLines":1985,"totalBytes":6011,"outputLines":130,"outputBytes":6011,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    /// from abandoned earlier attempts are swept here (no separate reaper — pairing\r\n    /// is rare and the map is tiny).\r\n    // [impl->REQ-JOIN-TWO-PHASE]\r\n    fn dispatch_pair_meet(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: PairMeetReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad pair-meet payload: {e}\"))?;\r\n        let host = self\r\n            .net\r\n            .get()\r\n            .ok_or(\"net disabled: this broker was bound without a network host\")?;\r\n        match host.pair_meet(&req.subnet, req.verbose) {\r\n            Ok(addr) => {\r\n                // The canonical lowercase-hex name (== node_id_hex everywhere),\r\n                // derived from the met address's proven id.\r\n                let member_hex = spt_proto::identity::PublicKey::from_bytes(addr.id.as_bytes())\r\n                    .map(|p| p.to_hex())\r\n                    .unwrap_or_default();\r\n                let session_id = self.next_id.fetch_add(1, Ordering::Relaxed);\r\n                {\r\n                    let mut holds = recover(&self.pair_holds);\r\n                    let now = Instant::now();\r\n                    holds.retain(|_, h| h.deadline > now); // sweep abandoned holds\r\n                    holds.insert(\r\n                        session_id,\r\n                        PairHold {\r\n                            subnet: req.subnet.clone(),\r\n                            addr,\r\n                            deadline: now + PAIR_CODE_WAIT,\r\n                        },\r\n                    );\r\n                }\r\n                let frame = Envelope::new(\r\n                    KIND_MET_MEMBER,\r\n                    serde_json::to_value(MetMember {\r\n                        session_id,\r\n                        member_hex,\r\n                    })\r\n                    .expect(\"MetMember serializes\"),\r\n                );\r\n                send_frame(send, &frame);\r\n            }\r\n            // A meet failure reuses the classified pair-joined reply shape (ok:\r\n            // false) — the CLI renders it before ever prompting for a code (the\r\n            // \"dead subnet must not make the user fetch a code\" path).\r\n            Err(fail) => send_frame(send, &pair_fail_frame(&fail)),\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Phase 2 of the two-phase join (REQ-JOIN-TWO-PHASE): submit the code for a\r\n    /// held meet session. Dials the HELD address (no re-search) and runs the\r\n    /// SPAKE2 ceremony. A wrong-code retry re-submits against the same session id\r\n    /// (the hold is KEPT — ceremony only); success or any other terminal verdict\r\n    /// (and an expired/unknown session) drops the hold.\r\n    // [impl->REQ-JOIN-TWO-PHASE]\r\n    fn dispatch_pair_code_submit(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: PairCodeSubmit = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad pair-code-submit payload: {e}\"))?;\r\n        // Read the held address FIRST, enforcing the wait-for-code timeout — a\r\n        // missing or expired hold is a clean failure that needs no net (the CLI\r\n        // re-runs the meet).\r\n        let held = {\r\n            let mut holds = recover(&self.pair_holds);\r\n            match holds.get(&req.session_id) {\r\n                Some(h) if hold_is_live(h.deadline, Instant::now()) => {\r\n                    Some((h.subnet.clone(), h.addr.clone()))\r\n                }\r\n                Some(_) => {\r\n                    holds.remove(&req.session_id);\r\n                    None\r\n                }\r\n                None => None,\r\n            }\r\n        };\r\n        let Some((subnet, addr)) = held else {\r\n            let reply = PairJoinReply {\r\n                ok: false,\r\n                class: Some(\"other\".to_string()),\r\n                retry_after_secs: None,\r\n                detail: \"the meet expired (the 5-minute wait-for-code window lapsed) \\\r\n                         — re-run `spt subnet join`\"\r\n                    .to_string(),\r\n            };\r\n            send_frame(\r\n                send,\r\n                &Envelope::new(\r\n                    KIND_PAIR_JOINED,\r\n                    serde_json::to_value(reply).expect(\"PairJoinReply serializes\"),\r\n                ),\r\n            );\r\n            return Ok(());\r\n        };\r\n        let host = self\r\n            .net\r\n            .get()\r\n            .ok_or(\"net disabled: this broker was bound without a network host\")?;\r\n        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {\r\n            Ok(outcome) => {\r\n                recover(&self.pair_holds).remove(&req.session_id);\r\n                Envelope::new(\r\n                    KIND_PAIR_JOINED,\r\n                    serde_json::to_value(PairJoinReply {\r\n                        ok: true,\r\n                        class: None,\r\n                        retry_after_secs: None,\r\n                        detail: outcome,\r\n                    })\r\n                    .expect(\"PairJoinReply serializes\"),\r\n                )\r\n            }\r\n            Err(fail) => {\r\n                // Keep the hold ONLY for a wrong-code retry (re-runs the ceremony\r\n                // against the same held address); every other verdict is terminal.\r\n                if !fail_keeps_hold(&fail) {\r\n                    recover(&self.pair_holds).remove(&req.session_id);\r\n                }\r\n                pair_fail_frame(&fail)\r\n            }\r\n        };\r\n        send_frame(send, &frame);\r\n        Ok(())\r\n    }\r\n\r\n    /// Open a bidi stream on a held connection (D4b). Journaled exactly-once\r\n    /// under the same discipline as [`Self::dispatch_net_dial`]: the minted\r\n    /// `stream_id` is recorded inside the `apply_once` closure, and a deduped\r\n    /// replay is answered with the stream the first apply opened.\r\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\r\n    fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: NetStreamOpenReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad net-stream-open payload: {e}\"))?;\n…\n    }","startLine":4079,"lineNumbers":[4079,4080,4081,4082,4083,4084,4085,4086,4087,4088,4089,4090,4091,4092,4093,4094,4095,4096,4097,4098,4099,4100,4101,4102,4103,4104,4105,4106,4107,4108,4109,4110,4111,4112,4113,4114,4115,4116,4117,4118,4119,4120,4121,4122,4123,4124,4125,4126,4127,4128,4129,4130,4131,4132,4133,4134,4135,4136,4137,4138,4139,4140,4141,4142,4143,4144,4145,4146,4147,4148,4149,4150,4151,4152,4153,4154,4155,4156,4157,4158,4159,4160,4161,4162,4163,4164,4165,4166,4167,4168,4169,4170,4171,4172,4173,4174,4175,4176,4177,4178,4179,4180,4181,4182,4183,4184,4185,4186,4187,4188,4189,4190,4191,4192,4193,4194,4195,4196,4197,4198,4199,4200,4201,4202,4203,4204,4205,4206,4207,4208,4209,null,4254]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6063,"totalBytes":6011,"outputLines":130,"outputBytes":6011,"shownRange":{"start":4079,"end":4208},"nextOffset":4209}}},"isError":false,"timestamp":1784247558601}}
{"type":"message","id":"3e0051f9","parentId":"c1fcf099","timestamp":"2026-07-17T00:19:27.340Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Assessing current dispatch fixes and retries**"},{"type":"toolCall","id":"call_aE9j1HTQFXuPu4cFQ5DgpxGL|fc_0ae44a63c9031803016a59750efe6c819b89032669d76385da","name":"read","arguments":{"i":"Reading current dispatcher retry lifecycle","path":"crates/spt-daemon/src/dispatch.rs","selector":"180-390,430-610"}},{"type":"toolCall","id":"call_4aSEXyfyWQyoUgiyQ7fs3dZO|fc_0ae44a63c9031803016a59750efe78819ba77ed6d4b1005a27","name":"grep","arguments":{"i":"Tracing stream retirement implementation","pattern":"KIND_NET_STREAM_RETIRE|dispatch_net_stream_retire|retire_stream|net_stream_retire|opener","path":"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/dispatch.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_7VdmxLsvPTM92HHSSfDbzDfR|fc_0ae44a63c9031803016a59750efe80819bb870e0d164884b9b","name":"read","arguments":{"i":"Reading connection drop tail","path":"crates/spt-daemon/src/conn.rs","selector":"630-690"}},{"type":"toolCall","id":"call_BqWB0EPNkZJgoN4iHXh0qDmW|fc_0ae44a63c9031803016a59750efe88819bbe8b1632af0fdbd3","name":"read","arguments":{"i":"Reading broker stream send exact","path":"crates/spt-daemon/src/broker.rs","selector":"4250-4315"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":12969,"output":354,"cacheRead":200192,"cacheWrite":0,"totalTokens":213515,"reasoningTokens":122,"cost":{"input":0.064845,"output":0.010620000000000001,"cacheRead":0.10009599999999999,"cacheWrite":0,"total":0.175561}},"stopReason":"toolUse","timestamp":1784247558665,"responseId":"resp_0ae44a63c9031803016a597506e368819baf655d0a51a229bf","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ae44a63c9031803016a5975085ed4819b9fe5c03f1768e47a","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXUOYZXsqHYNVVfGLwBL1q44r1LxEd0lp609nGHfVd-Hi7oIcIUezuTQab9ahDEeQX_6--cDKTTmbDGIvEzolsf-n6mH1IX7FHxrdnb_AphTSSLNw0z8H-E3QQ3UXkqeqGIizc5-67OdqWSxU_silv6vjtpJPTyRjoduNmTyGXlgJt2CYBcK3239h-5R6I2E3E6lsYGbLAt-cW08UvNryQZlJgkU_bmtP_fCF-cgwMx0i8qTRvdMRgFCoN0dllDU-JPYGap2PJS7dItQaOUJhs_R4FlXvzJxIoh6MmdaoGA6Vdejp6hXljnixoKMPGPU89maTawc9-R51_SxnNSqIqFXkYm2iprny3geN6wlHNuTLpNyFToDz_5R7uscnjxa_a6b-ACcLyCYXteRxsatqvZ3WLxfZq_8VxqxwWr6eIrlNefbYYlzkRGeMewKNmMjIB5f_L9ZNHXfrJMBBMJAtFy7IHtbjvfGXuHknYaVN6b7Y4XRMrz4lTeooWCAwrI6X4cGjKpmwdy9c_FbNL0eIfPJ8rsCCDAWSS8SrOBRUfOuQfg6DTarM36UmeWvvGvrApi9UVYReQCxd9R80ca2RGj96eID2Wo246srMb9ZSn9kVIXTFRWevRofMizy8g4nQ7OblvjqCcpyUVnY3UqLh5hqUY1_78kN2xSsRDD0WlLNIGW268i7rDFgWbEavpC7s_71a2-z4L_f-0LaHt6yP-8ETL72a704SkXc7k3Ufx7p8ErhD5fHgzyoGueri4gw-chgAJFY1YcuxZB4HM1LiBsg-Fo7F5t119L0Ul-kk8XipNPP19qIUrI48_VpaPzHpVRQ12ecAQkZOc9Vow2voiEkfyc-g_QFngAqWU9a9tX4PQylVwC-oFX9WCAPleGyBS8VE8rDYx6l9XIQ9bSgGS5jYT2oRZTJODsobfAXHDKFYwUvMgBYj-984vE0SRLBAkeO9DQu-xr9FIWDL5rED0ozQHlqUxjVVgFIVKVUciFMEJHQXUlqkxjWnaIsjI_-Tfz9N6gXD-V8QZnxgv6LRozLR3oqASBZJnExLRxUfAilmuWavxjVa5g62rAfGBWA1aiQ6NwLXk5C7Wd7xu0euDTKi1TAlRE5EaQQ7wEjst75Bh9dplhoRpBvOokGgl4SaVXfsakujvwlBYKJMO8Flwmyu_cgph4pQL_NDdw7IgQsjM1-MWJ-IDM3wo8UnvqGzkZBU0XnGfxNt7JXfGb_n_-EQ1-Ahb2CO30TLHxIuwrhpGvhVdgDM9UDCkeMmS2oUV9Ujb52saZbEWTBmSC3B0Dm8iNs1wb9_E-ewo10RdJBac23mZEL7qQzFh6fDcnqEj0-7g9laneKnAQwJ0ZTKJg8sy2weUTZP_GPTb0UNg5vTrNUZpogFEYnLp-ZdUIk1wO9IFgcZao35tsZRruZvny8V7khFUEmIOCbi68kEnC7Sn651WakM7q9jISTCRVwrdvgiWabOSUu0liiHKZeZA-o1dKK1C6xM_PQjaGAIRECDxVxWMiE5pgMXxDskCObUYWguzB1lZWrl1UGPdQJYqf9PQ0-ShGdWFQ2b4QwvdBg0Ms0Ag6Ny8-hdcCzYOPnmyIg_OvVLOl7VWYmG5X4dC6Oz_VdJ1oTibK4mbexpziKld0u1rmKNFrAlz9jVcw3BSxtlmlpZZXz25ZPzbw1FgOT9FN2npAXj2G33ITYFv22yRGmc3e1Us-yJOaniVmxiRvZ","summary":[{"type":"summary_text","text":"**Assessing current dispatch fixes and retries**"}]},{"id":"fc_0ae44a63c9031803016a59750efe6c819b89032669d76385da","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading current dispatcher retry lifecycle\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"180-390,430-610\"}","call_id":"call_aE9j1HTQFXuPu4cFQ5DgpxGL","name":"read"},{"id":"fc_0ae44a63c9031803016a59750efe78819ba77ed6d4b1005a27","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing stream retirement implementation\",\"pattern\":\"KIND_NET_STREAM_RETIRE|dispatch_net_stream_retire|retire_stream|net_stream_retire|opener\",\"path\":\"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/dispatch.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_4aSEXyfyWQyoUgiyQ7fs3dZO","name":"grep"},{"id":"fc_0ae44a63c9031803016a59750efe80819bb870e0d164884b9b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading connection drop tail\",\"path\":\"crates/spt-daemon/src/conn.rs\",\"selector\":\"630-690\"}","call_id":"call_7VdmxLsvPTM92HHSSfDbzDfR","name":"read"},{"id":"fc_0ae44a63c9031803016a59750efe88819bbe8b1632af0fdbd3","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker stream send exact\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"4250-4315\"}","call_id":"call_BqWB0EPNkZJgoN4iHXh0qDmW","name":"read"}]},"duration":8659.508299998939,"ttft":1782.253700003028,"contextSnapshot":{"promptTokens":213161,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_aE9j1HTQFXuPu4cFQ5DgpxGL|fc_0ae44a63c9031803016a59750efe6c819b89032669d76385da","toolName":"read","startedAt":"2026-07-17T00:19:27.340Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading current dispatcher retry lifecycle"},"id":"4438a72d","parentId":"3e0051f9","timestamp":"2026-07-17T00:19:27.340Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4aSEXyfyWQyoUgiyQ7fs3dZO|fc_0ae44a63c9031803016a59750efe78819ba77ed6d4b1005a27","toolName":"grep","startedAt":"2026-07-17T00:19:27.340Z","args":{"path":"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/dispatch.rs"},"intent":"Tracing stream retirement implementation"},"id":"e2017999","parentId":"4438a72d","timestamp":"2026-07-17T00:19:27.340Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_7VdmxLsvPTM92HHSSfDbzDfR|fc_0ae44a63c9031803016a59750efe80819bb870e0d164884b9b","toolName":"read","startedAt":"2026-07-17T00:19:27.340Z","args":{"path":"crates/spt-daemon/src/conn.rs"},"intent":"Reading connection drop tail"},"id":"068f494e","parentId":"e2017999","timestamp":"2026-07-17T00:19:27.340Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BqWB0EPNkZJgoN4iHXh0qDmW|fc_0ae44a63c9031803016a59750efe88819bbe8b1632af0fdbd3","toolName":"read","startedAt":"2026-07-17T00:19:27.340Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker stream send exact"},"id":"d5b08785","parentId":"068f494e","timestamp":"2026-07-17T00:19:27.340Z"}
{"type":"message","id":"7c3fe1c3","parentId":"d5b08785","timestamp":"2026-07-17T00:19:27.352Z","message":{"role":"toolResult","toolCallId":"call_aE9j1HTQFXuPu4cFQ5DgpxGL|fc_0ae44a63c9031803016a59750efe6c819b89032669d76385da","toolName":"read","content":[{"type":"text","text":"enum ClaimState {\n…\n    /// retries exhausted. Terminal outcomes never re-enter the queue.\n    Terminal,\n    /// A transient failure released the claim; eligible again at `next_at`.\n    Retry { attempts: u32, next_at: Instant },\n}\n\n/// Bounded retry budget per stream per dispatcher lifetime — transient setup\n/// failures recover, a persistently-failing stream goes terminal loud.\nconst MAX_DISPATCH_ATTEMPTS: u32 = 3;\n\n/// Backoff base for a released claim (doubles per failed attempt).\nconst DISPATCH_RETRY_BASE: Duration = Duration::from_millis(500);\n\n/// The backoff before retry number `attempts` (1-based) runs.\nfn retry_backoff(attempts: u32) -> Duration {\n    DISPATCH_RETRY_BASE * 2u32.saturating_pow(attempts.saturating_sub(1))\n}\n\n/// PURE claim decision: may this row be (re)claimed now? `Some(prior\n/// failures)` = claim it; `None` = leave it (in flight, terminal, or backing\n/// off). The unit-tested seam for the no-hot-loop ruling.\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\nfn should_claim(state: Option<&ClaimState>, now: Instant) -> Option<u32> {\n    match state {\n        None => Some(0),\n        Some(ClaimState::Retry { attempts, next_at }) if now >= *next_at => Some(*attempts),\n        _ => None,\n    }\n}\n\n/// PURE outcome transition: what a finished worker's claim becomes. Served\n/// and Unknown (unclassifiable) are TERMINAL — a terminal classification never\n/// hot-loops. A failure requeues with backoff until the bounded budget is\n/// spent, then goes terminal.\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\nfn outcome_transition(outcome: &DispatchOutcome, attempts: u32, now: Instant) -> ClaimState {\n    match outcome {\n        DispatchOutcome::Served(_) | DispatchOutcome::Unknown => ClaimState::Terminal,\n        DispatchOutcome::Failed(_) => {\n            let attempts = attempts + 1;\n            if attempts >= MAX_DISPATCH_ATTEMPTS {\n                ClaimState::Terminal\n            } else {\n                ClaimState::Retry {\n                    attempts,\n                    next_at: now + retry_backoff(attempts),\n                }\n            }\n        }\n    }\n}\n\n/// PURE lifecycle+family discriminator (gate round 1, ADR-0038 Decision 1's\n/// \"excluded by lifecycle state\" half): is a `finished` row TERMINAL for\n/// redispatch — retire unserved, never replay?\n///\n/// `finished` alone is NOT the answer, and a bare `s.finished` claim filter\n/// would be a regression: the request/reply families FIN their send WITH the\n/// request and await the reply on the same stream (sync.rs `request_sync` and\n/// propagate.rs `fetch_update` both `net_stream_send(.., finish=true)` on the\n/// opener), so every pull that arrives while the brain is down reads\n/// `finished=true` while genuinely PENDING — filtering it breaks context sync\n/// across exactly the refresh window this milestone fixes.\n///\n/// The steal class is ATTACH-specific: `finished` on an attach stream is the\n/// operator's clean detach — its exchange is definitionally over, and a\n/// replay takes a live controller (KNOWN-HAZARDS 7.41: the pre-existing\n/// backlog on long-lived conns, and the brain-down-detach window that\n/// Served-report retirement can never see). So: finished + Attach = terminal\n/// (retired unserved via the worker's Served report); finished + anything\n/// else = a pending request — serve it.\n// [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n// [impl->REQ-HAZARD-REDISPATCH-CONTROL-STEAL]\nfn finished_row_is_terminal(family: StreamFamily, finished: bool) -> bool {\n    finished && family == StreamFamily::Attach\n}\n\n/// TEST-RIG-ONLY (the D3 regression): `SPT_DISPATCH_INJECT_TRANSIENT_ONCE`\n/// set at dispatcher start makes exactly ONE worker fail before setup —\n/// proving a transient worker-start failure requeues and recovers through\n/// the REAL claim path. Never set in production; the latch arms once per\n/// process and disarms on first take.\nfn take_injected_transient() -> bool {\n    static ARMED: OnceLock<AtomicBool> = OnceLock::new();\n    let armed = ARMED.get_or_init(|| {\n        AtomicBool::new(std::env::var(\"SPT_DISPATCH_INJECT_TRANSIENT_ONCE\").is_ok())\n    });\n    armed.swap(false, Ordering::SeqCst)\n}\n\n/// The SPT_HOME-anchored paths the serve arms need. One value, cloned per\n/// worker (policies/stores re-load from the canonical paths per request —\n/// the access-gate stance, config edits take effect without a restart).\n#[derive(Debug, Clone)]\npub struct DispatchPaths {\n    /// The perch tree (WAN message funnel target).\n    pub owlery: PathBuf,\n    /// The staged-release cache dir ([`ReleaseCache::open`]).\n    pub releases: PathBuf,\n    /// Transient bundle/artifact scratch.\n    pub scratch: PathBuf,\n}\n\nimpl DispatchPaths {\n    /// The production layout under [`perch::spt_home`].\n    pub fn production() -> DispatchPaths {\n        let home = perch::spt_home();\n        DispatchPaths {\n            owlery: perch::owlery_dir(),\n            releases: home.join(\"releases\"),\n            scratch: home.join(\"scratch\").join(\"net\"),\n        }\n    }\n}\n\n/// Watch the broker's stream table and dispatch every **peer-initiated**\n/// stream to a family worker, until `stop` is set. Blocks the calling thread\n/// (the daemon spawns it on its own); workers detach (each ends with its\n/// stream).\npub fn run_dispatch_loop(\n    broker_name: &str,\n    paths: &DispatchPaths,\n    registry: Arc<RegistryHost>,\n    stop: &AtomicBool,\n    poll: Duration,\n) -> io::Result<()> {\n    let mut brain = connect(broker_name)?;\n    // Claim lifecycle (ADR-0038 C): in-flight / terminal / retryable —\n    // replaces the pre-fix one-shot `HashSet` whose burned claims permanently\n    // abandoned a stream on the first transient failure.\n    let claims: Arc<Mutex<HashMap<u64, ClaimState>>> = Arc::new(Mutex::new(HashMap::new()));\n\n    while !stop.load(Ordering::Relaxed) {\n        let reply = brain.net_streams()?;\n        for s in reply.streams {\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\n            // invisible until its first write anyway — gotcha #6 — but the\n            // next_seq guard also skips a row whose pump hasn't appended yet).\n            // Terminal rows never re-enter; a released (Retry) claim re-enters\n            // after its backoff (REQ-DISPATCH-CLAIM-RETRY).\n            if s.initiated_locally || s.next_seq == 0 {\n                continue;\n            }\n            let attempts = {\n                let mut c = claims.lock().unwrap();\n                match should_claim(c.get(&s.stream_id), Instant::now()) {\n                    Some(attempts) => {\n                        c.insert(s.stream_id, ClaimState::InFlight { attempts });\n                        attempts\n                    }\n                    None => continue,\n                }\n            };\n            let name = broker_name.to_string();\n            let origin = s.remote_id_hex.clone();\n            let stream_id = s.stream_id;\n            let floor_seq = s.floor_seq;\n            let finished = s.finished;\n            let p = paths.clone();\n            let reg = Arc::clone(&registry);\n            let claims_done = Arc::clone(&claims);\n            thread::spawn(move || {\n                let outcome = worker(&name, stream_id, &origin, floor_seq, finished, &p, &reg);\n                if matches!(outcome, DispatchOutcome::Served(_)) {\n                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n                    // this stream's exchange is over — take its row out of\n                    // every FUTURE dispatcher generation's enumeration (the\n                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n                    // an older broker without the verb answers error (KH-2.3)\n                    // and the row retires when the broker cycles.\n                    if let Ok(mut retire_brain) = connect(&name) {\n                        let _ = retire_brain.net_stream_retire(stream_id);\n                    }\n                } else {\n                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n                }\n                let mut c = claims_done.lock().unwrap();\n                c.insert(\n                    stream_id,\n                    outcome_transition(&outcome, attempts, Instant::now()),\n                );\n            });\n        }\n        thread::sleep(poll);\n    }\n    Ok(())\n}\n\n/// Connect a fresh brain IPC client (retrying through broker startup).\nfn connect(broker_name: &str) -> io::Result<Brain> {\n    let mut last = io::Error::other(\"broker never answered\");\n    for _ in 0..300 {\n        match Brain::cold_start(broker_name, now_ms()) {\n            Ok(b) => return Ok(b),\n            Err(e) => last = e,\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    Err(last)\n}\n\nfn now_ms() -> u64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_millis() as u64)\n        .unwrap_or(0)\n}\n\n/// The stream's first NDJSON line, restart-durable (ADR-0038 B,\n/// REQ-STREAM-OPENER-DURABLE): read the broker-pinned opener fact first —\n/// it survives any ring roll — and fall back to the ring peek only when the\n…\nfn peek_first_line(\n…\n) -> io::Result<Option<serde_json::Value>> {\n…\n    brain.net_stream_subscribe(stream_id, floor_seq)?;\n    let mut buf: Vec<u8> = Vec::new();\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                buf.extend_from_slice(&bytes);\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                // Finished without a complete line: classify what we have\n                // (a line without a trailing newline before EOF still counts).\n                return Ok(serde_json::from_slice(&buf).ok());\n            }\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n            _ => {}\n        }\n    }\n}\n\n/// One claimed stream's worker: peek → classify → run the family's serve\n/// loop on a fresh brain connection.\n#[allow(clippy::too_many_arguments)]\nfn worker(\n    broker_name: &str,\n    stream_id: u64,\n    origin: &str,\n    floor_seq: u64,\n    finished: bool,\n    paths: &DispatchPaths,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    if take_injected_transient() {\n        return DispatchOutcome::Failed(\n            \"injected transient worker-start failure (test valve)\".into(),\n        );\n    }\n    let line = match first_line(broker_name, stream_id, floor_seq) {\n        Ok(Some(v)) => v,\n        Ok(None) => return DispatchOutcome::Unknown,\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    };\n    let family = classify_first_line(&line);\n\n    // Gate round 1 (retire-on-sight, KNOWN-HAZARDS 7.41): a finished ATTACH\n    // row's exchange is over — the operator detached — and replaying it would\n    // take the live controller (pre-existing backlog + brain-down-detach\n    // orderings, which the Served-report path can never see). Report Served\n    // WITHOUT serving: the loop's completion path retires the row, so the\n    // backlog also drains from the table. Finished request/reply rows fall\n    // through — their FIN is protocol, not completion (see\n    // `finished_row_is_terminal`).\n    if finished_row_is_terminal(family, finished) {\n        return DispatchOutcome::Served(\"finished attach: terminal, retired unserved\".into());\n    }\n\n    let mut brain = match connect(broker_name) {\n        Ok(b) => b,\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    };\n\n    match family {\n        StreamFamily::Sync => {\n            let cs = match ContextStore::open_or_init() {\n                Ok(cs) => cs,\n                Err(e) => return DispatchOutcome::Failed(e.to_string()),\n            };\n            let policy = SyncPolicy::load();\n            match serve_sync(\n                &mut brain,\n                stream_id,\n                origin,\n                0,\n                &cs,\n                &policy,\n                &paths.scratch,\n            ) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Update => {\n            let cache = ReleaseCache::open(&paths.releases);\n            let roster = RosterStore::load();\n            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n        StreamFamily::Attach => {\n            // The gate subject is the session's OWNING endpoint, resolved\n            // from the broker's table (the spawn's label) — never from wire\n            // bytes. An unlabeled or unknown session refuses, fail-closed.\n            //\n            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n            // cannot know our local session id, so it names the ENDPOINT. When the\n            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n            // we actually host a live session for it (D5 session-presence gate;\n            // D6: a stale registry row that named a gone endpoint resolves to no\n            // session → refuse cleanly, never a false attach). The endpoint id is\n            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\n            // `endpoint`); the origin subject stays the handshake identity. The\n            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\n            // is unchanged.\n            // `endpoint` is the gate subject; `remote_session` carries the\n            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\n            // before serve_attach subscribes — never mid-serve, which would swallow\n            // interleaved attach records) and `None` on the local path.\n            let (endpoint, remote_session) = match line\n                .get(\"endpoint_id\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n            {\n                Some(ep) => match crate::attach::resolve_local_session(&mut brain, ep) {\n                    Some(sid) => (Some(ep.to_string()), Some(sid)),\n                    None => (None, None),\n                },\n                None => {\n                    let session_id = line.get(\"session_id\").and_then(|v| v.as_u64());\n                    let endpoint = session_id.and_then(|sid| {\n                        brain.sessions().ok().and_then(|reply| {\n                            reply\n                                .sessions\n                                .iter()\n                                .find(|s| s.session_id == sid && !s.endpoint.is_empty())\n                                .map(|s| s.endpoint.clone())\n                        })\n                    });\n                    (endpoint, None)\n                }\n            };\n            match endpoint {\n                // Seed the serve loop with the DURABLE opener Request\n                // (ADR-0038 B): after a ring roll the wire replay no longer\n                // carries the Request record, so the serve would idle forever\n                // waiting for one. The pinned opener IS that record — hand it\n                // over so reconstruction resumes the same operator stream.\n                // Subscribe from the ring FLOOR: a rolled ring's chunk 0 is\n                // gone, and a below-floor cursor trips the brain's gap check.\n                // [impl->REQ-STREAM-OPENER-DURABLE]\n                Some(endpoint) => match serve_attach(\n                    &mut brain,\n                    broker_name,\n                    stream_id,\n                    &endpoint,\n                    origin,\n                    floor_seq,\n                    remote_session,\n                    serde_json::from_value(line.clone()).ok(),\n                ) {\n                    Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                    Err(e) => DispatchOutcome::Failed(e.to_string()),\n                },\n                None => {\n                    // Refuse politely: finish our send side, claim stays.\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n                }\n            }\n        }\n        StreamFamily::Xfer => {\n            // The wire names whose whitelist gates (the rule set); the origin\n            // it is checked against stays handshake-proven. Blank refuses.\n            let endpoint = line\n                .get(\"endpoint\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n                .map(str::to_string);\n            match endpoint {\n                Some(endpoint) => {\n                    let root = spt_store::xfer::transfers_dir();\n                    let progress = paths.scratch.join(\"xfer-progress\");\n                    match serve_xfer(\n…\n                    ) {\n…\n                    }\n                }\n…\n            }\n        }\n…\n    }\n}"}],"details":{"displayContent":{"text":"enum ClaimState {\n…\n    /// retries exhausted. Terminal outcomes never re-enter the queue.\n    Terminal,\n    /// A transient failure released the claim; eligible again at `next_at`.\n    Retry { attempts: u32, next_at: Instant },\n}\n\n/// Bounded retry budget per stream per dispatcher lifetime — transient setup\n/// failures recover, a persistently-failing stream goes terminal loud.\nconst MAX_DISPATCH_ATTEMPTS: u32 = 3;\n\n/// Backoff base for a released claim (doubles per failed attempt).\nconst DISPATCH_RETRY_BASE: Duration = Duration::from_millis(500);\n\n/// The backoff before retry number `attempts` (1-based) runs.\nfn retry_backoff(attempts: u32) -> Duration {\n    DISPATCH_RETRY_BASE * 2u32.saturating_pow(attempts.saturating_sub(1))\n}\n\n/// PURE claim decision: may this row be (re)claimed now? `Some(prior\n/// failures)` = claim it; `None` = leave it (in flight, terminal, or backing\n/// off). The unit-tested seam for the no-hot-loop ruling.\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\nfn should_claim(state: Option<&ClaimState>, now: Instant) -> Option<u32> {\n    match state {\n        None => Some(0),\n        Some(ClaimState::Retry { attempts, next_at }) if now >= *next_at => Some(*attempts),\n        _ => None,\n    }\n}\n\n/// PURE outcome transition: what a finished worker's claim becomes. Served\n/// and Unknown (unclassifiable) are TERMINAL — a terminal classification never\n/// hot-loops. A failure requeues with backoff until the bounded budget is\n/// spent, then goes terminal.\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\nfn outcome_transition(outcome: &DispatchOutcome, attempts: u32, now: Instant) -> ClaimState {\n    match outcome {\n        DispatchOutcome::Served(_) | DispatchOutcome::Unknown => ClaimState::Terminal,\n        DispatchOutcome::Failed(_) => {\n            let attempts = attempts + 1;\n            if attempts >= MAX_DISPATCH_ATTEMPTS {\n                ClaimState::Terminal\n            } else {\n                ClaimState::Retry {\n                    attempts,\n                    next_at: now + retry_backoff(attempts),\n                }\n            }\n        }\n    }\n}\n\n/// PURE lifecycle+family discriminator (gate round 1, ADR-0038 Decision 1's\n/// \"excluded by lifecycle state\" half): is a `finished` row TERMINAL for\n/// redispatch — retire unserved, never replay?\n///\n/// `finished` alone is NOT the answer, and a bare `s.finished` claim filter\n/// would be a regression: the request/reply families FIN their send WITH the\n/// request and await the reply on the same stream (sync.rs `request_sync` and\n/// propagate.rs `fetch_update` both `net_stream_send(.., finish=true)` on the\n/// opener), so every pull that arrives while the brain is down reads\n/// `finished=true` while genuinely PENDING — filtering it breaks context sync\n/// across exactly the refresh window this milestone fixes.\n///\n/// The steal class is ATTACH-specific: `finished` on an attach stream is the\n/// operator's clean detach — its exchange is definitionally over, and a\n/// replay takes a live controller (KNOWN-HAZARDS 7.41: the pre-existing\n/// backlog on long-lived conns, and the brain-down-detach window that\n/// Served-report retirement can never see). So: finished + Attach = terminal\n/// (retired unserved via the worker's Served report); finished + anything\n/// else = a pending request — serve it.\n// [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n// [impl->REQ-HAZARD-REDISPATCH-CONTROL-STEAL]\nfn finished_row_is_terminal(family: StreamFamily, finished: bool) -> bool {\n    finished && family == StreamFamily::Attach\n}\n\n/// TEST-RIG-ONLY (the D3 regression): `SPT_DISPATCH_INJECT_TRANSIENT_ONCE`\n/// set at dispatcher start makes exactly ONE worker fail before setup —\n/// proving a transient worker-start failure requeues and recovers through\n/// the REAL claim path. Never set in production; the latch arms once per\n/// process and disarms on first take.\nfn take_injected_transient() -> bool {\n    static ARMED: OnceLock<AtomicBool> = OnceLock::new();\n    let armed = ARMED.get_or_init(|| {\n        AtomicBool::new(std::env::var(\"SPT_DISPATCH_INJECT_TRANSIENT_ONCE\").is_ok())\n    });\n    armed.swap(false, Ordering::SeqCst)\n}\n\n/// The SPT_HOME-anchored paths the serve arms need. One value, cloned per\n/// worker (policies/stores re-load from the canonical paths per request —\n/// the access-gate stance, config edits take effect without a restart).\n#[derive(Debug, Clone)]\npub struct DispatchPaths {\n    /// The perch tree (WAN message funnel target).\n    pub owlery: PathBuf,\n    /// The staged-release cache dir ([`ReleaseCache::open`]).\n    pub releases: PathBuf,\n    /// Transient bundle/artifact scratch.\n    pub scratch: PathBuf,\n}\n\nimpl DispatchPaths {\n    /// The production layout under [`perch::spt_home`].\n    pub fn production() -> DispatchPaths {\n        let home = perch::spt_home();\n        DispatchPaths {\n            owlery: perch::owlery_dir(),\n            releases: home.join(\"releases\"),\n            scratch: home.join(\"scratch\").join(\"net\"),\n        }\n    }\n}\n\n/// Watch the broker's stream table and dispatch every **peer-initiated**\n/// stream to a family worker, until `stop` is set. Blocks the calling thread\n/// (the daemon spawns it on its own); workers detach (each ends with its\n/// stream).\npub fn run_dispatch_loop(\n    broker_name: &str,\n    paths: &DispatchPaths,\n    registry: Arc<RegistryHost>,\n    stop: &AtomicBool,\n    poll: Duration,\n) -> io::Result<()> {\n    let mut brain = connect(broker_name)?;\n    // Claim lifecycle (ADR-0038 C): in-flight / terminal / retryable —\n    // replaces the pre-fix one-shot `HashSet` whose burned claims permanently\n    // abandoned a stream on the first transient failure.\n    let claims: Arc<Mutex<HashMap<u64, ClaimState>>> = Arc::new(Mutex::new(HashMap::new()));\n\n    while !stop.load(Ordering::Relaxed) {\n        let reply = brain.net_streams()?;\n        for s in reply.streams {\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\n            // invisible until its first write anyway — gotcha #6 — but the\n            // next_seq guard also skips a row whose pump hasn't appended yet).\n            // Terminal rows never re-enter; a released (Retry) claim re-enters\n            // after its backoff (REQ-DISPATCH-CLAIM-RETRY).\n            if s.initiated_locally || s.next_seq == 0 {\n                continue;\n            }\n            let attempts = {\n                let mut c = claims.lock().unwrap();\n                match should_claim(c.get(&s.stream_id), Instant::now()) {\n                    Some(attempts) => {\n                        c.insert(s.stream_id, ClaimState::InFlight { attempts });\n                        attempts\n                    }\n                    None => continue,\n                }\n            };\n            let name = broker_name.to_string();\n            let origin = s.remote_id_hex.clone();\n            let stream_id = s.stream_id;\n            let floor_seq = s.floor_seq;\n            let finished = s.finished;\n            let p = paths.clone();\n            let reg = Arc::clone(&registry);\n            let claims_done = Arc::clone(&claims);\n            thread::spawn(move || {\n                let outcome = worker(&name, stream_id, &origin, floor_seq, finished, &p, &reg);\n                if matches!(outcome, DispatchOutcome::Served(_)) {\n                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n                    // this stream's exchange is over — take its row out of\n                    // every FUTURE dispatcher generation's enumeration (the\n                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n                    // an older broker without the verb answers error (KH-2.3)\n                    // and the row retires when the broker cycles.\n                    if let Ok(mut retire_brain) = connect(&name) {\n                        let _ = retire_brain.net_stream_retire(stream_id);\n                    }\n                } else {\n                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n                }\n                let mut c = claims_done.lock().unwrap();\n                c.insert(\n                    stream_id,\n                    outcome_transition(&outcome, attempts, Instant::now()),\n                );\n            });\n        }\n        thread::sleep(poll);\n    }\n    Ok(())\n}\n\n/// Connect a fresh brain IPC client (retrying through broker startup).\nfn connect(broker_name: &str) -> io::Result<Brain> {\n    let mut last = io::Error::other(\"broker never answered\");\n    for _ in 0..300 {\n        match Brain::cold_start(broker_name, now_ms()) {\n            Ok(b) => return Ok(b),\n            Err(e) => last = e,\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    Err(last)\n}\n\nfn now_ms() -> u64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_millis() as u64)\n        .unwrap_or(0)\n}\n\n/// The stream's first NDJSON line, restart-durable (ADR-0038 B,\n/// REQ-STREAM-OPENER-DURABLE): read the broker-pinned opener fact first —\n/// it survives any ring roll — and fall back to the ring peek only when the\n…\nfn peek_first_line(\n…\n) -> io::Result<Option<serde_json::Value>> {\n…\n    brain.net_stream_subscribe(stream_id, floor_seq)?;\n    let mut buf: Vec<u8> = Vec::new();\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                buf.extend_from_slice(&bytes);\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                // Finished without a complete line: classify what we have\n                // (a line without a trailing newline before EOF still counts).\n                return Ok(serde_json::from_slice(&buf).ok());\n            }\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n            _ => {}\n        }\n    }\n}\n\n/// One claimed stream's worker: peek → classify → run the family's serve\n/// loop on a fresh brain connection.\n#[allow(clippy::too_many_arguments)]\nfn worker(\n    broker_name: &str,\n    stream_id: u64,\n    origin: &str,\n    floor_seq: u64,\n    finished: bool,\n    paths: &DispatchPaths,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    if take_injected_transient() {\n        return DispatchOutcome::Failed(\n            \"injected transient worker-start failure (test valve)\".into(),\n        );\n    }\n    let line = match first_line(broker_name, stream_id, floor_seq) {\n        Ok(Some(v)) => v,\n        Ok(None) => return DispatchOutcome::Unknown,\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    };\n    let family = classify_first_line(&line);\n\n    // Gate round 1 (retire-on-sight, KNOWN-HAZARDS 7.41): a finished ATTACH\n    // row's exchange is over — the operator detached — and replaying it would\n    // take the live controller (pre-existing backlog + brain-down-detach\n    // orderings, which the Served-report path can never see). Report Served\n    // WITHOUT serving: the loop's completion path retires the row, so the\n    // backlog also drains from the table. Finished request/reply rows fall\n    // through — their FIN is protocol, not completion (see\n    // `finished_row_is_terminal`).\n    if finished_row_is_terminal(family, finished) {\n        return DispatchOutcome::Served(\"finished attach: terminal, retired unserved\".into());\n    }\n\n    let mut brain = match connect(broker_name) {\n        Ok(b) => b,\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    };\n\n    match family {\n        StreamFamily::Sync => {\n            let cs = match ContextStore::open_or_init() {\n                Ok(cs) => cs,\n                Err(e) => return DispatchOutcome::Failed(e.to_string()),\n            };\n            let policy = SyncPolicy::load();\n            match serve_sync(\n                &mut brain,\n                stream_id,\n                origin,\n                0,\n                &cs,\n                &policy,\n                &paths.scratch,\n            ) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Update => {\n            let cache = ReleaseCache::open(&paths.releases);\n            let roster = RosterStore::load();\n            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n        StreamFamily::Attach => {\n            // The gate subject is the session's OWNING endpoint, resolved\n            // from the broker's table (the spawn's label) — never from wire\n            // bytes. An unlabeled or unknown session refuses, fail-closed.\n            //\n            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n            // cannot know our local session id, so it names the ENDPOINT. When the\n            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n            // we actually host a live session for it (D5 session-presence gate;\n            // D6: a stale registry row that named a gone endpoint resolves to no\n            // session → refuse cleanly, never a false attach). The endpoint id is\n            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\n            // `endpoint`); the origin subject stays the handshake identity. The\n            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\n            // is unchanged.\n            // `endpoint` is the gate subject; `remote_session` carries the\n            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\n            // before serve_attach subscribes — never mid-serve, which would swallow\n            // interleaved attach records) and `None` on the local path.\n            let (endpoint, remote_session) = match line\n                .get(\"endpoint_id\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n            {\n                Some(ep) => match crate::attach::resolve_local_session(&mut brain, ep) {\n                    Some(sid) => (Some(ep.to_string()), Some(sid)),\n                    None => (None, None),\n                },\n                None => {\n                    let session_id = line.get(\"session_id\").and_then(|v| v.as_u64());\n                    let endpoint = session_id.and_then(|sid| {\n                        brain.sessions().ok().and_then(|reply| {\n                            reply\n                                .sessions\n                                .iter()\n                                .find(|s| s.session_id == sid && !s.endpoint.is_empty())\n                                .map(|s| s.endpoint.clone())\n                        })\n                    });\n                    (endpoint, None)\n                }\n            };\n            match endpoint {\n                // Seed the serve loop with the DURABLE opener Request\n                // (ADR-0038 B): after a ring roll the wire replay no longer\n                // carries the Request record, so the serve would idle forever\n                // waiting for one. The pinned opener IS that record — hand it\n                // over so reconstruction resumes the same operator stream.\n                // Subscribe from the ring FLOOR: a rolled ring's chunk 0 is\n                // gone, and a below-floor cursor trips the brain's gap check.\n                // [impl->REQ-STREAM-OPENER-DURABLE]\n                Some(endpoint) => match serve_attach(\n                    &mut brain,\n                    broker_name,\n                    stream_id,\n                    &endpoint,\n                    origin,\n                    floor_seq,\n                    remote_session,\n                    serde_json::from_value(line.clone()).ok(),\n                ) {\n                    Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                    Err(e) => DispatchOutcome::Failed(e.to_string()),\n                },\n                None => {\n                    // Refuse politely: finish our send side, claim stays.\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n                }\n            }\n        }\n        StreamFamily::Xfer => {\n            // The wire names whose whitelist gates (the rule set); the origin\n            // it is checked against stays handshake-proven. Blank refuses.\n            let endpoint = line\n                .get(\"endpoint\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n                .map(str::to_string);\n            match endpoint {\n                Some(endpoint) => {\n                    let root = spt_store::xfer::transfers_dir();\n                    let progress = paths.scratch.join(\"xfer-progress\");\n                    match serve_xfer(\n…\n                    ) {\n…\n                    }\n                }\n…\n            }\n        }\n…\n    }\n}","startLine":176,"lineNumbers":[176,null,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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,null,424,null,428,null,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,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,null,612,null,615,616,null,621,622,null,657,658]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"}}},"isError":false,"timestamp":1784247567351}}
{"type":"message","id":"81f2175a","parentId":"7c3fe1c3","timestamp":"2026-07-17T00:19:27.359Z","message":{"role":"toolResult","toolCallId":"call_7VdmxLsvPTM92HHSSfDbzDfR|fc_0ae44a63c9031803016a59750efe80819bb870e0d164884b9b","toolName":"read","content":[{"type":"text","text":"impl BrokerConn {\n…\n    pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {\n        let mut g = recover(&self.inner.gate);\n        let half = g.half.take().expect(\"pin: gate half present\");\n        TestGatePin {\n            inner: Arc::clone(&self.inner),\n            half: Some(half),\n        }\n    }\n}\n\nimpl Drop for BrokerConn {\n    fn drop(&mut self) {\n        // Once-per-conn `transport-close` lifecycle record (leg d): the handle\n        // is about to close — the terminal record of this conn id's life, with\n        // how it ended (organic EOF vs poisoned vs deadline-poisoned).\n        self.inner.lifecycle(\n            \"transport-close\",\n            &format!(\n                \"poisoned={} timed_out={}\",\n                self.inner.poisoned.load(Ordering::Acquire),\n                self.inner.timed_out.load(Ordering::Acquire)\n            ),\n        );\n        {\n            let mut d = recover(&self.inner.dog);\n            d.shutdown = true;\n            self.inner.dog_cv.notify_all();\n        }\n        if let Some(dog) = self.dog.take() {\n            let _ = dog.join();\n        }\n        // Halves (gate or graveyard) drop AFTER the watchdog is gone — no raw\n        // handle use can race the CloseHandle/limbo path.\n    }\n}\n\nfn poisoned_err() -> io::Error {\n    io::Error::new(\n        io::ErrorKind::BrokenPipe,\n        \"broker conn poisoned (bounded write retired the physical connection)\",\n    )\n}\n\nfn timeout_err() -> io::Error {\n    io::Error::new(\n        io::ErrorKind::TimedOut,\n        \"broker conn write exceeded its bound (conn poisoned + aborted)\",\n    )\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::transport::{DaemonTransport, LocalSocketTransport};\n    use interprocess::local_socket::traits::Stream as _;\n    use interprocess::local_socket::Stream;\n    use std::sync::atomic::AtomicU32;\n\n    /// A real (loopback local-socket) conn pair — the send half wrapped in the\n    /// primitive, the client end returned so a test can hold it alive or drop\n    /// it to force an ORGANIC write failure.\n    fn make_conn() -> (BrokerConn, Stream) {\n        static SEQ: AtomicU32 = AtomicU32::new(0);\n        let name = format!(\n            \"spt-daemon-connattr-{}-{}.sock\",\n…\n        );\n…\n    }\n…\n}\n\n[Showing lines 629-693 of 815. Use :694 to continue]"}],"details":{"truncation":{"content":"    pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {\r\n        let mut g = recover(&self.inner.gate);\r\n        let half = g.half.take().expect(\"pin: gate half present\");\r\n        TestGatePin {\r\n            inner: Arc::clone(&self.inner),\r\n            half: Some(half),\r\n        }\r\n    }\r\n}\r\n\r\nimpl Drop for BrokerConn {\r\n    fn drop(&mut self) {\r\n        // Once-per-conn `transport-close` lifecycle record (leg d): the handle\r\n        // is about to close — the terminal record of this conn id's life, with\r\n        // how it ended (organic EOF vs poisoned vs deadline-poisoned).\r\n        self.inner.lifecycle(\r\n            \"transport-close\",\r\n            &format!(\r\n                \"poisoned={} timed_out={}\",\r\n                self.inner.poisoned.load(Ordering::Acquire),\r\n                self.inner.timed_out.load(Ordering::Acquire)\r\n            ),\r\n        );\r\n        {\r\n            let mut d = recover(&self.inner.dog);\r\n            d.shutdown = true;\r\n            self.inner.dog_cv.notify_all();\r\n        }\r\n        if let Some(dog) = self.dog.take() {\r\n            let _ = dog.join();\r\n        }\r\n        // Halves (gate or graveyard) drop AFTER the watchdog is gone — no raw\r\n        // handle use can race the CloseHandle/limbo path.\r\n    }\r\n}\r\n\r\nfn poisoned_err() -> io::Error {\r\n    io::Error::new(\r\n        io::ErrorKind::BrokenPipe,\r\n        \"broker conn poisoned (bounded write retired the physical connection)\",\r\n    )\r\n}\r\n\r\nfn timeout_err() -> io::Error {\r\n    io::Error::new(\r\n        io::ErrorKind::TimedOut,\r\n        \"broker conn write exceeded its bound (conn poisoned + aborted)\",\r\n    )\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use crate::transport::{DaemonTransport, LocalSocketTransport};\r\n    use interprocess::local_socket::traits::Stream as _;\r\n    use interprocess::local_socket::Stream;\r\n    use std::sync::atomic::AtomicU32;\r\n\r\n    /// A real (loopback local-socket) conn pair — the send half wrapped in the\r\n    /// primitive, the client end returned so a test can hold it alive or drop\r\n    /// it to force an ORGANIC write failure.\r\n    fn make_conn() -> (BrokerConn, Stream) {\r\n        static SEQ: AtomicU32 = AtomicU32::new(0);\r\n        let name = format!(\r\n            \"spt-daemon-connattr-{}-{}.sock\",\r","truncated":true,"truncatedBy":"lines","totalLines":187,"totalBytes":2267,"outputLines":65,"outputBytes":2267,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"impl BrokerConn {\n…\n    pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {\r\n        let mut g = recover(&self.inner.gate);\r\n        let half = g.half.take().expect(\"pin: gate half present\");\r\n        TestGatePin {\r\n            inner: Arc::clone(&self.inner),\r\n            half: Some(half),\r\n        }\r\n    }\r\n}\r\n\r\nimpl Drop for BrokerConn {\r\n    fn drop(&mut self) {\r\n        // Once-per-conn `transport-close` lifecycle record (leg d): the handle\r\n        // is about to close — the terminal record of this conn id's life, with\r\n        // how it ended (organic EOF vs poisoned vs deadline-poisoned).\r\n        self.inner.lifecycle(\r\n            \"transport-close\",\r\n            &format!(\r\n                \"poisoned={} timed_out={}\",\r\n                self.inner.poisoned.load(Ordering::Acquire),\r\n                self.inner.timed_out.load(Ordering::Acquire)\r\n            ),\r\n        );\r\n        {\r\n            let mut d = recover(&self.inner.dog);\r\n            d.shutdown = true;\r\n            self.inner.dog_cv.notify_all();\r\n        }\r\n        if let Some(dog) = self.dog.take() {\r\n            let _ = dog.join();\r\n        }\r\n        // Halves (gate or graveyard) drop AFTER the watchdog is gone — no raw\r\n        // handle use can race the CloseHandle/limbo path.\r\n    }\r\n}\r\n\r\nfn poisoned_err() -> io::Error {\r\n    io::Error::new(\r\n        io::ErrorKind::BrokenPipe,\r\n        \"broker conn poisoned (bounded write retired the physical connection)\",\r\n    )\r\n}\r\n\r\nfn timeout_err() -> io::Error {\r\n    io::Error::new(\r\n        io::ErrorKind::TimedOut,\r\n        \"broker conn write exceeded its bound (conn poisoned + aborted)\",\r\n    )\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use crate::transport::{DaemonTransport, LocalSocketTransport};\r\n    use interprocess::local_socket::traits::Stream as _;\r\n    use interprocess::local_socket::Stream;\r\n    use std::sync::atomic::AtomicU32;\r\n\r\n    /// A real (loopback local-socket) conn pair — the send half wrapped in the\r\n    /// primitive, the client end returned so a test can hold it alive or drop\r\n    /// it to force an ORGANIC write failure.\r\n    fn make_conn() -> (BrokerConn, Stream) {\r\n        static SEQ: AtomicU32 = AtomicU32::new(0);\r\n        let name = format!(\r\n            \"spt-daemon-connattr-{}-{}.sock\",\r\n…\n        );\n…\n    }\n…\n}","startLine":625,"lineNumbers":[625,null,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,null,696,null,705,null,814]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\conn.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":815,"totalBytes":2267,"outputLines":65,"outputBytes":2267,"shownRange":{"start":629,"end":693},"nextOffset":694}}},"isError":false,"timestamp":1784247567359}}
{"type":"message","id":"2fcdb73a","parentId":"81f2175a","timestamp":"2026-07-17T00:19:27.365Z","message":{"role":"toolResult","toolCallId":"call_4aSEXyfyWQyoUgiyQ7fs3dZO|fc_0ae44a63c9031803016a59750efe78819ba77ed6d4b1005a27","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## broker.rs\n 66|    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n*67|    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n 68|    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n 69|    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n 70|};\n...\n 3100|                }\n*3101|                KIND_NET_STREAM_RETIRE => {\n*3102|                    if let Err(msg) = self.dispatch_net_stream_retire(env, &send) {\n 3103|                        send_error(&send, &msg);\n 3104|                    }\n 3105|                }\n 3106|                KIND_NET_STREAM_OPENER => {\n*3107|                    if let Err(msg) = self.dispatch_net_stream_opener(env, &send) {\n 3108|                        send_error(&send, &msg);\n 3109|                    }\n 3110|                }\n...\n 4319|    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n*4320|    fn dispatch_net_stream_retire(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4321|        let req: NetStreamRetireReq = serde_json::from_value(env.payload)\n 4322|            .map_err(|e| format!(\"bad net-stream-retire payload: {e}\"))?;\n 4323|        let host = self.net.get().ok_or(\"net disabled\")?;\n*4324|        let removed = host.retire_stream(req.stream_id);\n 4325|        let frame = Envelope::new(\n*4326|            KIND_NET_STREAM_RETIRED,\n 4327|            serde_json::to_value(NetStreamRetired {\n 4328|                stream_id: req.stream_id,\n 4329|                removed,\n...\n 4336|\n*4337|    /// A stream's pinned opener line (ADR-0038 B) — the restart-durable\n 4338|    /// classification fact the dispatcher reads instead of peeking ring seq 0.\n 4339|    // [impl->REQ-STREAM-OPENER-DURABLE]\n*4340|    fn dispatch_net_stream_opener(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4341|        let req: NetStreamOpenerReq = serde_json::from_value(env.payload)\n*4342|            .map_err(|e| format!(\"bad net-stream-opener payload: {e}\"))?;\n 4343|        let host = self.net.get().ok_or(\"net disabled\")?;\n*4344|        let opener = host\n*4345|            .stream_opener(req.stream_id)\n 4346|            .ok_or_else(|| format!(\"no such stream {}\", req.stream_id))?;\n 4347|        let frame = Envelope::new(\n 4348|            KIND_NET_STREAM_OPENER_REPLY,\n...\n 4350|                stream_id: req.stream_id,\n*4351|                opener_b64: opener.map(|line| crate::msg::encode_bytes(&line)),\n 4352|            })\n 4353|            .expect(\"NetStreamOpenerReply serializes\"),\n 4354|        );\n## nethost.rs\n 115|\n*116|/// Max bytes buffered while hunting the opener's first newline (ADR-0038 B).\n*117|/// A protocol opener is one small NDJSON line; a stream whose first 64KiB has\n 118|/// no newline is not speaking an NDJSON protocol — capture gives up\n 119|/// (`Oversize`) and classification treats it as unclassifiable, bounded.\n 120|const OPENER_PIN_MAX: usize = 64 * 1024;\n 121|\n*122|/// Restart-durable opener capture (ADR-0038 B, REQ-STREAM-OPENER-DURABLE):\n 123|/// the stream's complete first NDJSON line, pinned OUTSIDE the evictable data\n 124|/// ring and held until the stream row goes away. Recovery classifies from\n 125|/// this fact, never from ring seq 0 (which a bounded ring evicts under\n...\n 131|    Pinned(Vec<u8>),\n*132|    /// No newline within [`OPENER_PIN_MAX`] — not an NDJSON opener; give up\n*133|    /// bounded (classification reads this as \"no opener\").\n 134|    Oversize,\n 135|}\n 136|\n...\n 208|    /// stream, independent of the ring's eviction. Fed by [`append`](Self::append).\n*209|    opener: OpenerCapture,\n 210|}\n 211|\n 212|impl StreamLog {\n...\n 221|            retentive_cap: 0,\n*222|            opener: OpenerCapture::Buffering(Vec::new()),\n 223|        }\n 224|    }\n 225|\n*226|    /// The pinned opener line, if the first `\\n` has arrived (ADR-0038 B).\n 227|    /// `None` while still buffering or when capture gave up (`Oversize`).\n*228|    fn opener_line(&self) -> Option<Vec<u8>> {\n*229|        match &self.opener {\n 230|            OpenerCapture::Pinned(line) => Some(line.clone()),\n 231|            _ => None,\n 232|        }\n...\n 255|        // [impl->REQ-STREAM-OPENER-DURABLE]\n*256|        if let OpenerCapture::Buffering(buf) = &mut self.opener {\n 257|            match bytes.iter().position(|&b| b == b'\\n') {\n 258|                Some(nl) => {\n 259|                    buf.extend_from_slice(&bytes[..nl]);\n 260|                    let line = std::mem::take(buf);\n*261|                    self.opener = if line.len() > OPENER_PIN_MAX {\n 262|                        OpenerCapture::Oversize\n 263|                    } else {\n 264|                        OpenerCapture::Pinned(line)\n...\n 269|                    if buf.len() > OPENER_PIN_MAX {\n*270|                        self.opener = OpenerCapture::Oversize;\n 271|                    }\n 272|                }\n 273|            }\n...\n 575|/// `SPT_STREAM_RING_CHUNKS` is a TEST-RIG-ONLY valve (the D2 regression rolls\n*576|/// an opener out of a small ring deterministically instead of pushing >4096\n 577|/// real transport chunks); production semantics and sizing are untouched.\n 578|fn ring_cap_from_env() -> usize {\n 579|    std::env::var(\"SPT_STREAM_RING_CHUNKS\")\n...\n 1554|    /// hazard). Retirement is ELIGIBILITY-VISIBILITY, not teardown: the row is\n*1555|    /// hidden from [`stream_infos`](Self::stream_infos) and the opener query\n 1556|    /// and its ring is freed, but the entry (send half, subscriber, pump) stays\n 1557|    /// alive so an in-flight reply always flushes — retire raced fire-and-forget\n 1558|    /// reply sends when it removed the entry (see the `retired` field note).\n...\n 1561|    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n*1562|    pub fn retire_stream(&self, stream_id: u64) -> bool {\n 1563|        let Some(entry) = self\n 1564|            .shared\n 1565|            .streams\n...\n 1577|            // A retired exchange's request bytes are dead weight — free the\n*1578|            // ring; the pinned opener and cursors stay (cheap, bounded).\n 1579|            entry.log.lock().unwrap().ring.clear();\n 1580|        }\n 1581|        newly\n...\n 1583|\n*1584|    /// The stream's pinned opener line (ADR-0038 B) — the restart-durable\n 1585|    /// classification fact, independent of the evictable data ring. Outer\n*1586|    /// `None` = no such stream; inner `None` = no opener pinned (first `\\n`\n 1587|    /// not yet arrived, or capture gave up bounded).\n 1588|    // [impl->REQ-STREAM-OPENER-DURABLE]\n*1589|    pub fn stream_opener(&self, stream_id: u64) -> Option<Option<Vec<u8>>> {\n 1590|        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n 1591|        // A retired row answers no-such-stream: classification exists only to\n 1592|        // serve, and a retired exchange must never be served again.\n## dispatch.rs\n 21|//!\n*22|//! Streams are single-purpose; the opener's first record names the protocol.\n 23|//! The kind-tagged families collide on variant names (`request` is both\n*24|//! sync's and attach's opener; `chunk`/`done`/`err` recur everywhere), so the\n 25|//! demux keys on the first line's **shape**, not its kind alone:\n 26|//!\n 27|//! | first line | family |\n...\n 239|/// propagate.rs `fetch_update` both `net_stream_send(.., finish=true)` on the\n*240|/// opener), so every pull that arrives while the brain is down reads\n 241|/// `finished=true` while genuinely PENDING — filtering it breaks context sync\n 242|/// across exactly the refresh window this milestone fixes.\n 243|///\n...\n 350|                    if let Ok(mut retire_brain) = connect(&name) {\n*351|                        let _ = retire_brain.net_stream_retire(stream_id);\n 352|                    }\n 353|                } else {\n 354|                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n...\n 388|/// The stream's first NDJSON line, restart-durable (ADR-0038 B,\n*389|/// REQ-STREAM-OPENER-DURABLE): read the broker-pinned opener fact first —\n 390|/// it survives any ring roll — and fall back to the ring peek only when the\n 391|/// broker predates the verb (KH-2.3 N-1 window). `Ok(None)` = unclassifiable\n 392|/// (terminal); `Err` = transient (the claim requeues bounded).\n...\n 397|) -> io::Result<Option<serde_json::Value>> {\n*398|    match connect(broker_name).and_then(|mut b| b.net_stream_opener(stream_id)) {\n 399|        // Pinned: classify from the durable fact. A pinned line that does not\n 400|        // parse is terminally unclassifiable (same contract as the peek path).\n 401|        Ok(Some(bytes)) => return Ok(serde_json::from_slice(&bytes).ok()),\n*402|        // The broker holds the stream but no opener is pinned yet (first\n 403|        // newline still in flight, or the capture gave up bounded). The\n 404|        // next_seq guard proved bytes exist, so treat as transient — the\n 405|        // bounded claim retry re-reads it shortly, and a stream that never\n...\n 408|            return Err(io::Error::other(format!(\n*409|                \"stream {stream_id}: opener not pinned yet\"\n 410|            )))\n 411|        }\n 412|        // Older broker (unknown verb answers error) or the row vanished —\n...\n 569|            match endpoint {\n*570|                // Seed the serve loop with the DURABLE opener Request\n 571|                // (ADR-0038 B): after a ring roll the wire replay no longer\n 572|                // carries the Request record, so the serve would idle forever\n*573|                // waiting for one. The pinned opener IS that record — hand it\n 574|                // over so reconstruction resumes the same operator stream.\n 575|                // Subscribe from the ring FLOOR: a rolled ring's chunk 0 is\n 576|                // gone, and a below-floor cursor trips the brain's gap check.\n...\n 916|\n*917|        // [unit->REQ-INST-6] the rest op's opener disambiguates by shape too.\n 918|        let rest =\n 919|            json!({\"kind\": \"request\", \"endpoint\": \"ling\", \"event\": \"suspend\", \"op_id\": \"a:1\"});\n 920|        assert_eq!(classify_first_line(&rest), StreamFamily::Rest);\n...\n 946|        for v in [\n*947|            json!({\"kind\": \"chunk\", \"sync_id\": \"s\", \"offset\": 0, \"data_b64\": \"\"}), // mid-protocol, not an opener\n 948|            json!({\"kind\": \"request\"}), // request without any discriminant\n 949|            json!({\"kind\": \"request\", \"endpoint\": \"ling\"}), // rest-ish but no event\n 950|            json!({\"kind\": \"nonsense\"}),"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs, crates/spt-daemon/src/nethost.rs, crates/spt-daemon/src/dispatch.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":45,"fileCount":3,"files":["crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/dispatch.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":13},{"path":"crates/spt-daemon/src/nethost.rs","count":20},{"path":"crates/spt-daemon/src/dispatch.rs","count":12}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## broker.rs\n   66│    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n  *67│    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n   68│    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n   69│    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n   70│};\n     │...\n 3100│                }\n*3101│                KIND_NET_STREAM_RETIRE => {\n*3102│                    if let Err(msg) = self.dispatch_net_stream_retire(env, &send) {\n 3103│                        send_error(&send, &msg);\n 3104│                    }\n 3105│                }\n 3106│                KIND_NET_STREAM_OPENER => {\n*3107│                    if let Err(msg) = self.dispatch_net_stream_opener(env, &send) {\n 3108│                        send_error(&send, &msg);\n 3109│                    }\n 3110│                }\n     │...\n 4319│    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n*4320│    fn dispatch_net_stream_retire(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4321│        let req: NetStreamRetireReq = serde_json::from_value(env.payload)\n 4322│            .map_err(|e| format!(\"bad net-stream-retire payload: {e}\"))?;\n 4323│        let host = self.net.get().ok_or(\"net disabled\")?;\n*4324│        let removed = host.retire_stream(req.stream_id);\n 4325│        let frame = Envelope::new(\n*4326│            KIND_NET_STREAM_RETIRED,\n 4327│            serde_json::to_value(NetStreamRetired {\n 4328│                stream_id: req.stream_id,\n 4329│                removed,\n     │...\n 4336│\n*4337│    /// A stream's pinned opener line (ADR-0038 B) — the restart-durable\n 4338│    /// classification fact the dispatcher reads instead of peeking ring seq 0.\n 4339│    // [impl->REQ-STREAM-OPENER-DURABLE]\n*4340│    fn dispatch_net_stream_opener(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4341│        let req: NetStreamOpenerReq = serde_json::from_value(env.payload)\n*4342│            .map_err(|e| format!(\"bad net-stream-opener payload: {e}\"))?;\n 4343│        let host = self.net.get().ok_or(\"net disabled\")?;\n*4344│        let opener = host\n*4345│            .stream_opener(req.stream_id)\n 4346│            .ok_or_else(|| format!(\"no such stream {}\", req.stream_id))?;\n 4347│        let frame = Envelope::new(\n 4348│            KIND_NET_STREAM_OPENER_REPLY,\n     │...\n 4350│                stream_id: req.stream_id,\n*4351│                opener_b64: opener.map(|line| crate::msg::encode_bytes(&line)),\n 4352│            })\n 4353│            .expect(\"NetStreamOpenerReply serializes\"),\n 4354│        );\n## nethost.rs\n  115│\n *116│/// Max bytes buffered while hunting the opener's first newline (ADR-0038 B).\n *117│/// A protocol opener is one small NDJSON line; a stream whose first 64KiB has\n  118│/// no newline is not speaking an NDJSON protocol — capture gives up\n  119│/// (`Oversize`) and classification treats it as unclassifiable, bounded.\n  120│const OPENER_PIN_MAX: usize = 64 * 1024;\n  121│\n *122│/// Restart-durable opener capture (ADR-0038 B, REQ-STREAM-OPENER-DURABLE):\n  123│/// the stream's complete first NDJSON line, pinned OUTSIDE the evictable data\n  124│/// ring and held until the stream row goes away. Recovery classifies from\n  125│/// this fact, never from ring seq 0 (which a bounded ring evicts under\n     │...\n  131│    Pinned(Vec<u8>),\n *132│    /// No newline within [`OPENER_PIN_MAX`] — not an NDJSON opener; give up\n *133│    /// bounded (classification reads this as \"no opener\").\n  134│    Oversize,\n  135│}\n  136│\n     │...\n  208│    /// stream, independent of the ring's eviction. Fed by [`append`](Self::append).\n *209│    opener: OpenerCapture,\n  210│}\n  211│\n  212│impl StreamLog {\n     │...\n  221│            retentive_cap: 0,\n *222│            opener: OpenerCapture::Buffering(Vec::new()),\n  223│        }\n  224│    }\n  225│\n *226│    /// The pinned opener line, if the first `\\n` has arrived (ADR-0038 B).\n  227│    /// `None` while still buffering or when capture gave up (`Oversize`).\n *228│    fn opener_line(&self) -> Option<Vec<u8>> {\n *229│        match &self.opener {\n  230│            OpenerCapture::Pinned(line) => Some(line.clone()),\n  231│            _ => None,\n  232│        }\n     │...\n  255│        // [impl->REQ-STREAM-OPENER-DURABLE]\n *256│        if let OpenerCapture::Buffering(buf) = &mut self.opener {\n  257│            match bytes.iter().position(|&b| b == b'\\n') {\n  258│                Some(nl) => {\n  259│                    buf.extend_from_slice(&bytes[..nl]);\n  260│                    let line = std::mem::take(buf);\n *261│                    self.opener = if line.len() > OPENER_PIN_MAX {\n  262│                        OpenerCapture::Oversize\n  263│                    } else {\n  264│                        OpenerCapture::Pinned(line)\n     │...\n  269│                    if buf.len() > OPENER_PIN_MAX {\n *270│                        self.opener = OpenerCapture::Oversize;\n  271│                    }\n  272│                }\n  273│            }\n     │...\n  575│/// `SPT_STREAM_RING_CHUNKS` is a TEST-RIG-ONLY valve (the D2 regression rolls\n *576│/// an opener out of a small ring deterministically instead of pushing >4096\n  577│/// real transport chunks); production semantics and sizing are untouched.\n  578│fn ring_cap_from_env() -> usize {\n  579│    std::env::var(\"SPT_STREAM_RING_CHUNKS\")\n     │...\n 1554│    /// hazard). Retirement is ELIGIBILITY-VISIBILITY, not teardown: the row is\n*1555│    /// hidden from [`stream_infos`](Self::stream_infos) and the opener query\n 1556│    /// and its ring is freed, but the entry (send half, subscriber, pump) stays\n 1557│    /// alive so an in-flight reply always flushes — retire raced fire-and-forget\n 1558│    /// reply sends when it removed the entry (see the `retired` field note).\n     │...\n 1561│    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n*1562│    pub fn retire_stream(&self, stream_id: u64) -> bool {\n 1563│        let Some(entry) = self\n 1564│            .shared\n 1565│            .streams\n     │...\n 1577│            // A retired exchange's request bytes are dead weight — free the\n*1578│            // ring; the pinned opener and cursors stay (cheap, bounded).\n 1579│            entry.log.lock().unwrap().ring.clear();\n 1580│        }\n 1581│        newly\n     │...\n 1583│\n*1584│    /// The stream's pinned opener line (ADR-0038 B) — the restart-durable\n 1585│    /// classification fact, independent of the evictable data ring. Outer\n*1586│    /// `None` = no such stream; inner `None` = no opener pinned (first `\\n`\n 1587│    /// not yet arrived, or capture gave up bounded).\n 1588│    // [impl->REQ-STREAM-OPENER-DURABLE]\n*1589│    pub fn stream_opener(&self, stream_id: u64) -> Option<Option<Vec<u8>>> {\n 1590│        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n 1591│        // A retired row answers no-such-stream: classification exists only to\n 1592│        // serve, and a retired exchange must never be served again.\n## dispatch.rs\n  21│//!\n *22│//! Streams are single-purpose; the opener's first record names the protocol.\n  23│//! The kind-tagged families collide on variant names (`request` is both\n *24│//! sync's and attach's opener; `chunk`/`done`/`err` recur everywhere), so the\n  25│//! demux keys on the first line's **shape**, not its kind alone:\n  26│//!\n  27│//! | first line | family |\n    │...\n 239│/// propagate.rs `fetch_update` both `net_stream_send(.., finish=true)` on the\n*240│/// opener), so every pull that arrives while the brain is down reads\n 241│/// `finished=true` while genuinely PENDING — filtering it breaks context sync\n 242│/// across exactly the refresh window this milestone fixes.\n 243│///\n    │...\n 350│                    if let Ok(mut retire_brain) = connect(&name) {\n*351│                        let _ = retire_brain.net_stream_retire(stream_id);\n 352│                    }\n 353│                } else {\n 354│                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n    │...\n 388│/// The stream's first NDJSON line, restart-durable (ADR-0038 B,\n*389│/// REQ-STREAM-OPENER-DURABLE): read the broker-pinned opener fact first —\n 390│/// it survives any ring roll — and fall back to the ring peek only when the\n 391│/// broker predates the verb (KH-2.3 N-1 window). `Ok(None)` = unclassifiable\n 392│/// (terminal); `Err` = transient (the claim requeues bounded).\n    │...\n 397│) -> io::Result<Option<serde_json::Value>> {\n*398│    match connect(broker_name).and_then(|mut b| b.net_stream_opener(stream_id)) {\n 399│        // Pinned: classify from the durable fact. A pinned line that does not\n 400│        // parse is terminally unclassifiable (same contract as the peek path).\n 401│        Ok(Some(bytes)) => return Ok(serde_json::from_slice(&bytes).ok()),\n*402│        // The broker holds the stream but no opener is pinned yet (first\n 403│        // newline still in flight, or the capture gave up bounded). The\n 404│        // next_seq guard proved bytes exist, so treat as transient — the\n 405│        // bounded claim retry re-reads it shortly, and a stream that never\n    │...\n 408│            return Err(io::Error::other(format!(\n*409│                \"stream {stream_id}: opener not pinned yet\"\n 410│            )))\n 411│        }\n 412│        // Older broker (unknown verb answers error) or the row vanished —\n    │...\n 569│            match endpoint {\n*570│                // Seed the serve loop with the DURABLE opener Request\n 571│                // (ADR-0038 B): after a ring roll the wire replay no longer\n 572│                // carries the Request record, so the serve would idle forever\n*573│                // waiting for one. The pinned opener IS that record — hand it\n 574│                // over so reconstruction resumes the same operator stream.\n 575│                // Subscribe from the ring FLOOR: a rolled ring's chunk 0 is\n 576│                // gone, and a below-floor cursor trips the brain's gap check.\n    │...\n 916│\n*917│        // [unit->REQ-INST-6] the rest op's opener disambiguates by shape too.\n 918│        let rest =\n 919│            json!({\"kind\": \"request\", \"endpoint\": \"ling\", \"event\": \"suspend\", \"op_id\": \"a:1\"});\n 920│        assert_eq!(classify_first_line(&rest), StreamFamily::Rest);\n    │...\n 946│        for v in [\n*947│            json!({\"kind\": \"chunk\", \"sync_id\": \"s\", \"offset\": 0, \"data_b64\": \"\"}), // mid-protocol, not an opener\n 948│            json!({\"kind\": \"request\"}), // request without any discriminant\n 949│            json!({\"kind\": \"request\", \"endpoint\": \"ling\"}), // rest-ish but no event\n 950│            json!({\"kind\": \"nonsense\"}),"},"isError":false,"timestamp":1784247567365}}
{"type":"message","id":"41950e72","parentId":"2fcdb73a","timestamp":"2026-07-17T00:19:27.399Z","message":{"role":"toolResult","toolCallId":"call_BqWB0EPNkZJgoN4iHXh0qDmW|fc_0ae44a63c9031803016a59750efe88819bbe8b1632af0fdbd3","toolName":"read","content":[{"type":"text","text":"    fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n…\n        match req.op_id {\n…\n            Some(op) => {\n…\n                let applied_now = outcome == Outcome::Applied;\n                send_frame(send, &build(stream_id, req.conn_id, Some(op), applied_now));\n                Ok(())\n            }\n        }\n    }\n\n    /// Write bytes to a held stream's send side (D4b). With an `op_id` the\n    /// write is exactly-once through the broker-owned journal (a replayed\n    /// chunk after a brain crash is not written twice — Spike #3 invariant D\n    /// on the send path) and acked with `net-sent`; without one it is a direct\n    /// write (no dedup, no ack), mirroring the PTY `input` contract.\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    fn dispatch_net_stream_send(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: NetStreamSendReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad net-stream-send payload: {e}\"))?;\n        let bytes =\n            decode_bytes(&req.data_b64).map_err(|e| format!(\"bad net-stream-send base64: {e}\"))?;\n        let host = self.net.get().ok_or(\"net disabled\")?;\n        match req.op_id {\n            None => host\n                .send_stream(req.stream_id, &bytes, req.finish)\n                .map_err(|e| e.to_string()),\n            Some(op) => {\n                let minter = req.minter.unwrap_or(Minter::Legacy);\n                let key = EffectKey {\n                    class: NET_EFFECT_SESSION,\n                    minter,\n                    op,\n                };\n                let outcome = self\n                    .journal\n                    .apply_once(key, EffectKind::NetSend, || {\n                        host.send_stream(req.stream_id, &bytes, req.finish)\n                    })\n                    .map_err(|e| format!(\"net-stream-send failed: {e}\"))?;\n                let frame = Envelope::new(\n                    KIND_NET_SENT,\n                    serde_json::to_value(NetSent {\n                        stream_id: req.stream_id,\n                        op_id: op,\n                        applied_now: outcome == Outcome::Applied,\n                    })\n                    .expect(\"NetSent serializes\"),\n                );\n                send_frame(send, &frame);\n                Ok(())\n            }\n        }\n    }\n\n    /// List every stream the broker holds (D4b) — the query a re-attaching\n    /// brain uses to find streams that arrived while it was down.\n    fn dispatch_net_streams(&self, send: &SharedSend) -> Result<(), String> {\n        let host = self.net.get().ok_or(\"net disabled\")?;\n        let frame = Envelope::new(\n            KIND_NET_STREAMS_REPLY,\n            serde_json::to_value(NetStreamsReply {\n                streams: host.stream_infos(),\n            })\n            .expect(\"NetStreamsReply serializes\"),\n        );\n        send_frame(send, &frame);\n        Ok(())\n    }\n\n    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A):\n    /// the dispatcher reports a serve loop that ran to completion, and the row\n    /// leaves the table so no future dispatcher generation can rediscover and\n    /// replay it (the KNOWN-HAZARDS 7.41 control-steal class).\n\n[Showing lines 4249-4318 of 6063. Use :4319 to continue]"}],"details":{"truncation":{"content":"                let applied_now = outcome == Outcome::Applied;\r\n                send_frame(send, &build(stream_id, req.conn_id, Some(op), applied_now));\r\n                Ok(())\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Write bytes to a held stream's send side (D4b). With an `op_id` the\r\n    /// write is exactly-once through the broker-owned journal (a replayed\r\n    /// chunk after a brain crash is not written twice — Spike #3 invariant D\r\n    /// on the send path) and acked with `net-sent`; without one it is a direct\r\n    /// write (no dedup, no ack), mirroring the PTY `input` contract.\r\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\r\n    fn dispatch_net_stream_send(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: NetStreamSendReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad net-stream-send payload: {e}\"))?;\r\n        let bytes =\r\n            decode_bytes(&req.data_b64).map_err(|e| format!(\"bad net-stream-send base64: {e}\"))?;\r\n        let host = self.net.get().ok_or(\"net disabled\")?;\r\n        match req.op_id {\r\n            None => host\r\n                .send_stream(req.stream_id, &bytes, req.finish)\r\n                .map_err(|e| e.to_string()),\r\n            Some(op) => {\r\n                let minter = req.minter.unwrap_or(Minter::Legacy);\r\n                let key = EffectKey {\r\n                    class: NET_EFFECT_SESSION,\r\n                    minter,\r\n                    op,\r\n                };\r\n                let outcome = self\r\n                    .journal\r\n                    .apply_once(key, EffectKind::NetSend, || {\r\n                        host.send_stream(req.stream_id, &bytes, req.finish)\r\n                    })\r\n                    .map_err(|e| format!(\"net-stream-send failed: {e}\"))?;\r\n                let frame = Envelope::new(\r\n                    KIND_NET_SENT,\r\n                    serde_json::to_value(NetSent {\r\n                        stream_id: req.stream_id,\r\n                        op_id: op,\r\n                        applied_now: outcome == Outcome::Applied,\r\n                    })\r\n                    .expect(\"NetSent serializes\"),\r\n                );\r\n                send_frame(send, &frame);\r\n                Ok(())\r\n            }\r\n        }\r\n    }\r\n\r\n    /// List every stream the broker holds (D4b) — the query a re-attaching\r\n    /// brain uses to find streams that arrived while it was down.\r\n    fn dispatch_net_streams(&self, send: &SharedSend) -> Result<(), String> {\r\n        let host = self.net.get().ok_or(\"net disabled\")?;\r\n        let frame = Envelope::new(\r\n            KIND_NET_STREAMS_REPLY,\r\n            serde_json::to_value(NetStreamsReply {\r\n                streams: host.stream_infos(),\r\n            })\r\n            .expect(\"NetStreamsReply serializes\"),\r\n        );\r\n        send_frame(send, &frame);\r\n        Ok(())\r\n    }\r\n\r\n    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A):\r\n    /// the dispatcher reports a serve loop that ran to completion, and the row\r\n    /// leaves the table so no future dispatcher generation can rediscover and\r\n    /// replay it (the KNOWN-HAZARDS 7.41 control-steal class).\r","truncated":true,"truncatedBy":"lines","totalLines":1815,"totalBytes":3182,"outputLines":70,"outputBytes":3182,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n…\n        match req.op_id {\n…\n            Some(op) => {\n…\n                let applied_now = outcome == Outcome::Applied;\r\n                send_frame(send, &build(stream_id, req.conn_id, Some(op), applied_now));\r\n                Ok(())\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Write bytes to a held stream's send side (D4b). With an `op_id` the\r\n    /// write is exactly-once through the broker-owned journal (a replayed\r\n    /// chunk after a brain crash is not written twice — Spike #3 invariant D\r\n    /// on the send path) and acked with `net-sent`; without one it is a direct\r\n    /// write (no dedup, no ack), mirroring the PTY `input` contract.\r\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\r\n    fn dispatch_net_stream_send(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: NetStreamSendReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad net-stream-send payload: {e}\"))?;\r\n        let bytes =\r\n            decode_bytes(&req.data_b64).map_err(|e| format!(\"bad net-stream-send base64: {e}\"))?;\r\n        let host = self.net.get().ok_or(\"net disabled\")?;\r\n        match req.op_id {\r\n            None => host\r\n                .send_stream(req.stream_id, &bytes, req.finish)\r\n                .map_err(|e| e.to_string()),\r\n            Some(op) => {\r\n                let minter = req.minter.unwrap_or(Minter::Legacy);\r\n                let key = EffectKey {\r\n                    class: NET_EFFECT_SESSION,\r\n                    minter,\r\n                    op,\r\n                };\r\n                let outcome = self\r\n                    .journal\r\n                    .apply_once(key, EffectKind::NetSend, || {\r\n                        host.send_stream(req.stream_id, &bytes, req.finish)\r\n                    })\r\n                    .map_err(|e| format!(\"net-stream-send failed: {e}\"))?;\r\n                let frame = Envelope::new(\r\n                    KIND_NET_SENT,\r\n                    serde_json::to_value(NetSent {\r\n                        stream_id: req.stream_id,\r\n                        op_id: op,\r\n                        applied_now: outcome == Outcome::Applied,\r\n                    })\r\n                    .expect(\"NetSent serializes\"),\r\n                );\r\n                send_frame(send, &frame);\r\n                Ok(())\r\n            }\r\n        }\r\n    }\r\n\r\n    /// List every stream the broker holds (D4b) — the query a re-attaching\r\n    /// brain uses to find streams that arrived while it was down.\r\n    fn dispatch_net_streams(&self, send: &SharedSend) -> Result<(), String> {\r\n        let host = self.net.get().ok_or(\"net disabled\")?;\r\n        let frame = Envelope::new(\r\n            KIND_NET_STREAMS_REPLY,\r\n            serde_json::to_value(NetStreamsReply {\r\n                streams: host.stream_infos(),\r\n            })\r\n            .expect(\"NetStreamsReply serializes\"),\r\n        );\r\n        send_frame(send, &frame);\r\n        Ok(())\r\n    }\r\n\r\n    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A):\r\n    /// the dispatcher reports a serve loop that ran to completion, and the row\r\n    /// leaves the table so no future dispatcher generation can rediscover and\r\n    /// replay it (the KNOWN-HAZARDS 7.41 control-steal class).\r","startLine":4207,"lineNumbers":[4207,null,4223,null,4229,null,4249,4250,4251,4252,4253,4254,4255,4256,4257,4258,4259,4260,4261,4262,4263,4264,4265,4266,4267,4268,4269,4270,4271,4272,4273,4274,4275,4276,4277,4278,4279,4280,4281,4282,4283,4284,4285,4286,4287,4288,4289,4290,4291,4292,4293,4294,4295,4296,4297,4298,4299,4300,4301,4302,4303,4304,4305,4306,4307,4308,4309,4310,4311,4312,4313,4314,4315,4316,4317,4318]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6063,"totalBytes":3182,"outputLines":70,"outputBytes":3182,"shownRange":{"start":4249,"end":4318},"nextOffset":4319}}},"isError":false,"timestamp":1784247567399}}
{"type":"message","id":"e16ca868","parentId":"41950e72","timestamp":"2026-07-17T00:19:34.378Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing serve_attach function signature**"},{"type":"toolCall","id":"call_cwpMW5hSuBzWTnSkquvU6fHR|fc_0ae44a63c9031803016a5975153160819b97509baccc41d7e4","name":"read","arguments":{"i":"Reading current attach reconstruction start","path":"crates/spt-daemon/src/attach.rs","selector":"180-285,350-430"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":11257,"output":147,"cacheRead":210432,"cacheWrite":0,"totalTokens":221836,"reasoningTokens":101,"cost":{"input":0.056285,"output":0.00441,"cacheRead":0.10521599999999999,"cacheWrite":0,"total":0.16591099999999998}},"stopReason":"toolUse","timestamp":1784247567450,"responseId":"resp_0ae44a63c9031803016a59750fa3bc819ba1959d24c8d3487c","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ae44a63c9031803016a597512d468819bb0af6dc77204fb53","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXUVoXGLGgALf1dY1VTVo0DFZSwDvm2Woar08ItuERuRuykWwWt3P_E1iOvKHdmU-bEXsHkyzMXTxGNVTgZXiCzgNNfuMjIx0r9t5nvWRl8mRIdAQMkXqqz5qwCBXlHfsTV0XB28vj1ONOQZAA8Sd7qh3freuGzARMe4XcoPaGydKHI98zmEzaNljfq_YvnkuULDk7RaPMolHWYDg4oyFDa094xMs1_6M2WXw-nb1YDCPb5o2VMZoCAX0XcYR_ZsdLQMS_SmHO8-spHHLQGQ8lUBn10AJMZajBCLsj4_NLr0VmILQ_gQrSvdzjL0_o-gT6LszKurJEDNgBBNwk9yKzYlaJgXwOzvz4l26kPFpftekXazci-7rBG6LnfVzbGK10N8PL3EYXcCwoQIXTtAfNFPBXn0Q1mmzvfuoPgtaXLp8D25xNyzUmY2BDVz3r2fyAS95zEVEGJ1oRAhG8ncLSkqt_mK1Q5URH2sZ5NfOMzcw4VQJ64N88GKcd3xBrw4XK7LLK_anQMD9Wtt2kdK12aUROSuHNFC9OcreuP8Nm2Xd-hl2t7kXxgvbEdrfnOrbYIHjyOvWWLazAL_Dgheb8s61cbcGQak1_siUQ4nHTi2vY8ZJ3waPv81h7RKoTX39999W3XMcmaP9kgO7QaKKlPJoLEJJty9ySa8_q4hMhS76PcnMVIoG3M3qosG93o_kl9YqNh8RoKeRNTR-RmzvK73gkno0zsWoaCy-zxqWBvxeG2N9V4qyYErgJnXw-qQJip4o4DBSsE-nMlw3dTrhCCJYKba7nPimyhBQCGScrLymICOEHFmK1E_c12BtxTzlGZ3hSQUSlARIvjqJdU3ez32NS-BbXD3MXVOjCvHp55Uz2rzKClFPl-sRv02rJvO8L44MPCg-tlcSi-YyApJo97V-4MHuiZJkLcd7En6i0Pef1SoS0O1PF5nVRCA7fRsqaQxf0YWQ5p3c25bM1Z8GqozIDs5sB2e3WU73xpZ1N1bTRgGukdD3JxFkliVRgI7SRIpRurHXdBurwrukSeGEKcYIV01D60tPV7D-Ezy9efVcIwu7nTYseUWfIE1yOsXjqt8Uao2oWjE1APWk-62Wtz_nccj4QwnAC6gLydUW7j21trFztE61bki14mor4Xa7f3WyBcJR8A6Uei1Zhd5c3CjeaxKE-zIewMyovouQGHBTdRjOgB_z81kAnpUbOwZjMjYSpM7PJSCevpMZtQWHnYVtcVQBa92GiJFB59YWKXzTp544NAEW4Xtllu6GIvDeuDYCDNHDDk3I21S4ZczpsxA6eC_gd1R5nmb9QK35JUphye5pIPvIFVlzmHHjkqOREeMCNFLxKr-bBY1dbg5BvtfLYD6NCb5v6eRKM4hvd93UF7k0z1tHy8BPXgwdfslR8e5Y7ogFiALQAtESU-JnQij1ItKi-vwawe-9uPh15ynbrVMVJRy1PUoay5hfHDKGZSpJeXJeow2GxXdD6DIQxctoCFGAIIW-LcTQp_ekfMDaJEZurxY06--R-ZAwizCGRCy4spAf6YSNK7JwP5usMaypLp1rloCDaWOU3eQC-cl4vSvvP8c0RhSDpvbB3sUSmHR","summary":[{"type":"summary_text","text":"**Analyzing serve_attach function signature**"}]},{"id":"fc_0ae44a63c9031803016a5975153160819b97509baccc41d7e4","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading current attach reconstruction start\",\"path\":\"crates/spt-daemon/src/attach.rs\",\"selector\":\"180-285,350-430\"}","call_id":"call_cwpMW5hSuBzWTnSkquvU6fHR","name":"read"}]},"duration":6910.305100001395,"ttft":3373.435300000012,"contextSnapshot":{"promptTokens":221689,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_cwpMW5hSuBzWTnSkquvU6fHR|fc_0ae44a63c9031803016a5975153160819b97509baccc41d7e4","toolName":"read","startedAt":"2026-07-17T00:19:34.378Z","args":{"path":"crates/spt-daemon/src/attach.rs"},"intent":"Reading current attach reconstruction start"},"id":"f05e65e5","parentId":"e16ca868","timestamp":"2026-07-17T00:19:34.378Z"}
{"type":"message","id":"e3f8a0f6","parentId":"f05e65e5","timestamp":"2026-07-17T00:19:34.386Z","message":{"role":"toolResult","toolCallId":"call_cwpMW5hSuBzWTnSkquvU6fHR|fc_0ae44a63c9031803016a5975153160819b97509baccc41d7e4","toolName":"read","content":[{"type":"text","text":"pub fn read_controller_event_resuming(\n…\n) -> io::Result<BrokerEvent> {\n…\n    loop {\n        match brain.read_event() {\n…\n            Err(e) if is_output_gap(&e) => {\n…\n                if last_resume_floor == Some(floor) {\n…\n                }\n                last_resume_floor = Some(floor);\n                // RE-FETCH from the frozen delivered_through (same operator `by` →\n                // a silent re-take, no Displaced); the broker replays the dropped\n                // frames from the ring.\n                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\n                swallow_resume_ack = true;\n            }\n            Err(e) => return Err(e),\n        }\n    }\n}\n\n/// Resolve an endpoint label to THIS node's local broker session id (the spawn's\n/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\n/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\n/// its own session table, so a remote operator — which cannot know our local\n/// session id — names the endpoint and we map it here, never trusting a wire\n/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n/// (a stale registry row routed us an attach for something gone → the caller\n/// refuses cleanly, D6).\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n    brain.sessions().ok().and_then(|reply| {\n        reply\n            .sessions\n            .into_iter()\n            .find(|s| s.endpoint == endpoint)\n            .map(|s| s.session_id)\n    })\n}\n\n/// One attach `Request` — the SINGLE handling path for both a wire-arriving\n/// record and the dispatcher's durable-opener seed (ADR-0038 B; one\n/// implementation so the two entries cannot drift):\n///\n/// - Unsolicited-inbound gate (ADR-0009) under the transport-proven origin;\n///   viewing gated IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A). A\n///   denial finishes our send side and ends the serve (`Some(Refused)`).\n/// - Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): the dispatcher-resolved\n///   session id wins (the owning node is authoritative for its own table);\n///   the LOCAL path uses the wire `session_id` unchanged.\n/// - Role-aware subscribe (REQ-RCVIEW-1): the broker resolves controller /\n///   viewer / busy / take by intent + origin and replies a `Subscribed`;\n///   driven_by / viewer_count are BROKER-owned (single writer). A VIEWER\n///   attach ARMS snap-above at the floor (REQ-HAZARD-VIEWER-RING-ROLL-SNAP);\n///   the CONTROLLER keeps strict reject-gap (exactly-once resume, B2).\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\n// [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\n#[allow(clippy::too_many_arguments)]\nfn handle_attach_request(\n    brain: &mut Brain,\n    wire: &mut Brain,\n    stream_id: u64,\n    endpoint: &str,\n    origin_node: &str,\n    remote_session: Option<u64>,\n    session_id: u64,\n    from_seq: u64,\n    req_intent: AttachIntent,\n    intent: &mut AttachIntent,\n    attached_session: &mut Option<u64>,\n    attached: &mut bool,\n) -> io::Result<Option<AttachServeOutcome>> {\n    if access_check(endpoint, origin_node, InboundClass::Unsolicited) == AccessDecision::Deny {\n        wire.net_stream_send(stream_id, &[], None, true)?;\n        return Ok(Some(AttachServeOutcome::Refused));\n    }\n    let sid = remote_session.unwrap_or(session_id);\n    *intent = req_intent;\n    *attached_session = Some(sid);\n    if req_intent == AttachIntent::Viewer {\n        brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\n    } else {\n        brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\n    }\n    *attached = true;\n    Ok(None)\n}\n\n/// Serve one attach stream against this brain's broker, blocking until the\n/// session exits, the operator detaches, or the gate refuses.\n///\n/// `origin_node` MUST be the handshake-proven remote id from the broker's\n/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes\n/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\n/// is being attached, for the gate. `net_from_seq` is this brain's resume\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n/// journal, and re-pumped output dedups at the operator's render cursor).\n///\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n/// round-trip inside the event loop would swallow interleaved attach records\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\n/// the wire `session_id`.\n///\n/// `seed_request` is the broker-pinned opener Request (ADR-0038 B): after the\n/// bounded ring rolls, the wire replay no longer carries the stream's Request\n/// record — a reconstruction that waited for one would idle forever. The\n/// dispatcher hands the durable opener here and it is handled EXACTLY like a\n/// wire-arriving Request (same gate, same subscribe); a Request that then\n/// replays from the ring anyway re-runs the same-identity re-take, which is\n…\n    if let Some(AttachRecord::Request {\n        session_id,\n        from_seq,\n        intent: req_intent,\n        endpoint_id: _,\n    }) = seed_request\n    {\n        if let Some(out) = 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            &mut intent,\n            &mut attached_session,\n            &mut attached,\n        )? {\n            return Ok(out);\n        }\n    }\n\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 = match (role, attached_session) {\n            (Some(ServeRole::Controller), Some(sid)) => {\n                match read_controller_event_resuming(brain, sid, 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                        let _ = wire.net_stream_send(stream_id, &line, None, true);\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                    Err(e) => {\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                }\n            }\n            _ => brain.read_event()?,\n        };\n        match event {\n            // ── Wire → session: requests, input, resize. ───────────────────\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Request {\n                            session_id,\n                            from_seq,\n                            intent: req_intent,\n                            // The dispatcher already resolved endpoint_id→session\n                            // (passed as remote_session) BEFORE this loop; ignore\n                            // the wire copy here (a mid-loop re-resolve would swallow\n                            // interleaved attach records — see remote_session docs).\n                            endpoint_id: _,\n                        } => {\n…\n                        }\n…\n                    }\n                }\n            }\n…\n        }\n    }"}],"details":{"displayContent":{"text":"pub fn read_controller_event_resuming(\n…\n) -> io::Result<BrokerEvent> {\n…\n    loop {\n        match brain.read_event() {\n…\n            Err(e) if is_output_gap(&e) => {\n…\n                if last_resume_floor == Some(floor) {\n…\n                }\n                last_resume_floor = Some(floor);\n                // RE-FETCH from the frozen delivered_through (same operator `by` →\n                // a silent re-take, no Displaced); the broker replays the dropped\n                // frames from the ring.\n                brain.attach_as(session_id, floor, AttachIntent::Control, by)?;\n                swallow_resume_ack = true;\n            }\n            Err(e) => return Err(e),\n        }\n    }\n}\n\n/// Resolve an endpoint label to THIS node's local broker session id (the spawn's\n/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\n/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\n/// its own session table, so a remote operator — which cannot know our local\n/// session id — names the endpoint and we map it here, never trusting a wire\n/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n/// (a stale registry row routed us an attach for something gone → the caller\n/// refuses cleanly, D6).\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\npub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n    brain.sessions().ok().and_then(|reply| {\n        reply\n            .sessions\n            .into_iter()\n            .find(|s| s.endpoint == endpoint)\n            .map(|s| s.session_id)\n    })\n}\n\n/// One attach `Request` — the SINGLE handling path for both a wire-arriving\n/// record and the dispatcher's durable-opener seed (ADR-0038 B; one\n/// implementation so the two entries cannot drift):\n///\n/// - Unsolicited-inbound gate (ADR-0009) under the transport-proven origin;\n///   viewing gated IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A). A\n///   denial finishes our send side and ends the serve (`Some(Refused)`).\n/// - Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): the dispatcher-resolved\n///   session id wins (the owning node is authoritative for its own table);\n///   the LOCAL path uses the wire `session_id` unchanged.\n/// - Role-aware subscribe (REQ-RCVIEW-1): the broker resolves controller /\n///   viewer / busy / take by intent + origin and replies a `Subscribed`;\n///   driven_by / viewer_count are BROKER-owned (single writer). A VIEWER\n///   attach ARMS snap-above at the floor (REQ-HAZARD-VIEWER-RING-ROLL-SNAP);\n///   the CONTROLLER keeps strict reject-gap (exactly-once resume, B2).\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\n// [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\n#[allow(clippy::too_many_arguments)]\nfn handle_attach_request(\n    brain: &mut Brain,\n    wire: &mut Brain,\n    stream_id: u64,\n    endpoint: &str,\n    origin_node: &str,\n    remote_session: Option<u64>,\n    session_id: u64,\n    from_seq: u64,\n    req_intent: AttachIntent,\n    intent: &mut AttachIntent,\n    attached_session: &mut Option<u64>,\n    attached: &mut bool,\n) -> io::Result<Option<AttachServeOutcome>> {\n    if access_check(endpoint, origin_node, InboundClass::Unsolicited) == AccessDecision::Deny {\n        wire.net_stream_send(stream_id, &[], None, true)?;\n        return Ok(Some(AttachServeOutcome::Refused));\n    }\n    let sid = remote_session.unwrap_or(session_id);\n    *intent = req_intent;\n    *attached_session = Some(sid);\n    if req_intent == AttachIntent::Viewer {\n        brain.attach_as_viewer_snap(sid, from_seq, Some(origin_node))?;\n    } else {\n        brain.attach_as(sid, from_seq, req_intent, Some(origin_node))?;\n    }\n    *attached = true;\n    Ok(None)\n}\n\n/// Serve one attach stream against this brain's broker, blocking until the\n/// session exits, the operator detaches, or the gate refuses.\n///\n/// `origin_node` MUST be the handshake-proven remote id from the broker's\n/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes\n/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session\n/// is being attached, for the gate. `net_from_seq` is this brain's resume\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n/// journal, and re-pumped output dedups at the operator's render cursor).\n///\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n/// round-trip inside the event loop would swallow interleaved attach records\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\n/// the wire `session_id`.\n///\n/// `seed_request` is the broker-pinned opener Request (ADR-0038 B): after the\n/// bounded ring rolls, the wire replay no longer carries the stream's Request\n/// record — a reconstruction that waited for one would idle forever. The\n/// dispatcher hands the durable opener here and it is handled EXACTLY like a\n/// wire-arriving Request (same gate, same subscribe); a Request that then\n/// replays from the ring anyway re-runs the same-identity re-take, which is\n…\n    if let Some(AttachRecord::Request {\n        session_id,\n        from_seq,\n        intent: req_intent,\n        endpoint_id: _,\n    }) = seed_request\n    {\n        if let Some(out) = 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            &mut intent,\n            &mut attached_session,\n            &mut attached,\n        )? {\n            return Ok(out);\n        }\n    }\n\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 = match (role, attached_session) {\n            (Some(ServeRole::Controller), Some(sid)) => {\n                match read_controller_event_resuming(brain, sid, 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                        let _ = wire.net_stream_send(stream_id, &line, None, true);\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                    Err(e) => {\n                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                        return Err(e);\n                    }\n                }\n            }\n            _ => brain.read_event()?,\n        };\n        match event {\n            // ── Wire → session: requests, input, resize. ───────────────────\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Request {\n                            session_id,\n                            from_seq,\n                            intent: req_intent,\n                            // The dispatcher already resolved endpoint_id→session\n                            // (passed as remote_session) BEFORE this loop; ignore\n                            // the wire copy here (a mid-loop re-resolve would swallow\n                            // interleaved attach records — see remote_session docs).\n                            endpoint_id: _,\n                        } => {\n…\n                        }\n…\n                    }\n                }\n            }\n…\n        }\n    }","startLine":158,"lineNumbers":[158,null,162,null,165,166,null,174,null,176,null,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,null,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,null,447,null,489,490,491,null,605,606]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\attach.rs"}}},"isError":false,"timestamp":1784247574386}}
{"type":"message","id":"a08ad9ef","parentId":"e3f8a0f6","timestamp":"2026-07-17T00:19:40.012Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reviewing ADR 0038 for root cause**"},{"type":"toolCall","id":"call_spLIRbbt7SmvkPl6BCTTgdwe|fc_0ae44a63c9031803016a59751ba9b4819bba9fb3ec7b7938a1","name":"read","arguments":{"i":"Reading stream redispatch ADR","path":"docs/adr/0038-broker-stream-lifecycle-and-redispatch.md","selector":"1-320"}},{"type":"toolCall","id":"call_au47Bblo2M03vG7xyl0w8dDW|fc_0ae44a63c9031803016a59751ba9c4819ba1d586bd33ab7b9e","name":"grep","arguments":{"i":"Finding poison dispatch latency discussion","pattern":"poison|delay|DISPATCH|15|completion|write-start|stream-subscriber|PUMP_PEER_FAIL|4361","path":"docs","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":5288,"output":195,"cacheRead":218624,"cacheWrite":0,"totalTokens":224107,"reasoningTokens":62,"cost":{"input":0.02644,"output":0.00585,"cacheRead":0.10931199999999999,"cacheWrite":0,"total":0.141602}},"stopReason":"toolUse","timestamp":1784247574414,"responseId":"resp_0ae44a63c9031803016a5975169940819b8388dbaa4188d790","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ae44a63c9031803016a597517e508819bb04d49a4ba78cd79","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXUb9FTzaSxMW9zBOfSoMAg4ZTXtifbHwvvRvmfogH64yxyMOXwpTegXVTYTAHGpDv8Z4kUO2WyP9e3Tebgo_lo60Ata_wEF_xfvgNJxmkJvl4KDhJhXkpqYyDrJf9GoA3RjVbPCFh43BN7Cr4hg0NNj4VS92KONKwbKqW_U94SYJFMT6s_ZDUfAEZcOcQMjJZAp5L6ZB7inZD6ib5TOryUZa5f-AWVsbPruGB0TQLLrZVRvQDqjAhMZmeJ8i7Aepeiwk-2OSyHnDINNp8310pRG2HtEgjjJl5gqqrmM4tdVvaNVsDr9pTmHlbbYAtzI4StZus8AllrRVIiFsaxtbKc_qpiBclaApOs1qxowmS2zOEl-j7fEvJDaiseegyQVL7CdNmOSJO-WeXLwRPmLs7TdMOgrchJujv2BD1E8xZwp9yTlEni1q6UGdS4ItDzRC0_bjVn1K4pcqNFvuwbP0axgOew92AeSjtxp6zD008u-NE8WH_7XLUf4-Z-JJjRDNJJzbHApwxt6TmPaaK4VjDVkZN45bztvYncx-qPTXa2rIHJ6M2NpNDSuzmcbpJD64aA61IYzDqfqVA0ogasNssHDK5jWdoH_i53S0HEO0Ahh3jtCloWJ0X-Wct_dFi191D4WhVNu31RhvzyEbqhAYj6OMjdsRjStXPGMyEuO4xZSZdjE6Prweu_QXgtkDEz3D9uPGWy98_TRfh88VGo58Vlh5GH02qVSfZOwhnvQv2GAJZT8rKGLbWkiqXPiMLtaDJz3-Ih8iSG626t6roBMehVCIA1_ZthvnsjF6NfHhgimsZuCcn_uZKEKhGFVb26Ofee-nP1rHEuC3XWg5fkOcLKah74f7kqqTpOaw_LA8ijLGVnkFuMlDyXEDvLcV2DuBAabCD3LPPXBNCIdJhIjF6_5BzcDJsfji04PRXHs9h2pnoE8jjqZoM6u_MEeHqiQSa37LW8w2x75Za5v29DgiSrd52YurbkI5u96jQwnxhIa1OYBwnEzs6tVQkLjhkM1uw5-9BT3eR6qENESxyzkCoSC3BAu0J4waEpe2oyojRKsYmOaY7izHM7NOvn5hAeAx1Aya9ZA8Gf2zDIMKkVMLbD_0t_79cJ2faSFEQ1E0BLK09m4Hw2P7ULQK7kXS_3Y-LkLNvVQQJbTBKqZiRVlMcP-00qiWbI5UwF1wh_TawQzqSawdyTjGoZguLWI0syHdUTufKLl7W4L0C6OgyOuy56h3sflMAYy63RUGxZPTVW7bBm8EA2UCBoSAsOuzHFYcFbSfDBvoAYw2rtejpolQJse8w==","summary":[{"type":"summary_text","text":"**Reviewing ADR 0038 for root cause**"}]},{"id":"fc_0ae44a63c9031803016a59751ba9b4819bba9fb3ec7b7938a1","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading stream redispatch ADR\",\"path\":\"docs/adr/0038-broker-stream-lifecycle-and-redispatch.md\",\"selector\":\"1-320\"}","call_id":"call_spLIRbbt7SmvkPl6BCTTgdwe","name":"read"},{"id":"fc_0ae44a63c9031803016a59751ba9c4819ba1d586bd33ab7b9e","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding poison dispatch latency discussion\",\"pattern\":\"poison|delay|DISPATCH|15|completion|write-start|stream-subscriber|PUMP_PEER_FAIL|4361\",\"path\":\"docs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_au47Bblo2M03vG7xyl0w8dDW","name":"grep"}]},"duration":5597.170199997723,"ttft":1476.8993000015616,"contextSnapshot":{"promptTokens":223912,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_spLIRbbt7SmvkPl6BCTTgdwe|fc_0ae44a63c9031803016a59751ba9b4819bba9fb3ec7b7938a1","toolName":"read","startedAt":"2026-07-17T00:19:40.012Z","args":{"path":"docs/adr/0038-broker-stream-lifecycle-and-redispatch.md"},"intent":"Reading stream redispatch ADR"},"id":"e4be4c34","parentId":"a08ad9ef","timestamp":"2026-07-17T00:19:40.012Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_au47Bblo2M03vG7xyl0w8dDW|fc_0ae44a63c9031803016a59751ba9c4819ba1d586bd33ab7b9e","toolName":"grep","startedAt":"2026-07-17T00:19:40.012Z","args":{"path":"docs"},"intent":"Finding poison dispatch latency discussion"},"id":"bf815f58","parentId":"e4be4c34","timestamp":"2026-07-17T00:19:40.012Z"}
{"type":"message","id":"286fe0bd","parentId":"bf815f58","timestamp":"2026-07-17T00:19:40.031Z","message":{"role":"toolResult","toolCallId":"call_spLIRbbt7SmvkPl6BCTTgdwe|fc_0ae44a63c9031803016a59751ba9b4819bba9fb3ec7b7938a1","toolName":"read","content":[{"type":"text","text":"Path 'docs/adr/0038-broker-stream-lifecycle-and-redispatch.md' not found"}],"details":{},"isError":true,"timestamp":1784247580031}}
{"type":"message","id":"eeed5873","parentId":"286fe0bd","timestamp":"2026-07-17T00:19:40.042Z","message":{"role":"toolResult","toolCallId":"call_au47Bblo2M03vG7xyl0w8dDW|fc_0ae44a63c9031803016a59751ba9c4819ba1d586bd33ab7b9e","toolName":"grep","content":[{"type":"text","text":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n 65|- `daemon.rs:165-170` — `Broker::bind_in_with_net(...)` returns an `Arc<Broker>`; the daemon `thread::spawn`s `broker.serve()`. `broker.rs:181/196` confirm `bind` returns `Arc<Self>` (an in-process object, not a spawned child).\n*66|- The pump (`daemon.rs:280`), dispatcher, digest hub, net consumers, psyche loops — **all threads in the same process.**\n 67|- A repo-wide search finds **no** `Command`/subcommand that spawns a broker child anywhere.\n 68|\n 69|### 2.3 Why this is a regression, not a deferral\n...\n 191|- `spt daemon run` becomes the **broker** process entry: binds the seed-control channel + broker socket + NetHost + digest hub; holds harness children + the `EffectJournal`; spawns the brain child.\n*192|- A new hidden `spt daemon brain` entry is the **brain** process: connects to the broker, runs pump/dispatch/psyche loops/session drivers, rehydrates from disk, emits a `ready` signal.\n 193|- `ensure_running` / `is_running` / `daemon stop` contracts are unchanged — they target the broker's seed channel.\n 194|- The de-elevation guard (`daemon.rs:60-103`) applies at the **broker** entry; the brain child inherits the unelevated identity.\n 195|\n## DEFERRED.md\n 10|| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |\n*11|| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one...\n 12|| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |\n 13|| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant s...\n 14|| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |\n...\n 33|| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred \"option 1\")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but no...\n*34|| ~~Subnet-scoped liveness probe (REQ-SUBNET-5 detach gap)~~ **SHIPPED 2026-06-08** (REQ-SUBNET-5 int stage): `subnet status --nodes` showed a DETACHED peer as **online** because liveness fell to `Probe` → a raw transport dial by node-pubkey, which succeeded on the peer's still-bound daemon endpoint (one ALPN `spt-core/net/0` serves all subnets, so the dial was subnet-blind). FIX: a new `ServeProbeRecord` (spt-net `serveprobe`) + daemon `serveprobe` handler/requester ask the peer \"serving subnet X?\", a...\n 35|| **Subnet full-mesh visibility (PLANNED MILESTONE — `SUBNET-MESH-PLAN.md`)**: non-directly-paired members never see each other — a subnet is currently the PAIRING GRAPH, not a mesh (A↔B, C↔B; B offline → A and C invisible; even with B online, B doesn't relay). Root: pairwise TOFU trust (`trust.rs`) + own-rows-only gossip / no transitive relay (`peerloop.rs`, KH 4.10/7.5) + no member roster in the pairing seed transfer. FIX = member-AUTHENTICATED subnet data (seed-MAC and/or per-node signatures...\n 36|| `xtask debug-converge` — automated convergence watcher for debug rollout (the REQ-UPD-6 first-slice item ADR-0016 names): watch every expected debug-pinned reachable lab node until all report the target version applied, else a per-node timeout table (Offline / StagedAwaitingConsent / BlockedByBrokerResources / Rejected{reason}). Activates REQ-UPD-6's `int` stage. Full build plan: `docs/DEBUG-CONVERGE-PLAN.md` | REQ-UPD-6 first slice (2026-06-06) | The first slice ships manual \"Apply and observe\" (`DE...\n*37|| **Origin-source update bootstrap (`spt update fetch` — pull the latest signed release from GitHub into the existing verify→stage→apply pipeline)**: today update DISCOVERY is **peer-only** (REQ-UPD-1: a daemon pulls offers from roster peers via `request_update`; `propagate.rs`/`peerloop.rs`). There is NO origin fetch, so the **first node in a fleet — or any isolated/solo node — cannot get a new release without a peer that already staged it** (the fleet converges by gossip, but only *after* a m...\n 38|| **Test broker socket-bind hardening (CI-flake removal)**: `applyhost` tests' `served_broker` helper does `Broker::bind(name).expect(\"bind broker\")` (`crates/spt-daemon/src/applyhost.rs:293`) where `name` = `spt-daemon-d7b-{pid}-{seq}.sock`. Flakes intermittently on the **kitsubito** self-hosted Linux runner (likely AF_UNIX path collision / stale socket file under the runner's reused workspace — Windows named pipes don't hit it). Cost the v0.3.2 release a full red CI run + a `--failed` rerun (2026-06-...\n 39|| **`spt update fetch` single→set shape-upgrade at same version (REQ-UPD-8 transitional trap)**: `cmd_update_fetch` sets the rollback floor = `staged_version` (the monotonic counter), and the gate is a plain `candidate > floor`. A node that fetched+staged version N as a platform **single** (pre-0.3.2 fetch behavior) can never re-fetch version N as a multi-platform **set** — same counter → `Rollback{current:N, candidate:N}`. Bites the **seed-node bootstrap**: the first node to origin-`fetch` a relea...\n 40|| **On-disk `current_exe` reconcile after auto-rollback (the D6-2 reboot residual)**: readiness-gated auto-rollback (ADR-0018 Q7, restoration D6-2) is **record-driven selection** — after a rollback the broker's supervisor keeps spawning the last-known-good `.old-N` binary, chosen from the durable `RolledBack` record, so the rescue survives a reboot for free **with no file rename at the failure instant** (the brittle path the design rejected). But the bytes on disk at `current_exe` are STILL the quarant...\n 41|| **Quarantine operator override (`spt update apply --force` — the note-i escape hatch)**: D6-2's quarantine guard refuses re-applying a version that auto-rolled-back on this node (the `RolledBack` record is the marker; clears naturally when N+1 stages). But a version whose failure was **environmental** (a transient boot condition, not bad bytes) leaves the node **permanently allergic** to a genuinely-good version with no escape but staging a newer one. FIX = an explicit operator override (`spt update ...\n*42|| **✅ RESOLVED — shipped v0.8.3 (REQ-HAZARD-BROKER-QUIC-DEADLINE, KNOWN-HAZARDS 7.8).** ~~Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)~~: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes, by bounding the pump's brain-IPC reads and escalating a TimedOut to a supervised restart. But the ROOT cause is broker-side: the broker's `net_open_stream` / `net_stream_send` / `net_dial` handlers m...\n 43|| **Durable in-daemon alarm scheduler (one-shot deadline machinery — the Q4/V3 deferral)**: the broker/brain restoration (ADR-0018 Q4) fixes the *rule* for one-shot scheduled events — persist the absolute `target-time` at creation; every brain start (update *or* crash) reads it and fires-if-due; **never reset on crash** (a user's \"remind me at 3pm\" is a commitment that must outlive any restart). But it deliberately does **not** build the machinery: the spt-core daemon has **no one-shot consumer today...\n 44|| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **aut...\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 50|| **Always-on endpoint kind + `#`-sigil addressing (ADR-0023; REQ-EP-8 / REQ-EP-9) — the core seam for adapter-bridged group/forum messaging**: a `[always-on]` manifest section declaring a resident, owner-less, addressable endpoint binary the daemon boot-launches + supervises **continuously** (independent of agent liveness, register-triggered, one per `<adapter>[:profile]`), which self-manages its `#`-addressed channel endpoints via the existing `api bind` and may call the already-ungated `endpoint wak...\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n 3|Status: **FROZEN 2026-07-03 — field review complete.** Author: doyle. Inputs:\n*4|perri field seeds + spawn-fail taxonomy (2026-07-03, 16-part dispatch,\n 5|observed-vs-inferred flagged), doyle code recon of the spawn/ready seam\n 6|(anchors below, verified @b6d8357), perri design-review answers Q1–Q4\n 7|(2026-07-03; all four folded in — see §7).\n...\n 90|  - `NO_SESSION` — broker session vanished mid-wait (mode 6 shape)\n*91|- Deadline: **default 60 s** (field-confirmed: real binds land t+5–15 s;\n 92|  every observed bringup exceeding ~20 s was a wedge that never bound — the\n 93|  deadline separates cleanly at ≥30 s; **never configure below 30 s**),\n 94|  overridable per-adapter via a manifest key (the bringup-time contract\n...\n 206|\n*207|1. **Deadline default — ANSWERED:** 60 s safe with margin (real binds t+5–15 s;\n 208|   every >20 s bringup was a wedge, never a slow success). Floor 30 s; per-node\n 209|   override kept for cold-broker first-spawn / loaded boxes. Folded into §3.\n 210|2. **`pre_spawn` on resume — ANSWERED: YES.** Resume re-checks trust (INT-2:\n## F-035-RCA.md\n 3|\n*4|**Status:** RCA-first, **report before fixing** (doyle-dispatched 2026-07-09).\n 5|**REQ:** `REQ-SEND-WINDOW-DRAIN-HONOR` (registered, `required_stages = []` — activate on doyle's fix ruling).\n 6|**Investigator:** todlando. **Off:** `origin/main@2c05dc9` (v0.30.2).\n 7|\n## FAULT-MATRIX.md\n 26|| 7 | **Registry feed: corrupt record in the stream** | That record only — skipped | The rest of the feed (decoder does not wedge) | Lease makes loss safe; next update supersedes | `replicate.rs::decoder_survives_chunk_splits_and_corrupt_lines` |\n*27|| 8 | **Net disabled / endpoint bind fails at boot** | No WAN; net frames answer `enabled:false` / typed error | PTY hosting (daemon degrades to net-less broker) | Fix config, restart daemon | `broker.rs::dispatch_net_status` (None arm); `Daemon::run` degrade path |\n*28|| 9 | **Broker process restart (journal survives, conns don't)** | Live conns/streams lost | Effect journal (durable) | Deduped op whose resource is gone → typed \"retry with a fresh op_id\" error; brain re-dials; an `spt rc` operator viewport RECONNECTS on its own — centered banner, full establish re-drive inside a bounded window, then a plain-language give-up (REQ-RC-RECONNECT) <!-- [doc->REQ-RC-RECONNECT] --> | `broker.rs::dispatch_net_dial` / `dispatch_net_stream_open` restart arms; `rc.rs` reconne...\n 29|| 10 | **WAN msg feed replayed** (sender redrive, or receiver brain restart resubscribing from a stale cursor) | None — replayed records dedup | Spool state (each op exactly one row); already-delivered ops | Durable `wan_seen` op-id claims absorb the replay; spool path claims atomically with the row | `tests/wanmsg.rs::receiver_restart_replays_feed_without_double_delivery`; `spool.rs` wan unit tests |\n 30|| 11 | **WAN msg record forges its origin** (payload `origin_node`/`node` field) | None — forgery inert (unknown field) | Access-gate subject (handshake-proven `remote_id_hex`) | None needed (records carry no origin field by design — KH 7.5) | `tests/wanmsg.rs::wan_message_lands_exactly_once_under_transport_origin`; `wanmsg.rs::forged_origin_field_is_inert` |\n 31|| 12 | **Target brain killed mid-remote-attach** (operator typing into the dead window) | Pumping pauses until handoff | Broker-held QUIC stream, session + child, output log, effect journal; the operator's viewport | Successor re-serves the same stream from seq 0 (worst case): replayed input dedups at the PTY-write journal; re-transmitted output dedups at the operator's render cursor — every byte exactly once | `tests/attach.rs::attach_survives_target_brain_restart_exactly_once` |\n...\n 33|| 14 | **Stream lost mid-context-bundle-sync** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's context store (an incomplete bundle is never fetched — length-gated, scratch file deleted); the responder's store untouched | No resume protocol by design: re-pull fresh — the apply is ancestry-idempotent (an already-joined tip short-circuits; fetched tips quarantine under `refs/spt-sync/` so a partial fetch never clobbers `refs/heads/`) | `tests/sync.rs...\n*34|| 15 | **Partition: concurrent context writes on two nodes** | The file's auto-propagation pauses (conflict surfaced, not propagated) | BOTH versions — local file untouched on each node + the other's version as a durable tracked artifact (hazard 6.6); every other file keeps syncing | Elected reconciler (Active instance's node; lowest-id fallback) runs one bounded Psyche turn → merged write `join(vA,vB)+bump` dominates both parents → propagates as a plain accept, clears artifacts subnet-wide | `test...\n 35|| 16 | **Reconcile turn fails** (harness absent / timeout / garbage or empty output / 6.5-suppressed write) | That file stays in conflict-holding state | Both versions (artifacts + local file) — exactly where they were; the store, the sync loop | Retry at the next sync/activation; a fresher direct write re-merges against the new local version | `spt-live reconcile.rs::failed_turn_preserves_everything` / `suppressed_write_preserves_artifacts`; `turn.rs::empty_stdout_is_an_error` |\n 36|| 17 | **Registry ambiguity during reconciler election** (partition: two nodes both see Active / no Active anywhere) | Two textually-different merges of the same pair may both mint | Neither version is ever lost — merged writes carry joined vectors, so the two merges classify **concurrent** and re-surface as a new conflict pair (detected, not silently last-wins) | Deterministic tiebreak bounds the storm (lowest node id among Actives / non-Offline holders); the re-surfaced pair reconciles on the next pa...\n*37|| 18 | **Compromised relay node serves a tampered update artifact** (bytes corrupted at rest after the relay's own verification) | That one pull ends `Rejected(ArtifactMismatch)` at the puller | The puller's staged release + running binary (nothing staged, nothing applied — no code path from wire bytes to the cache except through `plan_verified`); the rest of the subnet pulls from honest peers | Pull from another peer; the per-node gate (REQ-UPD-2) runs at **every** hop, so one poisoned node never re-p...\n 38|| 19 | **Non-conforming peer offers a rollback/expired/off-channel release** | None — rejected pre-fetch, zero artifact bytes move | The puller's version (monotonic floor holds); the wire (no transfer ever starts) | None needed — the offer-then-fetch shape gates metadata before bytes (REQ-HAZARD-UPDATE-ROLLBACK) | `tests/propagate.rs::rollback_offer_is_rejected_before_any_fetch` |\n 39|| 20 | **Untrusted node queries for the staged update** | None — refused by not offering (fail-closed, the up-to-date shape: learns nothing, not even whether a release exists) | The staged release; the serve loop (one refusal, no state) | None needed; pair the node to entitle it | `tests/propagate.rs::untrusted_origin_gets_no_offer` |\n 40|| 21 | **Stream lost mid-update-artifact** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's staged release (staging is atomic, artifact-then-metadata commit point — a torn pull stages nothing); the scratch file is transient | No resume protocol by design: re-query fresh — the query is idempotent and chunks are positional | torn-pull shape shared with row 14 (`request_update` errors on EOF-before-`Done`); staging atomicity: `relcache.rs::torn_or_cor...\n## FLAKE-LEDGER.md\n 13|| 4 | `spt-live context::write_context_suppresses_stale_llm` | 1× gravity (2026-06-04 run 26967808321) | test calls `write_context` (reads SPT_HOME node identity + epoch stamp) WITHOUT the `with_home` lock — a parallel `with_home` test's env swap + temp-dir teardown races the stamp's epoch write into a deleted home (`NotFound`) | HARDENED (2026-06-04): test wrapped in `with_home` — serializes on the home lock like every other SPT_HOME-touching test |\n*14|| 5 | `spt-daemon attach.rs:600` re-serve (`output gap: got seq 1 want 0`) | 1× kitsubito Linux (2026-06-15 run 27545857429, PR #14 stale-base CI) | re-serve replays the buffered output stream on re-attach; under load the new subscriber observed `seq 1` before `seq 0` — an ordering/timing gap in the re-serve sequence assert, not a content error. Subsystem disjoint from the triggering change (PR #14 = digest-proof CLI key-fill, cannot touch attach sequencing) | OBSERVED 1×: did NOT reproduce on the cl...\n*15|| 6 | `spt-term tests/stream.rs:49 bounded_backpressure_stalls_an_unconsumed_reader` | 1× hfenduleam Windows (2026-06-15 run 27545857429, PR #14 stale-base CI) | asserts an unconsumed bounded stream stalls the writer-side reader; got `16 -> 82` (reader advanced past the bound) — a timing-sensitive backpressure assert racing the bound check under hosted-runner load. Subsystem disjoint from PR #14 (digest-proof CLI) | OBSERVED 1×: did NOT reproduce on the clean-base rerun (green both runners). Not hard...\n 16|| 7 | `spt-daemon::attach attach_survives_target_brain_restart_exactly_once` | 1× kitsubito (2026-06-16 run 27595180782 attempt 1, M11-W3) | **TIMEOUT, not an assertion fail** — SLOW>60>120>180>TERMINATING, nextest TIMEOUT at the 240s cap (`attach.rs:600`). A heavy multi-process brain-restart-exactly-once test on the slow shared kitsubito box under concurrent load (n1-gate on the same runner + a parallel `ci.yml` docs-publish run firing simultaneously). W3-INDEPENDENT: Windows green end-to-end; ALL tu...\n 17|| 8 | 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). H...\n 18|| 9 | `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 b...\n*19|| 10 | `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: gre...\n 20|| 11 | `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...\n 21|| 12 | `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-...\n 22|| 13 | `spt::bin cli::tests::probe_all_cap_batches_into_windows` | 1× hfenduleam local gate (2026-07-06, stack matrix @0f08fa9 leg-3 rerun) | fast unit (0.52s natural) FAILED at 1.7s under the 1614-test full-parallel pool — probe-batching window assert with a timing component starved under load; subsystem disjoint from the triggering stack (F-2 reporting / E-2 grid title / picker rider). Isolated ×5: green, 0.52–0.55s. Same night as #12 — the full-parallel local matrix under a busy box is the com...\n## GATEWAY-LIVENESS-DISPATCH.md\n*1|# Gateway liveness fix — DISPATCH (doyle → todlando, 2026-07-09) — RCA-FIRST\n 2|\n*3|Field bug (flynn, operator's live phone-comms link mobile-gw DOWN). Root LOCALIZED via flynn's 3-probe field data + doyle source trace. **The operator held an earlier dispatch on an incomplete root — this is the reconciled clean root. RCA-first: pin the exact site, report, then fix.**\n 4|\n 5|## Confirmed root — a READER DIVERGENCE (not the earlier psyche-gate theory)\n 6|At the SAME instant, for the SAME pid-alive locally-hosted gateway (mobile-gw, status field ABSENT in info.json, pid alive):\n...\n 15|- So the `--json` Suspended comes from the projection/rendering path dropping the pid-fallback the human local-roster applies. **Candidate sites to pin (STEP 1):**\n*16|  1. `resource_projection` (spt-net registry.rs:566) — does it re-derive/filter liveness (e.g. `is_registry_entry_alive`, status-based: liveness.rs:152, which for a status-ABSENT row = not ONLINE||UNBOUND → stale/offline) and DOWNGRADE the row to Suspended/offline, ignoring the advertised Active? (its test `resource_projection_filters_hidden_and_offline` @registry.rs:1821 says it DOES filter on liveness.)\n 17|  2. OR the registry ROW's advertised status is genuinely Active but the `--json` render maps it wrong.\n 18|  3. OR the human path applies a `reconcile_self_owned` local-truth override for locally-owned endpoints that the `--json` path skips (roster.rs:52 mentions reconcile_self_owned overriding stale subnet gossip).\n 19|- **DEFECT A (separate, CONFIRMED, stands):** `establish_perch`'s build (spt/src/api/startup.rs:214-302) carries `cwd/controllable/adapter/read_env` forward from the prior record but NOT `rest_state` → a re-bind writes a fresh InfoJson that WIPES the wake intent (flynn's tick11 rest_state:active vanish).\n## KNOWN-HAZARDS.md\n 20|- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\n*21|- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\n 22|\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...\n 38|- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.\n*39|- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.\n 40|\n 41|---\n 42|\n...\n 158|- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the \"node\" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the...\n*159|- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future upd...\n 160|- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.\n 161|- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Ghost registry row eviction\"); the AMBIGUOUS render fix rode along.\n 162|- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. \"No transitive gossip\" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So \"any future update for a node comes from that node itself, alive\" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transiti...\n...\n 228|### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`\n*229|- **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 230|- **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 231|- **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 232|- **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...\n 271|<!-- [doc->REQ-HAZARD-ATOMIC-TMP-COLLISION] -->\n*272|### 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file)  `[REQ-HAZARD-ATOMIC-TMP-COLLISION]`\n 273|- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `Bind...\n 274|- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind t...\n 275|- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\n...\n 279|### 5.16 Unlocked whole-record info.json write races a locked RMW → silent lost update  `[REQ-HAZARD-INFO-RMW-LOST-UPDATE]`\n*280|- **Failure:** `mutate_info` serializes its read→mutate→write under the per-perch `.info.lock` sentinel, but `establish_perch` (`spt::api::startup`) did read→conflict-check→`write_info` with **no lock**. At bind the two writers race (~700µs apart): the daemon RMW reads the PRE-BIND record, bind's `write_info` renames the full record in (`state=live_agent`, `controllable=Some(true)`, `session_id`), then the RMW writes its STALE pre-bind snapshot back plus a `status=online` stamp. The surviving `i...\n*281|- **Invariant:** every `info.json` writer serializes under the one per-perch `.info.lock`. A whole-record write (`write_info`) takes the lock exactly as the RMW (`mutate_info`) does — a unique tmp only makes concurrent writes *last-writer-wins*, which is safe ONLY if the writers are serialized (cross-ref 5.15). A multi-step read→check→write (bind's establish) must hold ONE lock acquisition across all three (a true compare-and-set) — check-then-write with the lock dropped in between still interlea...\n 282|- **spt-core mapping:** `spt_store::info` — `write_info` now acquires the sentinel then calls a private `write_info_unlocked` (and `mutate_info` — the public RMW primitive — calls the unlocked writer while holding its own lock, so no double-lock deadlock); `info::establish_locked(perch, build)` runs the bind's read→check→build→write as one locked CAS, called by `spt::api::startup::establish_perch`. The other read-modify-write callers were audited and the two that mutate a load-bearing field w...\n 283|- **Source:** RCA 2026-07-01 (doyle gate rig) — differential (baseline PASS / 77beeac+baseline-atomic PASS / fe385f5 2/3 FAIL) isolated the atomic rework as the delta; `reconcile_hosted_liveness` emitting ZERO `LIVENESS_RECONCILE_OFFLINE` lines for the dead victim over 20s pinned the silent-skip. The unique-tmp fix worked (bringup + serve fine) but exposed this deeper pairing.\n 284|\n...\n 310|- **spt-core mapping:** the daemon is the single writer; the harness-invoked mind never mutates drop files. Bake the ownership into the runtime contract.\n*311|- **Sister cite:** CHANGELOG v1.11.7; `src/live/context.rs:615` (removed delete).\n 312|\n 313|### 6.5 Direct-write precedence guard against stale LLM overwrites\n 314|- **Failure:** the LLM emits an older snapshot that clobbers a fresher direct write to a context file.\n...\n 388|- **Failure:** echo-commune (`run_bounded_stdin`) and the live-Psyche turn driver (D7.5) are **bounded LLM calls that block their calling thread** until the child answers or the timeout fires. Today each agent drives its own pulse from its **own process** (`spt/src/api/{live,startup}.rs`), so blocking is isolated. When the daemon hosts **N per-agent loops** (ADR-0004 target; the `run_pulse_loop` fan-out is currently def+test only), a **single serial driver** that calls these invocations inline lets one a...\n*389|- **Invariant:** each agent's bounded LLM-bearing work (echo-commune summarizer, Psyche turn) runs on its **own thread / off the shared scheduler** — no single-threaded driver iterating all agents may call a blocking invocation inline. One agent's slow/timed-out call must not delay another agent's next tick beyond tolerance.\n 390|- **spt-core mapping:** the daemon's multi-agent pulse/psyche hosting (ADR-0004, \"all Psyche/pulse loops\" consolidated); `run_pulse_loop` fan-out; echo-commune + the D7.5 Psyche driver.\n 391|- **Source:** grill-with-docs 2026-06-03 (forward invariant — the multi-agent fan-out is not yet wired). Distinct from ADR-0002 SERIOUS #6 (crash blast-radius, not scheduling latency).\n 392|\n...\n 403|- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop...\n*404|- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a heal...\n*405|- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the ...\n 406|- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.\n 407|\n 408|### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`\n...\n 416|<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\n*417|- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only v...\n*418|- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`...\n 419|- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIE...\n 420|- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.\n 421|\n...\n 437|- **Failure (hypothesized, v0.12.0):** a legitimately dead PTY child (crash/kill) plus an `rc` pump dropped without a clean detach (closed tab) was thought to make the broker's loopback forward `write_all` block forever on a full 64 KB duplex, park a worker in the 2-worker net runtime, saturate both, and stall every new attach / `endpoint run` (with `daemon stop` unable to join).\n*438|- **Invariant — DISPOSITION = PROVE-DON'T-CHANGE** (doyle GATE-PASS @e883f45, 2026-06-18): the post-L0 code ALREADY prevents the wedge; NO fail-fast / worker-count code was added. (1) `serve_attach` forwards fire-and-forget (`net_stream_send` `op_id=None`) and the broker-side `send_stream` is already deadline-bounded (`bounded_block_on`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s — hazard 7.8), not forever. (2) the loopback duplex is drained broker-INTERNALLY by the operator row's OWN read pump (`nethost.rs` ...\n 439|- **spt-core mapping:** int `crates/spt/tests/attach_wedge_e2e.rs` (REAL detached daemon + dummy-harness fixture): serve the victim (rc sees its tick), abruptly kill rc (dropped pump) + kill the PTY child → a NEW endpoint still comes online + is served (no wedge), the dead endpoint is offlined within a tick (`LIVENESS_RECONCILE_OFFLINE`), `daemon stop` bounded. Leans on 7.8 (the QUIC-op deadline) + 7.7 (viewer isolation).\n 440|- **Source:** v0.12.0 real-harness finding (operator) → v0.12.1 L2 @e883f45; doyle GATE-PASS 2026-06-18.\n 441|\n...\n 455|### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`\n*456|- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two ...\n 457|- **Invariant:** `apply_once` RELEASES the inner lock across `effect()` — reserve the key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable, mark applied). Crash-idempotency comes from the per-key reservation + the applied-set, not from holding the lock across the effect (the brain-crash guarantee is unchanged: the broker survives the brain, so reserve→effect→finalize runs uninterrupted on the broker threa...\n 458|- **spt-core mapping:** impl `effect.rs` `apply_once` (reserve/release/finalize) + `EffectKind::is_durable`; unit (`effect.rs`) a barrier proving the lock is NOT held across `effect()` (a different key's `apply_once` completes while one effect blocks) + `PtyWrite` writes no journal line while a durable kind (`NetSend`) does (the no-fsync proxy); int `crates/spt-daemon/tests/inject_control_wedge.rs` (real broker+PTY, a stalled consumer + sustained blocking input effect: a concurrent `spt rc` attach stays ...\n 459|- **Source:** v0.13.0 W1b (operator dogfooding post-W1 escape; doyle /diagnose + measurement 2026-06-19).\n 460|\n*461|### 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 462|- **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 463|- **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*464|- **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 465|- **spt-core mapping:** impl `livehost.rs` `reconcile_hosted_liveness` Gap-B clear + the additive `SessionInfo.controller_by` observability field (`msg.rs`, populated in `broker.rs` `KIND_SESSIONS`); unit `livehost.rs` `pull_liveness…` extended (offlined sessionless perch clears `driven_by`; live/relay perches untouched); int `crates/spt-daemon/tests/driven_by_selfheal.rs` `gap_b` (real broker: reconcile offlines AND clears `driven_by`) + the A1/A2 characterization (`inject_control_wedge.rs` w5_a1/w5_a2).\n 466|- **Cross-ref 7.29 (who MAY clear vs who MUST NOT):** the BRAIN reconcile here MUST NOT clear `driven_by` off `controller_by==None` (ambiguous with a live LOCAL controller). The BROKER stamp-convergence (7.29) MAY — it reads `has_controller()` DEFINITIVELY (its own controller slot), so it authoritatively clears a stale `driven_by` against a live session with no controller (the `has_controller()==false` leg). The invariant's owner for that leg moved to the broker; this brain-side ambiguity-safety is unc...\n 467|- **Source:** v0.13.0 W5 (repro-first, todlando; doyle-assigned 2026-06-19). A2 (idle wedged-remote leg) deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.\n...\n 485|- **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*486|- **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*487|- **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 488|- **Source:** v0.13.0 P1b (operator HITL re-open; doyle /diagnose on the enhanced rc+broker capture 2026-06-19). The real ship-blocker behind the ctrl+V wedge.\n 489|\n 490|<!-- [doc->REQ-RC-MOUSE-FORWARD] -->\n...\n 499|- **Failure (doyle instrumented RACEDIAG repro, kitsubito):** on a brain-restart re-serve the handoff brain registered as controller on the SAME session TWICE over the SAME socket — `Brain::handoff` eagerly `subscribe(prior.next_seq=1)` → `become_controller(from_seq=1)`, spawning writer-A (writes seq 1); then `serve_attach` re-handled the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, spawning writer-B (writes 0,1). `become_controller` dropped the prior `Co...\n*500|- **Invariant:** on a single brain↔broker connection exactly ONE `controller_writer` is ever the LIVE writer; a SUPERSEDED writer writes no further frames after the epoch bump it observes; and **every `controller_writer` emits a strictly ASCENDING seq stream** (sorted initial batch + ascending live frames). The CORRECTNESS guarantee that falls out: a snap-above consumer over any interleaving of ascending writers — where the surviving writer (`serve_attach`'s `attach_as(sid,0)`) offers the COMPLETE ra...\n 501|- **spt-core mapping:** impl `brain.rs` `Brain::handoff` (KEEP the eager `subscribe`; seed `session_cursors`) + `Brain::subscribe_with` (resume-mode dedup-cursor reset to `from_seq`, shared by `attach`/`attach_as` — the operator-stream boundary fix) + `broker.rs` `OutputLog.controller_epoch: Arc<AtomicU64>` / `become_controller` (atomic `fetch_add`, passes the new epoch + `Arc::clone` into the writer) / `controller_writer` (epoch gate read UNDER the lock on both loops) / `mark_controller_gone` + the `C...\n 502|- **Source:** v0.13.0 P1c (operator-ruled root-fix before ship; doyle root-cause via instrumented repro 2026-06-20; design corrected across two gate rounds — fix #1 reverted, then the kitsubito RACEDIAG pinned the residual to the consumer-side operator-stream boundary, fixed by the `subscribe_with` cursor reset on `attach_as` re-subscribe). The last v0.13.0 ship-blocker.\n 503|\n## MANIFEST.md\n 172|\n*173|Because they carry no session/event context, an adapter can build a path to its **own packed binary** and have spt-core *resolve* it without spt-core ever executing it — spt-core substitutes and **returns** the string; the adapter's own wrapper runs the result (resolve-not-execute; see ADR-0029). The canonical use is hook dispatch: an adapter stores a command template in `[strings]` (below), reads it once per session with `spt adapter get-string`, and runs the resolved command per-hook from a thin stat...\n 174|\n\n[Showing lines 1-202 of 604 (49.9KB limit). Use :203 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":"docs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":128,"fileCount":20,"files":["docs/BROKER-BRAIN-SPLIT-RESTORATION.md","docs/DEFERRED.md","docs/F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md","docs/F-035-RCA.md","docs/FAULT-MATRIX.md","docs/FLAKE-LEDGER.md","docs/GATEWAY-LIVENESS-DISPATCH.md","docs/KNOWN-HAZARDS.md","docs/MANIFEST.md","docs/MESH-RECOVERY-TRIAGE.md","docs/MSG-IDENTITY-DISPATCH.md","docs/MUSL-W1-DISPATCH-RULING.md","docs/MUSL-W3-DISPATCH-RULING.md","docs/NEXT-MILESTONE-BUG-TRIAGE.md","docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","docs/NEXT-MILESTONE-MUSL-TIER-TRIAGE.md","docs/NEXT-MILESTONE-PICKER-POLISH-TRIAGE.md","docs/NEXT-MILESTONE-PICKER-TRIAGE.md","docs/NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md","docs/NEXT-MILESTONE-RUNTRUTH-TRIAGE.md"],"fileMatches":[{"path":"docs/BROKER-BRAIN-SPLIT-RESTORATION.md","count":2},{"path":"docs/DEFERRED.md","count":5},{"path":"docs/F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md","count":3},{"path":"docs/F-035-RCA.md","count":1},{"path":"docs/FAULT-MATRIX.md","count":4},{"path":"docs/FLAKE-LEDGER.md","count":3},{"path":"docs/GATEWAY-LIVENESS-DISPATCH.md","count":3},{"path":"docs/KNOWN-HAZARDS.md","count":20},{"path":"docs/MANIFEST.md","count":4},{"path":"docs/MESH-RECOVERY-TRIAGE.md","count":7},{"path":"docs/MSG-IDENTITY-DISPATCH.md","count":8},{"path":"docs/MUSL-W1-DISPATCH-RULING.md","count":1},{"path":"docs/MUSL-W3-DISPATCH-RULING.md","count":1},{"path":"docs/NEXT-MILESTONE-BUG-TRIAGE.md","count":20},{"path":"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","count":5},{"path":"docs/NEXT-MILESTONE-MUSL-TIER-TRIAGE.md","count":1},{"path":"docs/NEXT-MILESTONE-PICKER-POLISH-TRIAGE.md","count":4},{"path":"docs/NEXT-MILESTONE-PICKER-TRIAGE.md","count":5},{"path":"docs/NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md","count":18},{"path":"docs/NEXT-MILESTONE-RUNTRUTH-TRIAGE.md","count":13}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n  65│- `daemon.rs:165-170` — `Broker::bind_in_with_net(...)` returns an `Arc<Broker>`; the daemon `thread::spawn`s `broker.serve()`. `broker.rs:181/196` confirm `bind` returns `Arc<Self>` (an in-process object, not a spawned child).\n *66│- The pump (`daemon.rs:280`), dispatcher, digest hub, net consumers, psyche loops — **all threads in the same process.**\n  67│- A repo-wide search finds **no** `Command`/subcommand that spawns a broker child anywhere.\n  68│\n  69│### 2.3 Why this is a regression, not a deferral\n    │...\n 191│- `spt daemon run` becomes the **broker** process entry: binds the seed-control channel + broker socket + NetHost + digest hub; holds harness children + the `EffectJournal`; spawns the brain child.\n*192│- A new hidden `spt daemon brain` entry is the **brain** process: connects to the broker, runs pump/dispatch/psyche loops/session drivers, rehydrates from disk, emits a `ready` signal.\n 193│- `ensure_running` / `is_running` / `daemon stop` contracts are unchanged — they target the broker's seed channel.\n 194│- The de-elevation guard (`daemon.rs:60-103`) applies at the **broker** entry; the brain child inherits the unelevated identity.\n 195│\n## DEFERRED.md\n 10│| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |\n*11│| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one...\n 12│| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |\n 13│| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant s...\n 14│| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |\n   │...\n 33│| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred \"option 1\")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but no...\n*34│| ~~Subnet-scoped liveness probe (REQ-SUBNET-5 detach gap)~~ **SHIPPED 2026-06-08** (REQ-SUBNET-5 int stage): `subnet status --nodes` showed a DETACHED peer as **online** because liveness fell to `Probe` → a raw transport dial by node-pubkey, which succeeded on the peer's still-bound daemon endpoint (one ALPN `spt-core/net/0` serves all subnets, so the dial was subnet-blind). FIX: a new `ServeProbeRecord` (spt-net `serveprobe`) + daemon `serveprobe` handler/requester ask the peer \"serving subnet X?\", a...\n 35│| **Subnet full-mesh visibility (PLANNED MILESTONE — `SUBNET-MESH-PLAN.md`)**: non-directly-paired members never see each other — a subnet is currently the PAIRING GRAPH, not a mesh (A↔B, C↔B; B offline → A and C invisible; even with B online, B doesn't relay). Root: pairwise TOFU trust (`trust.rs`) + own-rows-only gossip / no transitive relay (`peerloop.rs`, KH 4.10/7.5) + no member roster in the pairing seed transfer. FIX = member-AUTHENTICATED subnet data (seed-MAC and/or per-node signatures...\n 36│| `xtask debug-converge` — automated convergence watcher for debug rollout (the REQ-UPD-6 first-slice item ADR-0016 names): watch every expected debug-pinned reachable lab node until all report the target version applied, else a per-node timeout table (Offline / StagedAwaitingConsent / BlockedByBrokerResources / Rejected{reason}). Activates REQ-UPD-6's `int` stage. Full build plan: `docs/DEBUG-CONVERGE-PLAN.md` | REQ-UPD-6 first slice (2026-06-06) | The first slice ships manual \"Apply and observe\" (`DE...\n*37│| **Origin-source update bootstrap (`spt update fetch` — pull the latest signed release from GitHub into the existing verify→stage→apply pipeline)**: today update DISCOVERY is **peer-only** (REQ-UPD-1: a daemon pulls offers from roster peers via `request_update`; `propagate.rs`/`peerloop.rs`). There is NO origin fetch, so the **first node in a fleet — or any isolated/solo node — cannot get a new release without a peer that already staged it** (the fleet converges by gossip, but only *after* a m...\n 38│| **Test broker socket-bind hardening (CI-flake removal)**: `applyhost` tests' `served_broker` helper does `Broker::bind(name).expect(\"bind broker\")` (`crates/spt-daemon/src/applyhost.rs:293`) where `name` = `spt-daemon-d7b-{pid}-{seq}.sock`. Flakes intermittently on the **kitsubito** self-hosted Linux runner (likely AF_UNIX path collision / stale socket file under the runner's reused workspace — Windows named pipes don't hit it). Cost the v0.3.2 release a full red CI run + a `--failed` rerun (2026-06-...\n 39│| **`spt update fetch` single→set shape-upgrade at same version (REQ-UPD-8 transitional trap)**: `cmd_update_fetch` sets the rollback floor = `staged_version` (the monotonic counter), and the gate is a plain `candidate > floor`. A node that fetched+staged version N as a platform **single** (pre-0.3.2 fetch behavior) can never re-fetch version N as a multi-platform **set** — same counter → `Rollback{current:N, candidate:N}`. Bites the **seed-node bootstrap**: the first node to origin-`fetch` a relea...\n 40│| **On-disk `current_exe` reconcile after auto-rollback (the D6-2 reboot residual)**: readiness-gated auto-rollback (ADR-0018 Q7, restoration D6-2) is **record-driven selection** — after a rollback the broker's supervisor keeps spawning the last-known-good `.old-N` binary, chosen from the durable `RolledBack` record, so the rescue survives a reboot for free **with no file rename at the failure instant** (the brittle path the design rejected). But the bytes on disk at `current_exe` are STILL the quarant...\n 41│| **Quarantine operator override (`spt update apply --force` — the note-i escape hatch)**: D6-2's quarantine guard refuses re-applying a version that auto-rolled-back on this node (the `RolledBack` record is the marker; clears naturally when N+1 stages). But a version whose failure was **environmental** (a transient boot condition, not bad bytes) leaves the node **permanently allergic** to a genuinely-good version with no escape but staging a newer one. FIX = an explicit operator override (`spt update ...\n*42│| **✅ RESOLVED — shipped v0.8.3 (REQ-HAZARD-BROKER-QUIC-DEADLINE, KNOWN-HAZARDS 7.8).** ~~Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)~~: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes, by bounding the pump's brain-IPC reads and escalating a TimedOut to a supervised restart. But the ROOT cause is broker-side: the broker's `net_open_stream` / `net_stream_send` / `net_dial` handlers m...\n 43│| **Durable in-daemon alarm scheduler (one-shot deadline machinery — the Q4/V3 deferral)**: the broker/brain restoration (ADR-0018 Q4) fixes the *rule* for one-shot scheduled events — persist the absolute `target-time` at creation; every brain start (update *or* crash) reads it and fires-if-due; **never reset on crash** (a user's \"remind me at 3pm\" is a commitment that must outlive any restart). But it deliberately does **not** build the machinery: the spt-core daemon has **no one-shot consumer today...\n 44│| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **aut...\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 50│| **Always-on endpoint kind + `#`-sigil addressing (ADR-0023; REQ-EP-8 / REQ-EP-9) — the core seam for adapter-bridged group/forum messaging**: a `[always-on]` manifest section declaring a resident, owner-less, addressable endpoint binary the daemon boot-launches + supervises **continuously** (independent of agent liveness, register-triggered, one per `<adapter>[:profile]`), which self-manages its `#`-addressed channel endpoints via the existing `api bind` and may call the already-ungated `endpoint wak...\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n   3│Status: **FROZEN 2026-07-03 — field review complete.** Author: doyle. Inputs:\n  *4│perri field seeds + spawn-fail taxonomy (2026-07-03, 16-part dispatch,\n   5│observed-vs-inferred flagged), doyle code recon of the spawn/ready seam\n   6│(anchors below, verified @b6d8357), perri design-review answers Q1–Q4\n   7│(2026-07-03; all four folded in — see §7).\n    │...\n  90│  - `NO_SESSION` — broker session vanished mid-wait (mode 6 shape)\n *91│- Deadline: **default 60 s** (field-confirmed: real binds land t+5–15 s;\n  92│  every observed bringup exceeding ~20 s was a wedge that never bound — the\n  93│  deadline separates cleanly at ≥30 s; **never configure below 30 s**),\n  94│  overridable per-adapter via a manifest key (the bringup-time contract\n    │...\n 206│\n*207│1. **Deadline default — ANSWERED:** 60 s safe with margin (real binds t+5–15 s;\n 208│   every >20 s bringup was a wedge, never a slow success). Floor 30 s; per-node\n 209│   override kept for cold-broker first-spawn / loaded boxes. Folded into §3.\n 210│2. **`pre_spawn` on resume — ANSWERED: YES.** Resume re-checks trust (INT-2:\n## F-035-RCA.md\n 3│\n*4│**Status:** RCA-first, **report before fixing** (doyle-dispatched 2026-07-09).\n 5│**REQ:** `REQ-SEND-WINDOW-DRAIN-HONOR` (registered, `required_stages = []` — activate on doyle's fix ruling).\n 6│**Investigator:** todlando. **Off:** `origin/main@2c05dc9` (v0.30.2).\n 7│\n## FAULT-MATRIX.md\n 26│| 7 | **Registry feed: corrupt record in the stream** | That record only — skipped | The rest of the feed (decoder does not wedge) | Lease makes loss safe; next update supersedes | `replicate.rs::decoder_survives_chunk_splits_and_corrupt_lines` |\n*27│| 8 | **Net disabled / endpoint bind fails at boot** | No WAN; net frames answer `enabled:false` / typed error | PTY hosting (daemon degrades to net-less broker) | Fix config, restart daemon | `broker.rs::dispatch_net_status` (None arm); `Daemon::run` degrade path |\n*28│| 9 | **Broker process restart (journal survives, conns don't)** | Live conns/streams lost | Effect journal (durable) | Deduped op whose resource is gone → typed \"retry with a fresh op_id\" error; brain re-dials; an `spt rc` operator viewport RECONNECTS on its own — centered banner, full establish re-drive inside a bounded window, then a plain-language give-up (REQ-RC-RECONNECT) <!-- [doc->REQ-RC-RECONNECT] --> | `broker.rs::dispatch_net_dial` / `dispatch_net_stream_open` restart arms; `rc.rs` reconne...\n 29│| 10 | **WAN msg feed replayed** (sender redrive, or receiver brain restart resubscribing from a stale cursor) | None — replayed records dedup | Spool state (each op exactly one row); already-delivered ops | Durable `wan_seen` op-id claims absorb the replay; spool path claims atomically with the row | `tests/wanmsg.rs::receiver_restart_replays_feed_without_double_delivery`; `spool.rs` wan unit tests |\n 30│| 11 | **WAN msg record forges its origin** (payload `origin_node`/`node` field) | None — forgery inert (unknown field) | Access-gate subject (handshake-proven `remote_id_hex`) | None needed (records carry no origin field by design — KH 7.5) | `tests/wanmsg.rs::wan_message_lands_exactly_once_under_transport_origin`; `wanmsg.rs::forged_origin_field_is_inert` |\n 31│| 12 | **Target brain killed mid-remote-attach** (operator typing into the dead window) | Pumping pauses until handoff | Broker-held QUIC stream, session + child, output log, effect journal; the operator's viewport | Successor re-serves the same stream from seq 0 (worst case): replayed input dedups at the PTY-write journal; re-transmitted output dedups at the operator's render cursor — every byte exactly once | `tests/attach.rs::attach_survives_target_brain_restart_exactly_once` |\n   │...\n 33│| 14 | **Stream lost mid-context-bundle-sync** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's context store (an incomplete bundle is never fetched — length-gated, scratch file deleted); the responder's store untouched | No resume protocol by design: re-pull fresh — the apply is ancestry-idempotent (an already-joined tip short-circuits; fetched tips quarantine under `refs/spt-sync/` so a partial fetch never clobbers `refs/heads/`) | `tests/sync.rs...\n*34│| 15 | **Partition: concurrent context writes on two nodes** | The file's auto-propagation pauses (conflict surfaced, not propagated) | BOTH versions — local file untouched on each node + the other's version as a durable tracked artifact (hazard 6.6); every other file keeps syncing | Elected reconciler (Active instance's node; lowest-id fallback) runs one bounded Psyche turn → merged write `join(vA,vB)+bump` dominates both parents → propagates as a plain accept, clears artifacts subnet-wide | `test...\n 35│| 16 | **Reconcile turn fails** (harness absent / timeout / garbage or empty output / 6.5-suppressed write) | That file stays in conflict-holding state | Both versions (artifacts + local file) — exactly where they were; the store, the sync loop | Retry at the next sync/activation; a fresher direct write re-merges against the new local version | `spt-live reconcile.rs::failed_turn_preserves_everything` / `suppressed_write_preserves_artifacts`; `turn.rs::empty_stdout_is_an_error` |\n 36│| 17 | **Registry ambiguity during reconciler election** (partition: two nodes both see Active / no Active anywhere) | Two textually-different merges of the same pair may both mint | Neither version is ever lost — merged writes carry joined vectors, so the two merges classify **concurrent** and re-surface as a new conflict pair (detected, not silently last-wins) | Deterministic tiebreak bounds the storm (lowest node id among Actives / non-Offline holders); the re-surfaced pair reconciles on the next pa...\n*37│| 18 | **Compromised relay node serves a tampered update artifact** (bytes corrupted at rest after the relay's own verification) | That one pull ends `Rejected(ArtifactMismatch)` at the puller | The puller's staged release + running binary (nothing staged, nothing applied — no code path from wire bytes to the cache except through `plan_verified`); the rest of the subnet pulls from honest peers | Pull from another peer; the per-node gate (REQ-UPD-2) runs at **every** hop, so one poisoned node never re-p...\n 38│| 19 | **Non-conforming peer offers a rollback/expired/off-channel release** | None — rejected pre-fetch, zero artifact bytes move | The puller's version (monotonic floor holds); the wire (no transfer ever starts) | None needed — the offer-then-fetch shape gates metadata before bytes (REQ-HAZARD-UPDATE-ROLLBACK) | `tests/propagate.rs::rollback_offer_is_rejected_before_any_fetch` |\n 39│| 20 | **Untrusted node queries for the staged update** | None — refused by not offering (fail-closed, the up-to-date shape: learns nothing, not even whether a release exists) | The staged release; the serve loop (one refusal, no state) | None needed; pair the node to entitle it | `tests/propagate.rs::untrusted_origin_gets_no_offer` |\n 40│| 21 | **Stream lost mid-update-artifact** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's staged release (staging is atomic, artifact-then-metadata commit point — a torn pull stages nothing); the scratch file is transient | No resume protocol by design: re-query fresh — the query is idempotent and chunks are positional | torn-pull shape shared with row 14 (`request_update` errors on EOF-before-`Done`); staging atomicity: `relcache.rs::torn_or_cor...\n## FLAKE-LEDGER.md\n 13│| 4 | `spt-live context::write_context_suppresses_stale_llm` | 1× gravity (2026-06-04 run 26967808321) | test calls `write_context` (reads SPT_HOME node identity + epoch stamp) WITHOUT the `with_home` lock — a parallel `with_home` test's env swap + temp-dir teardown races the stamp's epoch write into a deleted home (`NotFound`) | HARDENED (2026-06-04): test wrapped in `with_home` — serializes on the home lock like every other SPT_HOME-touching test |\n*14│| 5 | `spt-daemon attach.rs:600` re-serve (`output gap: got seq 1 want 0`) | 1× kitsubito Linux (2026-06-15 run 27545857429, PR #14 stale-base CI) | re-serve replays the buffered output stream on re-attach; under load the new subscriber observed `seq 1` before `seq 0` — an ordering/timing gap in the re-serve sequence assert, not a content error. Subsystem disjoint from the triggering change (PR #14 = digest-proof CLI key-fill, cannot touch attach sequencing) | OBSERVED 1×: did NOT reproduce on the cl...\n*15│| 6 | `spt-term tests/stream.rs:49 bounded_backpressure_stalls_an_unconsumed_reader` | 1× hfenduleam Windows (2026-06-15 run 27545857429, PR #14 stale-base CI) | asserts an unconsumed bounded stream stalls the writer-side reader; got `16 -> 82` (reader advanced past the bound) — a timing-sensitive backpressure assert racing the bound check under hosted-runner load. Subsystem disjoint from PR #14 (digest-proof CLI) | OBSERVED 1×: did NOT reproduce on the clean-base rerun (green both runners). Not hard...\n 16│| 7 | `spt-daemon::attach attach_survives_target_brain_restart_exactly_once` | 1× kitsubito (2026-06-16 run 27595180782 attempt 1, M11-W3) | **TIMEOUT, not an assertion fail** — SLOW>60>120>180>TERMINATING, nextest TIMEOUT at the 240s cap (`attach.rs:600`). A heavy multi-process brain-restart-exactly-once test on the slow shared kitsubito box under concurrent load (n1-gate on the same runner + a parallel `ci.yml` docs-publish run firing simultaneously). W3-INDEPENDENT: Windows green end-to-end; ALL tu...\n 17│| 8 | 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). H...\n 18│| 9 | `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 b...\n*19│| 10 | `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: gre...\n 20│| 11 | `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...\n 21│| 12 | `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-...\n 22│| 13 | `spt::bin cli::tests::probe_all_cap_batches_into_windows` | 1× hfenduleam local gate (2026-07-06, stack matrix @0f08fa9 leg-3 rerun) | fast unit (0.52s natural) FAILED at 1.7s under the 1614-test full-parallel pool — probe-batching window assert with a timing component starved under load; subsystem disjoint from the triggering stack (F-2 reporting / E-2 grid title / picker rider). Isolated ×5: green, 0.52–0.55s. Same night as #12 — the full-parallel local matrix under a busy box is the com...\n## GATEWAY-LIVENESS-DISPATCH.md\n *1│# Gateway liveness fix — DISPATCH (doyle → todlando, 2026-07-09) — RCA-FIRST\n  2│\n *3│Field bug (flynn, operator's live phone-comms link mobile-gw DOWN). Root LOCALIZED via flynn's 3-probe field data + doyle source trace. **The operator held an earlier dispatch on an incomplete root — this is the reconciled clean root. RCA-first: pin the exact site, report, then fix.**\n  4│\n  5│## Confirmed root — a READER DIVERGENCE (not the earlier psyche-gate theory)\n  6│At the SAME instant, for the SAME pid-alive locally-hosted gateway (mobile-gw, status field ABSENT in info.json, pid alive):\n   │...\n 15│- So the `--json` Suspended comes from the projection/rendering path dropping the pid-fallback the human local-roster applies. **Candidate sites to pin (STEP 1):**\n*16│  1. `resource_projection` (spt-net registry.rs:566) — does it re-derive/filter liveness (e.g. `is_registry_entry_alive`, status-based: liveness.rs:152, which for a status-ABSENT row = not ONLINE||UNBOUND → stale/offline) and DOWNGRADE the row to Suspended/offline, ignoring the advertised Active? (its test `resource_projection_filters_hidden_and_offline` @registry.rs:1821 says it DOES filter on liveness.)\n 17│  2. OR the registry ROW's advertised status is genuinely Active but the `--json` render maps it wrong.\n 18│  3. OR the human path applies a `reconcile_self_owned` local-truth override for locally-owned endpoints that the `--json` path skips (roster.rs:52 mentions reconcile_self_owned overriding stale subnet gossip).\n 19│- **DEFECT A (separate, CONFIRMED, stands):** `establish_perch`'s build (spt/src/api/startup.rs:214-302) carries `cwd/controllable/adapter/read_env` forward from the prior record but NOT `rest_state` → a re-bind writes a fresh InfoJson that WIPES the wake intent (flynn's tick11 rest_state:active vanish).\n## KNOWN-HAZARDS.md\n  20│- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\n *21│- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\n  22│\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    │...\n  38│- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.\n *39│- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.\n  40│\n  41│---\n  42│\n    │...\n 158│- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the \"node\" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the...\n*159│- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future upd...\n 160│- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.\n 161│- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Ghost registry row eviction\"); the AMBIGUOUS render fix rode along.\n 162│- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. \"No transitive gossip\" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So \"any future update for a node comes from that node itself, alive\" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transiti...\n    │...\n 228│### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`\n*229│- **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 230│- **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 231│- **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 232│- **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    │...\n 271│<!-- [doc->REQ-HAZARD-ATOMIC-TMP-COLLISION] -->\n*272│### 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file)  `[REQ-HAZARD-ATOMIC-TMP-COLLISION]`\n 273│- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `Bind...\n 274│- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind t...\n 275│- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\n    │...\n 279│### 5.16 Unlocked whole-record info.json write races a locked RMW → silent lost update  `[REQ-HAZARD-INFO-RMW-LOST-UPDATE]`\n*280│- **Failure:** `mutate_info` serializes its read→mutate→write under the per-perch `.info.lock` sentinel, but `establish_perch` (`spt::api::startup`) did read→conflict-check→`write_info` with **no lock**. At bind the two writers race (~700µs apart): the daemon RMW reads the PRE-BIND record, bind's `write_info` renames the full record in (`state=live_agent`, `controllable=Some(true)`, `session_id`), then the RMW writes its STALE pre-bind snapshot back plus a `status=online` stamp. The surviving `i...\n*281│- **Invariant:** every `info.json` writer serializes under the one per-perch `.info.lock`. A whole-record write (`write_info`) takes the lock exactly as the RMW (`mutate_info`) does — a unique tmp only makes concurrent writes *last-writer-wins*, which is safe ONLY if the writers are serialized (cross-ref 5.15). A multi-step read→check→write (bind's establish) must hold ONE lock acquisition across all three (a true compare-and-set) — check-then-write with the lock dropped in between still interlea...\n 282│- **spt-core mapping:** `spt_store::info` — `write_info` now acquires the sentinel then calls a private `write_info_unlocked` (and `mutate_info` — the public RMW primitive — calls the unlocked writer while holding its own lock, so no double-lock deadlock); `info::establish_locked(perch, build)` runs the bind's read→check→build→write as one locked CAS, called by `spt::api::startup::establish_perch`. The other read-modify-write callers were audited and the two that mutate a load-bearing field w...\n 283│- **Source:** RCA 2026-07-01 (doyle gate rig) — differential (baseline PASS / 77beeac+baseline-atomic PASS / fe385f5 2/3 FAIL) isolated the atomic rework as the delta; `reconcile_hosted_liveness` emitting ZERO `LIVENESS_RECONCILE_OFFLINE` lines for the dead victim over 20s pinned the silent-skip. The unique-tmp fix worked (bringup + serve fine) but exposed this deeper pairing.\n 284│\n    │...\n 310│- **spt-core mapping:** the daemon is the single writer; the harness-invoked mind never mutates drop files. Bake the ownership into the runtime contract.\n*311│- **Sister cite:** CHANGELOG v1.11.7; `src/live/context.rs:615` (removed delete).\n 312│\n 313│### 6.5 Direct-write precedence guard against stale LLM overwrites\n 314│- **Failure:** the LLM emits an older snapshot that clobbers a fresher direct write to a context file.\n    │...\n 388│- **Failure:** echo-commune (`run_bounded_stdin`) and the live-Psyche turn driver (D7.5) are **bounded LLM calls that block their calling thread** until the child answers or the timeout fires. Today each agent drives its own pulse from its **own process** (`spt/src/api/{live,startup}.rs`), so blocking is isolated. When the daemon hosts **N per-agent loops** (ADR-0004 target; the `run_pulse_loop` fan-out is currently def+test only), a **single serial driver** that calls these invocations inline lets one a...\n*389│- **Invariant:** each agent's bounded LLM-bearing work (echo-commune summarizer, Psyche turn) runs on its **own thread / off the shared scheduler** — no single-threaded driver iterating all agents may call a blocking invocation inline. One agent's slow/timed-out call must not delay another agent's next tick beyond tolerance.\n 390│- **spt-core mapping:** the daemon's multi-agent pulse/psyche hosting (ADR-0004, \"all Psyche/pulse loops\" consolidated); `run_pulse_loop` fan-out; echo-commune + the D7.5 Psyche driver.\n 391│- **Source:** grill-with-docs 2026-06-03 (forward invariant — the multi-agent fan-out is not yet wired). Distinct from ADR-0002 SERIOUS #6 (crash blast-radius, not scheduling latency).\n 392│\n    │...\n 403│- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop...\n*404│- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a heal...\n*405│- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the ...\n 406│- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.\n 407│\n 408│### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`\n    │...\n 416│<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\n*417│- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only v...\n*418│- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`...\n 419│- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIE...\n 420│- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.\n 421│\n    │...\n 437│- **Failure (hypothesized, v0.12.0):** a legitimately dead PTY child (crash/kill) plus an `rc` pump dropped without a clean detach (closed tab) was thought to make the broker's loopback forward `write_all` block forever on a full 64 KB duplex, park a worker in the 2-worker net runtime, saturate both, and stall every new attach / `endpoint run` (with `daemon stop` unable to join).\n*438│- **Invariant — DISPOSITION = PROVE-DON'T-CHANGE** (doyle GATE-PASS @e883f45, 2026-06-18): the post-L0 code ALREADY prevents the wedge; NO fail-fast / worker-count code was added. (1) `serve_attach` forwards fire-and-forget (`net_stream_send` `op_id=None`) and the broker-side `send_stream` is already deadline-bounded (`bounded_block_on`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s — hazard 7.8), not forever. (2) the loopback duplex is drained broker-INTERNALLY by the operator row's OWN read pump (`nethost.rs` ...\n 439│- **spt-core mapping:** int `crates/spt/tests/attach_wedge_e2e.rs` (REAL detached daemon + dummy-harness fixture): serve the victim (rc sees its tick), abruptly kill rc (dropped pump) + kill the PTY child → a NEW endpoint still comes online + is served (no wedge), the dead endpoint is offlined within a tick (`LIVENESS_RECONCILE_OFFLINE`), `daemon stop` bounded. Leans on 7.8 (the QUIC-op deadline) + 7.7 (viewer isolation).\n 440│- **Source:** v0.12.0 real-harness finding (operator) → v0.12.1 L2 @e883f45; doyle GATE-PASS 2026-06-18.\n 441│\n    │...\n 455│### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`\n*456│- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two ...\n 457│- **Invariant:** `apply_once` RELEASES the inner lock across `effect()` — reserve the key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable, mark applied). Crash-idempotency comes from the per-key reservation + the applied-set, not from holding the lock across the effect (the brain-crash guarantee is unchanged: the broker survives the brain, so reserve→effect→finalize runs uninterrupted on the broker threa...\n 458│- **spt-core mapping:** impl `effect.rs` `apply_once` (reserve/release/finalize) + `EffectKind::is_durable`; unit (`effect.rs`) a barrier proving the lock is NOT held across `effect()` (a different key's `apply_once` completes while one effect blocks) + `PtyWrite` writes no journal line while a durable kind (`NetSend`) does (the no-fsync proxy); int `crates/spt-daemon/tests/inject_control_wedge.rs` (real broker+PTY, a stalled consumer + sustained blocking input effect: a concurrent `spt rc` attach stays ...\n 459│- **Source:** v0.13.0 W1b (operator dogfooding post-W1 escape; doyle /diagnose + measurement 2026-06-19).\n 460│\n*461│### 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 462│- **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 463│- **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*464│- **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 465│- **spt-core mapping:** impl `livehost.rs` `reconcile_hosted_liveness` Gap-B clear + the additive `SessionInfo.controller_by` observability field (`msg.rs`, populated in `broker.rs` `KIND_SESSIONS`); unit `livehost.rs` `pull_liveness…` extended (offlined sessionless perch clears `driven_by`; live/relay perches untouched); int `crates/spt-daemon/tests/driven_by_selfheal.rs` `gap_b` (real broker: reconcile offlines AND clears `driven_by`) + the A1/A2 characterization (`inject_control_wedge.rs` w5_a1/w5_a2).\n 466│- **Cross-ref 7.29 (who MAY clear vs who MUST NOT):** the BRAIN reconcile here MUST NOT clear `driven_by` off `controller_by==None` (ambiguous with a live LOCAL controller). The BROKER stamp-convergence (7.29) MAY — it reads `has_controller()` DEFINITIVELY (its own controller slot), so it authoritatively clears a stale `driven_by` against a live session with no controller (the `has_controller()==false` leg). The invariant's owner for that leg moved to the broker; this brain-side ambiguity-safety is unc...\n 467│- **Source:** v0.13.0 W5 (repro-first, todlando; doyle-assigned 2026-06-19). A2 (idle wedged-remote leg) deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.\n    │...\n 485│- **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*486│- **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*487│- **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 488│- **Source:** v0.13.0 P1b (operator HITL re-open; doyle /diagnose on the enhanced rc+broker capture 2026-06-19). The real ship-blocker behind the ctrl+V wedge.\n 489│\n 490│<!-- [doc->REQ-RC-MOUSE-FORWARD] -->\n    │...\n 499│- **Failure (doyle instrumented RACEDIAG repro, kitsubito):** on a brain-restart re-serve the handoff brain registered as controller on the SAME session TWICE over the SAME socket — `Brain::handoff` eagerly `subscribe(prior.next_seq=1)` → `become_controller(from_seq=1)`, spawning writer-A (writes seq 1); then `serve_attach` re-handled the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, spawning writer-B (writes 0,1). `become_controller` dropped the prior `Co...\n*500│- **Invariant:** on a single brain↔broker connection exactly ONE `controller_writer` is ever the LIVE writer; a SUPERSEDED writer writes no further frames after the epoch bump it observes; and **every `controller_writer` emits a strictly ASCENDING seq stream** (sorted initial batch + ascending live frames). The CORRECTNESS guarantee that falls out: a snap-above consumer over any interleaving of ascending writers — where the surviving writer (`serve_attach`'s `attach_as(sid,0)`) offers the COMPLETE ra...\n 501│- **spt-core mapping:** impl `brain.rs` `Brain::handoff` (KEEP the eager `subscribe`; seed `session_cursors`) + `Brain::subscribe_with` (resume-mode dedup-cursor reset to `from_seq`, shared by `attach`/`attach_as` — the operator-stream boundary fix) + `broker.rs` `OutputLog.controller_epoch: Arc<AtomicU64>` / `become_controller` (atomic `fetch_add`, passes the new epoch + `Arc::clone` into the writer) / `controller_writer` (epoch gate read UNDER the lock on both loops) / `mark_controller_gone` + the `C...\n 502│- **Source:** v0.13.0 P1c (operator-ruled root-fix before ship; doyle root-cause via instrumented repro 2026-06-20; design corrected across two gate rounds — fix #1 reverted, then the kitsubito RACEDIAG pinned the residual to the consumer-side operator-stream boundary, fixed by the `subscribe_with` cursor reset on `attach_as` re-subscribe). The last v0.13.0 ship-blocker.\n 503│\n## MANIFEST.md\n 172│\n*173│Because they carry no session/event context, an adapter can build a path to its **own packed binary** and have spt-core *resolve* it without spt-core ever executing it — spt-core substitutes and **returns** the string; the adapter's own wrapper runs the result (resolve-not-execute; see ADR-0029). The canonical use is hook dispatch: an adapter stores a command template in `[strings]` (below), reads it once per session with `spt adapter get-string`, and runs the resolved command per-hook from a thin stat...\n 174│\n 175│**Node-static key — `{node}` (since v0.20.0).** Like the adapter-static keys, `{node}` needs no session/event context, so it is available wherever the session-scoped keys populate (both spawn topologies' `[session.*]` templates) **and** in lazy `[strings]` resolution at `get-string` read time. It fills to **this node's advertised label** — the same value the registry advertises and `spt endpoint list` / the picker render (`LABEL (keyprefix…)`), i.e. the node's OS hostname (read into the label store...\n 176│\n    │...\n 326│[strings]\n*327│# adapter stores its own hook-dispatch command, resolved at get-string time\n 328│hook_cmd = \"{adapter_dir}/claude-spt hook\"\n 329│```\n 330│\n*331│A harness hook dispatcher runs `spt adapter get-string claude-spt hook_cmd` **once per session** (e.g. memoized into an env var for a hot-path hook like PostToolUse), getting back the fully-resolved `\"<install_dir>/claude-spt hook\"`, then executes that itself per-hook. spt-core never grows a hook-execution surface — it only resolves (ADR-0029).\n 332│\n 333│### `[inject]` — inject-input methods\n 334│Per activity-state, any combination: `pty`, `hook`, `relay`, `http`.\n    │...\n 362│- **stdin** (spt-core → binary, one JSON object per line): `{\"type\":\"init\",\"endpoint_id\":…,\"node\":…}` first · `{\"type\":\"event\",\"envelope\":\"<EVENT…>\"}` per inbound message (the ADR-0020 envelope) · `{\"type\":\"input\"}` a **content-free** ping each time the operator types (so the binary can track user-idle; the PTY input content is **never** duplicated to the binary).\n*363│- **stdout** (binary → spt-core, one per line): `{\"key\":\"ctrl+s\"}` · `{\"delay_ms\":50}` · `{\"text\":\"<payload>\"}` · `{\"key\":\"enter\"}` · **`{\"commit\":true}`**, … (extensible vocabulary).\n 364│- **`{\"commit\":true}` is the mandatory sequence TERMINATOR, required for EVERY `{\"type\":\"event\"}`.** While a sequence is in flight spt-core buffers the live `spt rc` controller's keystrokes (the *inject floor*) and applies the emitted commands atomically; the trailing `{\"commit\":true}` releases the floor and flushes the buffered controller input *after* the sequence. The submit keystroke is NOT the terminator (a choreography may keep typing after `{\"key\":\"enter\"}`). **An empty response is a protocol viol...\n 365│- **Commit deadline + miss tolerance (F029 C-1, ADR-0022 amendment).** If no `{\"commit\":true}` arrives within the **commit deadline (5 s, `INJECT_COMMIT_DEADLINE`)**, spt-core flushes the buffered operator input (never stranded) and re-spools that message once — but a SINGLE miss does NOT fault a healthy binary (it is preserved; the next event delivers through it). Only **3 consecutive** missed commits, or a real binary death, faults the binary — and a real fault gets a **bounded eager respawn** (a h...\n 366│- **Known keys today:** `command` (preferred) and the deprecated `path`. spt-core does **not** `deny_unknown_fields` here — a newer adapter declaring a future key against an older spt-core parses fine (the unknown key is ignored), so the lifecycle-binary contract degrades gracefully.\n## MESH-RECOVERY-TRIAGE.md\n  12│**Field incident:** HFENDULEAM + ENLYZEAM symmetric sequester (2026-07-10),\n *13│both sides `PUMP_PEER_FAIL:<peer>:submit-dial exceeded the 10s bound` for all\n  14│targets, Tailscale ping ~12ms both directions, both `net_up: true`.\n  15│\n  16│## Seam census (verified current, main @37f0b38)\n    │...\n  22│| Refill requires success | `seedproofx.rs:870-877` (`gapfill_peeraddrs`, fill-only) + `:933-937` (runs on seed-proof exchange only) | recovery requires the connectivity the drop destroyed |\n *23│| Unvalidated cache write | `spt-store/src/peeraddrs.rs:82-90` (`put`) | accepts outer-key/`address.id` mismatch; poison rows persist (live on both incident nodes) |\n *24│| Falsified invariant text | `peeraddrs.rs:14-15,96`, `pump/mod.rs:945` (\"a stale addr never strands a peer\") | premise falsified in field; amend with the fix |\n  25│| Green-while-dead status | `net_up` = local NetHost only; heartbeat written pre-fanout (`pump/mod.rs` round loop); counts = durable membership | incident invisible to every existing surface |\n  26│\n  27│## Build shape — ONE wave (build/mesh-recovery-w1), legs A-D\n    │...\n  48│ALPN, seed-proof send, seed-proof receive/verify, roster exchange — stamped\n *49│(wall+mono) and peer-attributed (folds the 2026-07-14 PUMP_PEER_FAIL-unstamped\n  50│seed). `spt daemon status` / `spt subnet status` report: live peer count,\n  51│last successful peer dial, last admitted registry update, all-peer-failure\n  52│duration. Health = the milestone's acceptance surface, same contract.\n    │...\n  77│4. **Validation is id-match.** A roster or cache address whose `id` ≠ the\n *78│   peer key resolves NOTHING (poison row can never become a route) — unit\n  79│   discriminator required.\n  80│5. **Migration preserves fallback.** The W2 rebuild touches invalid rows\n  81│   only; valid rows and the absent-file/corrupt-degrades-empty behavior are\n    │...\n 109│\n*110│1. **Poison-row repair:** both incident nodes' caches held the `5ff…`-outer\n*111│   poison row class — post-upgrade first load repairs/rejects it, loudly;\n 112│   hertz verifies on the real files.\n 113│2. **Health truth:** induced peer outage (stop a remote daemon) renders\n 114│   degraded status naming the stage; recovery renders healthy on real\n## MSG-IDENTITY-DISPATCH.md\n *1│# MSG-IDENTITY milestone — dispatch (doyle, 2026-07-10)\n  2│\n  3│Operator GO (2026-07-10): *\"next milestone: include those two triages plus the most prominent\n  4│backlog seeds, especially bugs that affect messaging and endpoint identity … once sufficient data\n   │...\n 43│- `REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE` [impl,unit,int] — never delete a drop whose slice\n*44│  wasn't durably committed (ingest.rs:156 empty-project_id gate vs :200 unconditional delete).\n 45│  Distinguish precedence-SUPPRESSED (safe delete) from un-committable-now (keep/preserve).\n 46│  Repro fixture banked (F-032-commune-2026-07-08T222721Z.md). Field-verify: perri.\n 47│\n   │...\n 55│- `REQ-CARRIER-CLAIM-EXCLUSIVE` [impl,unit,int] — **RCA-first**: operator-observed default-window\n*56│  send to BUSY doyle delivered via BOTH poll AND relay (spool row 156 `taken_leg='idle-inject'`).\n 57│  Pin with spool-audit: poll-drain vs relay delivered=1 ordering; busy→idle edge re-offer;\n 58│  take-mark atomicity. Then: one atomic cross-carrier claim per row.\n 59│- `REQ-RELAY-NO-BUSY-DELIVER` [impl,unit] — companion: idle-edge drain re-verifies activity /\n   │...\n 75│\n*76│### W6 — POISON-LOG-SCOPE (F-039; deployah v0.30.6 PASS handoff, **RCA-first**)\n*77│- `REQ-CONN-POISON-DIAL-SCOPE` [impl,unit] — CONN_WRITE_POISONED churn 1:1 with PUMP_PEER_FAIL\n*78│  submit-dials to offline peers; no wedge, pre-existed the rig. Code context: poison_and_cancel\n 79│  (conn.rs:181) logs the SAME loud token for \"exceeded bound **(or failed)**\" — the fast-FAIL\n 80│  branch shares the wedge observable's tag. **RCA-first: pin WHICH broker-conn write fails per\n 81│  pump dial cycle** (fan-out to a departed subscriber? brain-side notification?) — the 1:1\n 82│  correlation mechanism is unpinned. Fix: loud token reserved for DEADLINE-EXCEEDED; fast-fail\n*83│  retires quietly. **Poison/retire behavior unchanged** (REQ-HAZARD-SHAREDSEND invariant holds).\n 84│\n 85│## Sequencing + gates\n 86│\n   │...\n 95│  census = field symptoms across all six waves (dup deliveries, seat-theft, lost communes,\n*96│  bringup dead-ends, gateway-down-after-restart, poison log floods).\n 97│\n 98│<!-- Co-authored by: doyle -->\n## MUSL-W1-DISPATCH-RULING.md\n*1│# MUSL-TIER W1 — DISPATCH RULING (doyle → todlando)\n 2│\n 3│**GO 2026-07-08 (operator \"start now, drive to release publish\").** Branch off `main` @738eb28. Activate `REQ-PLATFORM-REGISTRY` `required_stages = [\"impl\",\"unit\"]` in the work-start commit ([[traceable-per-wave-activation]]).\n 4│\n## MUSL-W3-DISPATCH-RULING.md\n*1│# MUSL-TIER W3 — DISPATCH RULING (doyle → todlando)\n 2│\n 3│**GO 2026-07-08** (operator \"drive to release publish\"). W1 (registry, `b659420`) + W2 (register musl, `7a3ee6e`) both merged to `main`. Branch W3 off `main` @`7a3ee6e`. Activate `REQ-RELEASE-MUSL-ARTIFACT` `required_stages = [\"impl\",\"unit\",\"int\"]` in the work-start commit ([[traceable-per-wave-activation]]) — W3 is the wave that delivers it; it stays `[]` until you start.\n 4│\n## NEXT-MILESTONE-BUG-TRIAGE.md\n   2│\n  *3│> Live tracking doc — operator surfaced 15 issues 2026-06-30 for a follow-up milestone.\n   4│> doyle drives diagnosis (root cause + fix approach), then mints REQs → todlando builds → doyle gates.\n   5│> Status legend: `TRIAGE` (raw) → `DIAGNOSED` (root cause + fix approach) → `PLANNED` (REQ minted) → `BUILT` → `GATED` → `SHIPPED`.\n   6│> Field origin: enlyzeam / sceltouin = remote field nodes; hfenduleam = dev/CI box; win10+pwsh vs win11+WT divergence matters (#12).\n    │...\n   8│## Clusters\n  *9│- **A — registry gossip & presence:** #2, #3, #15\n  10│- **B — cross-node messaging / routing / address-staleness:** #4, #9, #10\n  11│- **C — CLI / picker UX:** #1, #5, #11, #13\n  12│- **D — PTY / terminal rendering:** #6, #7, #8, #12, #14\n    │...\n  27│\n *28│### A-#15 — `dormant` applied to a single-instance endpoint  · STATUS: TRIAGE\n  29│enlyzeam shows sceltouin's endpoint as **Dormant** — but dormant is the multi-instance routing differentiator (a warm non-target sibling). A single detached instance should not read Dormant. (Flagged during v0.17.0 W4 as a known design-Q: `Detach → Dormant` makes a lone detached instance read Dormant — this is that tail.)\n  30│- Root cause: _TBD (resting.rs Detach→Dormant transition; should a lone instance be Active or Suspended?)_\n  31│- Fix approach: _TBD_\n    │...\n  45│- Fix approach: _TBD_\n *46│- Relates: [[broken-ipv6-poisons-iroh-discovery]] (address/path staleness class).\n  47│\n  48│---\n  49│\n    │...\n 100│\n*101│## Diagnosis dispatch\n 102│4 parallel read-only diagnostic agents (one per cluster) launched 2026-06-30. Each returns per-bug {root cause file:line, evidence, fix approach, confidence, effort}. Synthesized back here on report.\n 103│\n 104│---\n    │...\n 123│\n*124│**D-#6 scrollback corrupt** · conf H · effort L · cross-platform. ROOT: broker is raw-byte pump, NO terminal/grid model — OutputLog.ring VecDeque<(seq,Vec<u8>)> (broker.rs:213-216), on attach replays raw bytes from from_seq; cold rc → net_from_seq=0 (attach.rs:198-213)+cursor=0 (rc.rs:1313) → ENTIRE ring dumped into fresh terminal. For alt-screen TUI (CC) history has interleaved ?1049h/l + scroll-region + abs-cursor → flips alt on/off mid-replay → TUI frames into main buffer = corrupt scroll...\n 125│\n*126│**D-#7 residual chars on animate** · conf M · effort M · coupled marker · cross-platform. ROOT: (1) raw passthrough can't clear cells / reconcile ConPTY repaint-dup (stream.rs:11-15); (2) StatusRow injection (rc.rs:1404-1420) splices DECSTBM+DECSC/2K/SGR/DECRC into harness in-flight drawing — DECSC/DECRC unreliable on legacy conhost; scanner runs after writing chunk → banner spliced mid-sequence; reserved scroll-region fights CC's abs-cursor moves. FIX: folds into #6 grid; short-term disable mark...\n 127│\n 128│**D-#8 resize artifacts + marker vanishes** · conf H(marker)/M(artifacts) · effort S-disable/L-right · cross-platform. ROOT: marker is static absolutely-positioned one-shot, repainted only on detected size-change/reassert; resize reflow pushes row-1 banner into scrollback + re-emits dup cells; written-to-buffer not live-overlay → \"only visible if you scroll up\". NOT an axis-swap (size destructure correct). FIX: sticky overlay (needs #6 grid); until then disable.\n 129│\n    │...\n 137│\n*138│**B-#4 cross-node rc can't find remote live session** · conf H · effort M · MISSING-FEATURE. ROOT: rc.rs:1063-1072 `run_attach_inner` cold-starts the LOCAL broker then `resolve_session` (rc.rs:758-765) queries only `brain.sessions()` (local table) → errors \"no live session\" (1070-1072) → RC_FAIL (cli.rs:1294-1299). rc always rides loopback (`net_dial_loopback` rc.rs:1083-1085), never consults registry/resolves owning node. `endpoint list` shows it because it reads gossiped registry snapshots. Tran...\n 139│\n 140│**B-#9/#10 cross-node send SENT(WAN) but no delivery** — TWO issues:\n 141│- **(b) PRIMARY DEFECT — SENT(WAN) is optimistic local-buffer ack, not delivery confirmation** · conf H · effort M. ROOT: wansend.rs:138-149 send is fire-and-forget — net_stream_send → SendHalf::write_all (buffers) + finish() (nethost.rs:370-374 quinn finish() = non-blocking, does NOT await peer stopped() ack) → Ok()=bytes-buffered+locally-finished only → prints SENT(WAN) (cli.rs:4187). Receiver `receive_wan` (wan.rs:133-185) COMPUTES Refused/NoPerch/Duplicate/DeliveredTcp/Spooled but **write...\n*142│- **(a) CONTRIBUTING MISSING-MECHANISM — no address-refresh-on-IP-change; send bypasses warm cache** · conf M · effort L. ROOT: gossiped registry `Instance` carries NO address (wanmsg.rs:104-120) → IP change NEVER re-propagated via gossip; addressing = 100% iroh (mDNS/n0-DNS/relay) + local `PeerAddrStore` (peeraddrs.rs) refreshed ONLY reactively by peer pump `dial_seeded` (pump/mod.rs:694-713); writers = pairing (pairhost.rs:547)/roster gapfill (seedproofx.rs:933)/pump write-back. NO proactive \"my ...\n 143│\n 144│**B cross-cut:** #4 + #9/#10 both want the same primitive: resolve-owning-node → dial-by-id → round-trip-to-that-node's-broker. wansend.rs has the resolve+dial half. **Do #9/#10(b) FIRST** (receiver→sender reply, highest-leverage/lowest-risk, makes failure visible), then #4 cross-node rc reuses the helper, then (a) address-refresh.\n 145│\n    │...\n 149│\n*150│**A-#3 controlled-state not gossiped (driven_by is remote-only BY DESIGN)** · conf H · effort M. ROOT: W5 wiring is correct end-to-end, but the SOURCE is structurally None for a host-local controller. advertise_local reads `info.json.driven_by` (registryhost.rs:437) ✓; picker reads controller_node→Controlled (data.rs:289/model.rs:312) ✓; BUT `driven_by` stamped by broker from `controller_by().by` (broker.rs:630-636/384-386), and `by` = Some(origin_node) ONLY for a REMOTE WAN attach (attach.rs:337...\n 151│\n*152│**A-#15 dormant on lone detached instance — FAITHFUL to CONTEXT, display problem** · conf H · effort S-display/M-model. ROOT: resting.rs:123 `(Active,Detach)=>Dormant` exactly implements CONTEXT.md:283/600 (\"active→dormant: the driver detaches\"); active REQUIRES a driver (CONTEXT:599). NOT an impl defect — it's CONTEXT's internal tension (281 calls active/dormant the \"routing differentiator\" yet 283/600 make Detach→Dormant even for a lone instance). Naive fixes all wrong: Detach→Active breaks...\n 153│\n*154│**A cross-cut:** #2 (stale rows counted) + #3 (missing source datum) are the same family — \"remote viewer derives wrong fact from gossip\" — but NEITHER is a freshness problem (node-refresh helps neither). #15 couples #11 (display).\n 155│\n 156│---\n 157│\n    │...\n 159│- **Quick independent wins (S):** D-#12 (VT-out enable, Win10), C-#5 (node-name label), D-#14 (flag ID-marker OFF — also defuses #7/#8 regression-half), A-#2 primary (routable denominator).\n*160│- **Picker cluster (one ordered build):** C-#11 + C-#13 + A-#15-display — extract shared `ResourceRow→EpDisplay` builder + pub picker enums FIRST, then regroup-by-machine (#13), then endpoint-list reuse (#11) which also fixes #15's label.\n 161│- **Gossip-truth (M):** A-#3 (separate any-controller datum) + A-#2 secondary (Offline-TTL eviction).\n 162│- **WAN cluster (ordered):** B-#9/#10(b) reply-leg FIRST (makes loss visible) → B-#4 cross-node rc (reuse resolve→dial→round-trip) → B-#9/#10(a) address-refresh-on-IP-change (L).\n 163│- **PTY foundational (L):** D-#6 server-side VT grid → unlocks proper sticky marker redo (#8/#14) + kills residual artifacts (#7).\n*164│- **Design-gate first:** A-#15 model-change (if wanted) needs CONTEXT.md amendment; C-#1 decide if install-time runs [update.post].\n 165│\n 166│---\n 167│\n    │...\n 173│`spt rc eel-a` → `RC_FAIL:eel-a: attach request: brain IPC read deadline elapsed`. Worked shortly after creation. **LIVE EVIDENCE (doyle, this box):** endpoint list/daemon show eel-a `ready=true alive=true` (alive is BIND-gated — set at api bind, does NOT reflect live brain health), yet the attach IPC times out → the brain isn't servicing IPC (wedged/dead) while the perch still reads alive. daemon pid 19180. **STRONG LEAD:** the v0.18.0 `spt update apply` daemon/brain RESPAWN (done on this box earl...\n*174│- **Root cause (agent E, conf M-mechanism-H): broker-resident shared-state corruption from the self-update brain-RESPAWN — NOT the brain itself.** The only deadline-bounded call in request_attach is `net_open_stream` (rc.rs:1086, awaits NetStreamOpened under ~10s); error minted brain.rs:1442 = broker's per-conn serve thread sent NO frame for KIND_NET_STREAM_OPEN. DISCRIMINATOR: on the same rc conn, KIND_SESSIONS + KIND_NET_DIAL_LOOPBACK already SUCCEEDED (broker pid 19180 alive, thread reached attach) ...\n 175│\n 176│### G-#17 — no digest for eel-a  · STATUS: SHIPPED CLOSED — W6 env-read @9305d19 (REQ-DIGEST-PROFILE-ENV) + W6b fetcher-strategy @b9e5ce9 (REQ-DIGEST-FETCHER-STRATEGY)\n 177│> **W6** shipped the harness-agnostic `[env] direction=\"read\"` seam (capture-at-bind → info.json.read_env → digest fill_template keys → `value` fallback → `~` expansion → skip-diagnose). **W6b** closed the `{project}` design-gate per doyle's CONTEXT-symmetry ruling: `[digest]` gained a `fetcher` strategy mirroring `[history]`'s — the ADAPTER's extractor locates+reads+emits (no spt-core pre-read, no `{project}` slug), fed only harness-neutral `{session_id}`+`{cwd}`(=info.json.cwd)+captured rea...\n    │...\n 182│`claude-spt 0.9.0→0.9.2`: fetch OK, unsigned-trusting OK, LIVE daemon-coordinated apply, then `ADAPTER_UPDATE_FAIL:claude-spt: re-register: io: The system cannot find the file specified. (os error 2)`. The post-extract re-register step can't find a file — likely the extracted adapter root path or a manifest-referenced binary/install_dir. Class: adapter update ripple / install-dir resolve (cf. F-019 translation install-dir, REQ-HAZARD-DEFERRED-MANIFEST). perri-adjacent (claude-spt v0.9.2).\n*183│- **Root cause (agent G, conf H): SPT-CORE DEFECT (3 coupled), NOT perri packaging, NOT coupled to #16.** Chain: update writes to HARDCODED `_github/{safe}` dest (cli.rs:7674) ignoring the adapter's registered source_dir — claude-spt was plugin/sptc:setup-installed, NOT via `adapter add --release`, so `create_dir_all(dest)` makes a FRESH EMPTY dir. `adapter_has_live_endpoint` (cli.rs:7394/7407) scans on-disk perch info.json parent-prefix → eel-a alive → routes to `apply_release_via_daemon` (ADAPTER...\n 184│\n 185│## REOPENS (new evidence refutes/extends prior diagnosis)\n 186│\n    │...\n 188│NEW: hfenduleam→enlyzeam ALSO failed to deliver, BOTH IPs stable. So address-staleness (prior cause (a)) is NOT the whole story, and the no-reply-leg (b) explains silent-LOSS but not WHY nothing delivers AT ALL. Need a DEEPER dive into the actual delivery path on a stable-IP pair: receiver perch-resolution, access gate (Refused?), spool-not-inject, whether the WAN stream even reaches receive_wan, whether the connection/membership-proof actually establishes. The false-SENT(WAN) (b) stands as a separate ...\n*189│- **Root cause (agent F, conf H):** the REAL drop is the DATA-PATH (candidate #4). `spt send` resolves the dial with id-only `addr_for_node_hex` (wansend.rs:79/207/333 → endpoint.rs:538-541 = EndpointAddr::new(id), ZERO direct paths/relay) → EVERY send forces a fresh iroh DISCOVERY round-trip (mDNS/n0-DNS/relay). The gossip PUMP — which makes the nodes \"mutually visible\" — instead uses `dial_seeded` (pump/mod.rs:694-716): tries the durable last-known direct addr from PeerAddrStore FIRST + caches ...\n*190│- **Fix (conf H, effort M):** route WAN dial through the pump's seeded-direct-addr resolution (PeerAddrStore first, id-only fallback — mirror dial_seeded); PAIR with (b) the ack reply-leg (receiver writes WanOutcome back, sender blocks under QUIC deadline → honest CLI + retry next addr). Contributing/lower-now: proactive addr-refresh on local IP change (a). **Confirm field (read-only):** PeerAddrStore peer-addrs.json WILL hold a good direct addr the send never tries; add a WanOutcome eprintln at disp...\n 191│\n 192│### D-#12 — REOPENED: `rc` garbled but `endpoint run --attach` renders FINE (same Win10/pwsh env)\n 193│NEW: operator confirms `spt endpoint run --attach` renders cleanly in the SAME Win10/raw-pwsh environment; ONLY `spt rc` to a pre-running endpoint is garbled (raw color+cursor codes). This REFUTES \"VT-output never enabled\" (if so, --attach would garble too). Real question: what does the `rc`-to-existing path do DIFFERENTLY from `endpoint run --attach` in terminal/VT setup? Diff the two paths' client terminal-mode init.\n    │...\n 208│**DESIGN-GATE RULINGS (operator 2026-06-30):**\n*209│- **#15 → DISPLAY-ONLY.** Leave resting.rs/CONTEXT.md as-is; fix the lone-instance \"Dormant\" label at the display layer (couples #11). No model change.\n 210│- **#1 → ALSO RUN `[update.post]` at install-time** (not just surface the swallowed error). So `adapter add`'s install-as-first-update should run the composite post-step too, AND surface the error detail.\n 211│\n 212│## CORRECTION 2 (operator, 2026-06-30 — my #17 \"no transcript\" was WRONG)\n    │...\n 220│- **#6:** server-side grid approach confirmed correct.\n*221│- **#15:** display-fix path accepted.\n 222│\n 223│## Backlog (non-blocking, surfaced during the build)\n## NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\n   3│Field-hardening milestone off the 2026-07-06/07 incident night: the v0.27→v0.28 update wedge,\n  *4│the mobile-gw orphan, the claude-spt 0.15.x delivery outage collateral, and the flynn wake\n   5│double-spawn. Every root below is PINNED (rig-reproduced or line-level) — evidence ledger in\n   6│doyle's findings backlog (seeds #5–#10) and this doc's per-wave notes. Owner: doyle (gate),\n   7│executor: todlando. Waves ordered by field pressure × dependency; **mint ALL REQs\n    │...\n  21│Root: `fire_echo` (spt-daemon lifecycle.rs:790) passes the RAW manifest `commune_dir` into\n *22│`run_echo_commune` → echo.rs:115-117 `create_dir_all`+join; relative `.claude` under the\n  23│WMI-launched daemon's System32 cwd → `os error 5` deterministic. Siblings already resolve\n  24│correctly (ingest ~:583, `psyche_drop_file` :1072 via `resolve_endpoint_drop_dir(raw, cwd)`).\n  25│Fix: fire_echo routes through the SAME resolver; relative-with-no-cwd = SKIP LOUD (stderr),\n    │...\n 144│> over `EVENT_LINE_THRESHOLD` (400B, `chunk.rs`) is split into `EVENT-PART` lines;\n*145│> the TERMINAL-SIDE pre-settle window swallows a raw byte span of ~1148–1152B\n 146│> (incl. line framing) and cuts MID part-3 body — a ~20B tail survives, which is\n 147│> exactly why every artifact starts mid-word. Slot math verified on perri's verbatim\n 148│> `seq=1/4` line: first slot is exactly 321B, attrs `seq`/`id`/`type`/`from` only.\n    │...\n 172│**REQ-IDLE-PARKED-DELIVERY** (impl, unit, int)\n*173│Root (live during this very dispatch, 2026-07-07): a message QUEUED to an ALREADY-idle\n 174│endpoint sleeps indefinitely — the idle-edge drain (F-023 leg 2) fires only on the\n 175│ACTIVE→IDLE transition, and no new edge ever comes for a parked session; the send-time inject\n*176│didn't carry it either (both doyle→todlando milestone dispatches sat delivered=0 in the spool\n 177│while the endpoint showed ONLINE). Fix: send-time inject must fire for an already-idle\n 178│spt-hosted target (the activity sense says idle ⇒ inject now, not spool), and/or a bounded\n 179│spool sweep re-offers pending rows to idle endpoints (piggyback the pulse tick, no new loop).\n## NEXT-MILESTONE-MUSL-TIER-TRIAGE.md\n 25│One authoritative platform registry; `current_platform()`, `KNOWN_TARGET_TRIPLES`, the applyhost \"other\" logic, and the asset↔triple map all derive from it. gnu + windows behaviour **byte-identical** — the existing release/update/apply/propagate test suite stays green (that IS the gate).\n*26│- **DESIGN FORK (doyle to rule before dispatch):** (a) registry shape — `enum SupportedPlatform { WindowsMsvc, LinuxGnu, LinuxMusl }` with triple/asset/cfg methods, vs a `SUPPORTED_PLATFORMS: &[PlatformSpec]` const table. Lean: const table of `{ triple, asset_name }` — data-driven, one row per platform, no match-arm sprawl. (b) `applyhost` \"other platform\" — generalize the binary if/else to \"every registered platform except `current_platform()`\" (a filter over the registry). (c) `current_platform()...\n 27│- **Gate:** clippy --workspace, full nextest both legs (kitsubito Linux), traceable; diff review = no behaviour change for gnu/windows (release/update/apply/propagate suites unchanged & green). No new platform yet.\n 28│\n 29│### W2 — Register the musl platform (self-IDENTIFY) — `REQ-PLATFORM-MUSL`\n## NEXT-MILESTONE-PICKER-POLISH-TRIAGE.md\n  18│\"Confirm selection\" top panel.\n *19│**Root (pinned):** `picker/view.rs:415-419` `render_selection_summary` builds the\n  20│line from `ep.project_history.join(\", \")` — the field documented as \"RAW project\n  21│IDs\" (`model.rs:419`). The v0.22.0 A1 display-name machinery\n  22│(`disambiguate_project_ids` over `project_refs[].display`,\n    │...\n 146│the checkpoint ARM's clear-only inject drives `/clear`; the `{commit}` for that\n*147│sequence is never observed within `INJECT_COMMIT_DEADLINE` (5 s, broker.rs:158);\n 148│the inject worker then FAULTS the binary — flush+release floor, respool the\n 149│envelope (broker.rs:1394 `respool_and_fault`), **terminate the translate child,\n 150│latch `Translation.faulted`, worker exits** — and by DESIGN **never respawns**\n*151│(broker.rs:152-157, ADR-0022 amendment). From that instant every force-native\n 152│reports `delivered=false` → the CLI's generic `FORCE_NATIVE_UNDELIVERED: no\n 153│live translation binary` (cli.rs:5046) while the translate *process* the\n 154│operator checks is the terminated one's successor-less absence — B6's exact\n    │...\n 230│  \"eaten commit\" attribution is DEAD. (A) EXCLUDED — all her sequences total\n*231│  ≤~150 ms. (C) EXCLUDED — zero `TRANSLATION_BIN_BAD_LINE`; commit line bytes\n 232│  verified pure-ASCII `{\"commit\":true}\\n`.\n 233│- **C-1 UNHELD — final shape confirmed + two additions:**\n 234│  1. Deadline semantics UNCHANGED (whole-sequence 5 s is fine — (A) excluded).\n## NEXT-MILESTONE-PICKER-TRIAGE.md\n  11│\n *12│## #1 — Details panel shows ONE wrong project (\"f015b-probe-psyche\") for `hall-a`\n  13│\n  14│**Answer to the operator's purge question first: hall-a was NOT purged.** Its\n  15│`sessions.log` is continuous since creation (2026-07-02 08:12Z) and holds the full story:\n    │...\n  20│ordinal 4  boot   cwd=...\\spt-core\\owlery\\hall-a\\nested\\hall-a-psyche\n *21│ordinal 5  boot   cwd=...\\owlery\\f015b-probe\\nested\\f015b-probe-psyche   <- current info.json cwd\n  22│```\n  23│\n  24│**Root cause — three stacked defects** (`crates/spt/src/picker/data.rs`):\n    │...\n  31│  which is **rewritten on every rebind** — it is \"latest boot cwd\", not origin. hall-a's\n *32│  latest rebind was into the f015b-probe **psyche** dir, so that's what displays.\n  33│- **1c.** Psyche-host sessions bind into the agent's perch with owlery-internal cwds\n  34│  (`owlery\\*\\nested\\*-psyche`) — internal dirs pollute the cwd history and get presented\n  35│  as \"projects\".\n    │...\n 107│**Local half (code bug, both machines):** `display_status()`\n*108│(`crates/spt/src/picker/model.rs:415`) derives Controlled **only** from\n*109│`driven_by.is_some()` — and `driven_by` is REMOTE-only by design (KH 7.15). A\n 110│locally-controlled endpoint has `driven_by=None` + `controlled=true`, and `local_rows`\n 111│(data.rs:220) never threads `controlled` into `EndpointRow` — so a locally-RC'd endpoint\n 112│renders plain ONLINE in its own node's picker. Remote rows DON'T have this hole (gossip\n## NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md\n 148│\n*149│**B-1 — poison-proof the broker floor locks (the wedge itself; top-ranked\n 150│root).** Brain-only update keeps broker + QUIC conns + ControllerSink alive\n*151│by design (REQ-UPD-3); E-#16 fixed journal poison + loopback bound; the\n 152│surviving class is the inject/controller floor Mutexes with bare\n 153│`.lock().unwrap()` — broker.rs:1163 (flush_inject_floor), :1297 (inject\n 154│worker open), :2142 (buffer_if_held). One panic under a shared floor lock\n*155│during attach churn permanently wedges serve/dispatch → every attach\n*156│deadlines (`brain IPC read deadline elapsed`, brain.rs:1505) + pump tick\n 157│stalls behind the same thread. This also matches the banked B5 churn-pump\n 158│observation. Fix: `lock_recover`-style conversion (mirror effect.rs:207).\n 159│Red-first: inject_control_wedge.rs — scripted panic-under-floor-lock during\n 160│concurrent attach; next attach must still open. KNOWN-HAZARDS entry rider\n*161│(brain-only update + poisoned broker-resident lock strands serve/attach).\n 162│**B-1 PIVOT (doyle ruling, 2026-07-05, build sweep — 4th triage-drift this\n*163│wave):** the 3 named floor sites were ALREADY poison-proofed post-triage\n*164│(`lock_floor()` recover helper, REQ-HAZARD-INJECT-WORKER-POISON, red-first\n 165│covered) — B-1-as-specified is closed. The SURVIVING members of the same\n 166│hazard class (a panic under a broker-resident lock during brain-only-update\n*167│churn permanently wedges serve/attach) are the attach/serve/dispatch-path\n 168│bare `.lock().unwrap()`s: `sessions` Mutex<HashMap> ×18, per-session\n*169│`OutputLog` Mutex ×7, `pair_holds` Mutex ×4. B-1 pivots to poison-proof\n 170│ALL THREE via one shared `lock_recover` helper (the inject-floor pattern\n 171│generalized). **Ring coherence bind:** `OutputLog`'s ring append mutates\n 172│counters — recover-then-serve of a mid-append-torn ring could hand garbage\n    │...\n 177│ops (coherent-on-recovery, same argument as the floor/journal). The\n*178│REQ-HAZARD-BROKER-FLOOR-LOCK-POISON seed is REDIRECTED to this class in\n 179│the same commit (redirect-folded-req-seeds discipline): floor half noted\n*180│subsumed by REQ-HAZARD-INJECT-WORKER-POISON, title recast to the\n 181│attach-path lock set. Red-first unchanged in spirit: scripted\n 182│panic-under-sessions-lock (and a torn-ring variant) during concurrent\n 183│attach; next attach must still open and serve sane bytes.\n    │...\n 190│(broker.rs:635 seam + mark_controller_gone :645), boot-tick + detach-detect\n*191│triggers. KH 7.15 guard: reap only — never latch `driven_by` for a local\n 192│controller. Red-first: sever a controller stream across a brain restart →\n 193│stamps clear within a reconcile tick.\n 194│**B-2 ruling addendum (doyle, 2026-07-05, build recon — the triage's \"detect\n    │...\n 201│Wire-level severance detection stays with the EXISTING paths (conn EOF,\n*202│unsubscribe, drain deadline-evict) — B-1's poison fix un-wedged exactly the\n 203│cleanup that failed on ball-b, and QUIC keepalive surfaces an idle-severed\n 204│conn as an EOF eventually. Fix shape (recorded-truth, Q5 kin):\n 205│- Broker gains an authoritative **live-controller query** per session id\n    │...\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    │...\n 244│\n*245│**REQ seeds:** `REQ-HAZARD-BROKER-FLOOR-LOCK-POISON` (B-1),\n 246│`REQ-CONTROLLER-LIVENESS-REAP` (B-2), `REQ-RC-RECONNECT` (B-3).\n 247│\n 248│## C — Picker cluster (operator #1, #4, #8, #9)\n    │...\n 344│so the v0.22.0 ancestry fix's surviving hole is the root: self-detect leg (c)\n*345│matches `rec.pid` only (roster.rs:153) — for an spt-hosted endpoint that pid\n 346│is the ephemeral bind-CLI pid, ALREADY DEAD (the F-026 #11 dead-pid class,\n 347│sighted on hall-bf today) — never in any sender's ancestry. `rec.parent_pid`\n 348│holds the harness pid (startup.rs:237), IS the ancestor, and CONTEXT names it\n    │...\n 357│**E-2 — rc title replay (#2).** Root (certain): `GridState` has no\n*358│`osc_dispatch` (vte default drops OSC 0/2) and `render_repaint` emits no\n 359│title (screen.rs:687/571) — cold attach paints a perfect screen with a stale\n 360│title. Fix: `title: Option<String>` on the grid, capture OSC 0/2, clear on\n 361│RIS, emit `\\x1b]2;{title}\\x07` in repaint; flows through repaint_initial\n    │...\n 386│  down.\n*387│- **F-4 docs:** KNOWN-HAZARDS entries (B-1 floor-lock poison; A-1 effective\n 388│  instance state as hazard-class), ADR-0031 amendment (E-2), ADR-0033/0034\n 389│  land with this doc.\n 390│- **F-5 adapter floor NOT enforced on acquisition (perri finding, public-repro\n## NEXT-MILESTONE-RUNTRUTH-TRIAGE.md\n  24│(picker/model.rs:346) suffix mechanism for collisions. Applies to confirm-panel\n *25│history (view.rs:415-419), choose-project labels (model.rs:314-337), resume-row\n  26│titles, and the `endpoint list` project column (shared source:\n  27│`latest_project_ref`, picker/data.rs:417).\n  28│\n    │...\n  74│duplicate; (ii) RCA the attach/output routing that let frames cross\n *75│same-id sessions (broker session-slot keying, dispatch_adapter vs serve_attach\n  76│resolution) and lock it with an int test: two sessions one endpoint id →\n  77│each attach sees only its own frames.\n  78│\n    │...\n 127│Evidence: hall-a's ledger (ordinals 4–5) holds its own psyche's session AND\n*128│f015b-probe-psyche's session; worse, hall-a's `info.json.session_id` IS\n*129│f015b-probe-psyche's session (`359d7bd7…`) — the foreign psyche's SessionStart\n 130│hook stamped hall-a's ledger AND rebound hall-a's identity record to itself.\n*131│f015b-probe's own ledger has neither psyche session. Same class as hall-b's\n*132│dead-pid `info.json` stamp (141556) seen in the attach-stall diagnosis.\n 133│Three verified defects compose:\n 134│  (1) `roster::detect_self_id` leg (a) scans the owlery ONE level deep\n 135│      (roster.rs:107 `read_dir(owlery)`) — a NESTED psyche perch\n    │...\n 256│only on harness idle/turn events (none occurred). Repro sketch: bind an\n*257│endpoint, leave it turn-less through several dormant/wake cycles ~15 min,\n 258│then any `--force-native` send while the translate pid is verifiably alive.\n 259│\n 260│#### RCA (todlando 2026-07-03, code-trace; the \"liveness decay\" framing is a\n    │...\n 281│  active endpoint, no exit in the window ⇒ the session persists and\n*282│  `dispatch_endpoint_input` finds it.\n 283│- **LEADING (H4): a PANIC in `run_inject_worker` orphans the child + strands\n 284│  delivery.** The floor `Mutex` is locked via bare `.lock().unwrap()` at THREE\n*285│  sites that a POISON cascades across: the inject worker (broker.rs:1297,\n 286│  `open()`), `flush_inject_floor` (1163), and — crucially — the CONTROLLER-input\n*287│  path `dispatch_input` Layer C (2142, `buffer_if_held`). The repro's \"two rc\n 288│  nudges\" are controller input through 2142. A panic under the floor lock on\n*289│  ANY of these (a slot the controller and the inject worker SHARE) POISONS the\n 290│  mutex; the worker's next `floor.lock().unwrap()` then panics, the worker thread\n 291│  dies WITHOUT running its fault/terminate path, so `event_rx` drops but the\n 292│  child is NEVER reaped (pid stays alive) and `event_tx.send` now fails →\n*293│  `dispatch_endpoint_input` reports the \"worker-gone\" delivered=false →\n 294│  force-native refused, PERMANENTLY (a faulted/dead worker never respawns).\n 295│  Matches EVERY pinned constant (same daemon, same LIVE pid, un-faulted, no\n 296│  exit, no adapter-apply, active endpoint, rc-nudge involvement) and the \"wake\n    │...\n 304│by bug #16 / REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE): (i) make the floor lock\n*305│poison-tolerant (`.lock().unwrap_or_else(|p| p.into_inner())`) at all three\n 306│sites so one panic can't brick the worker; (ii) make `run_inject_worker`\n 307│panic-resilient (catch_unwind → fault+terminate the child on a worker panic, so\n 308│a dead worker never orphans a live binary + strands force-native forever).","truncation":{"content":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n 65|- `daemon.rs:165-170` — `Broker::bind_in_with_net(...)` returns an `Arc<Broker>`; the daemon `thread::spawn`s `broker.serve()`. `broker.rs:181/196` confirm `bind` returns `Arc<Self>` (an in-process object, not a spawned child).\n*66|- The pump (`daemon.rs:280`), dispatcher, digest hub, net consumers, psyche loops — **all threads in the same process.**\n 67|- A repo-wide search finds **no** `Command`/subcommand that spawns a broker child anywhere.\n 68|\n 69|### 2.3 Why this is a regression, not a deferral\n...\n 191|- `spt daemon run` becomes the **broker** process entry: binds the seed-control channel + broker socket + NetHost + digest hub; holds harness children + the `EffectJournal`; spawns the brain child.\n*192|- A new hidden `spt daemon brain` entry is the **brain** process: connects to the broker, runs pump/dispatch/psyche loops/session drivers, rehydrates from disk, emits a `ready` signal.\n 193|- `ensure_running` / `is_running` / `daemon stop` contracts are unchanged — they target the broker's seed channel.\n 194|- The de-elevation guard (`daemon.rs:60-103`) applies at the **broker** entry; the brain child inherits the unelevated identity.\n 195|\n## DEFERRED.md\n 10|| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |\n*11|| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one...\n 12|| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |\n 13|| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant s...\n 14|| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |\n...\n 33|| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred \"option 1\")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but no...\n*34|| ~~Subnet-scoped liveness probe (REQ-SUBNET-5 detach gap)~~ **SHIPPED 2026-06-08** (REQ-SUBNET-5 int stage): `subnet status --nodes` showed a DETACHED peer as **online** because liveness fell to `Probe` → a raw transport dial by node-pubkey, which succeeded on the peer's still-bound daemon endpoint (one ALPN `spt-core/net/0` serves all subnets, so the dial was subnet-blind). FIX: a new `ServeProbeRecord` (spt-net `serveprobe`) + daemon `serveprobe` handler/requester ask the peer \"serving subnet X?\", a...\n 35|| **Subnet full-mesh visibility (PLANNED MILESTONE — `SUBNET-MESH-PLAN.md`)**: non-directly-paired members never see each other — a subnet is currently the PAIRING GRAPH, not a mesh (A↔B, C↔B; B offline → A and C invisible; even with B online, B doesn't relay). Root: pairwise TOFU trust (`trust.rs`) + own-rows-only gossip / no transitive relay (`peerloop.rs`, KH 4.10/7.5) + no member roster in the pairing seed transfer. FIX = member-AUTHENTICATED subnet data (seed-MAC and/or per-node signatures...\n 36|| `xtask debug-converge` — automated convergence watcher for debug rollout (the REQ-UPD-6 first-slice item ADR-0016 names): watch every expected debug-pinned reachable lab node until all report the target version applied, else a per-node timeout table (Offline / StagedAwaitingConsent / BlockedByBrokerResources / Rejected{reason}). Activates REQ-UPD-6's `int` stage. Full build plan: `docs/DEBUG-CONVERGE-PLAN.md` | REQ-UPD-6 first slice (2026-06-06) | The first slice ships manual \"Apply and observe\" (`DE...\n*37|| **Origin-source update bootstrap (`spt update fetch` — pull the latest signed release from GitHub into the existing verify→stage→apply pipeline)**: today update DISCOVERY is **peer-only** (REQ-UPD-1: a daemon pulls offers from roster peers via `request_update`; `propagate.rs`/`peerloop.rs`). There is NO origin fetch, so the **first node in a fleet — or any isolated/solo node — cannot get a new release without a peer that already staged it** (the fleet converges by gossip, but only *after* a m...\n 38|| **Test broker socket-bind hardening (CI-flake removal)**: `applyhost` tests' `served_broker` helper does `Broker::bind(name).expect(\"bind broker\")` (`crates/spt-daemon/src/applyhost.rs:293`) where `name` = `spt-daemon-d7b-{pid}-{seq}.sock`. Flakes intermittently on the **kitsubito** self-hosted Linux runner (likely AF_UNIX path collision / stale socket file under the runner's reused workspace — Windows named pipes don't hit it). Cost the v0.3.2 release a full red CI run + a `--failed` rerun (2026-06-...\n 39|| **`spt update fetch` single→set shape-upgrade at same version (REQ-UPD-8 transitional trap)**: `cmd_update_fetch` sets the rollback floor = `staged_version` (the monotonic counter), and the gate is a plain `candidate > floor`. A node that fetched+staged version N as a platform **single** (pre-0.3.2 fetch behavior) can never re-fetch version N as a multi-platform **set** — same counter → `Rollback{current:N, candidate:N}`. Bites the **seed-node bootstrap**: the first node to origin-`fetch` a relea...\n 40|| **On-disk `current_exe` reconcile after auto-rollback (the D6-2 reboot residual)**: readiness-gated auto-rollback (ADR-0018 Q7, restoration D6-2) is **record-driven selection** — after a rollback the broker's supervisor keeps spawning the last-known-good `.old-N` binary, chosen from the durable `RolledBack` record, so the rescue survives a reboot for free **with no file rename at the failure instant** (the brittle path the design rejected). But the bytes on disk at `current_exe` are STILL the quarant...\n 41|| **Quarantine operator override (`spt update apply --force` — the note-i escape hatch)**: D6-2's quarantine guard refuses re-applying a version that auto-rolled-back on this node (the `RolledBack` record is the marker; clears naturally when N+1 stages). But a version whose failure was **environmental** (a transient boot condition, not bad bytes) leaves the node **permanently allergic** to a genuinely-good version with no escape but staging a newer one. FIX = an explicit operator override (`spt update ...\n*42|| **✅ RESOLVED — shipped v0.8.3 (REQ-HAZARD-BROKER-QUIC-DEADLINE, KNOWN-HAZARDS 7.8).** ~~Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)~~: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes, by bounding the pump's brain-IPC reads and escalating a TimedOut to a supervised restart. But the ROOT cause is broker-side: the broker's `net_open_stream` / `net_stream_send` / `net_dial` handlers m...\n 43|| **Durable in-daemon alarm scheduler (one-shot deadline machinery — the Q4/V3 deferral)**: the broker/brain restoration (ADR-0018 Q4) fixes the *rule* for one-shot scheduled events — persist the absolute `target-time` at creation; every brain start (update *or* crash) reads it and fires-if-due; **never reset on crash** (a user's \"remind me at 3pm\" is a commitment that must outlive any restart). But it deliberately does **not** build the machinery: the spt-core daemon has **no one-shot consumer today...\n 44|| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **aut...\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 50|| **Always-on endpoint kind + `#`-sigil addressing (ADR-0023; REQ-EP-8 / REQ-EP-9) — the core seam for adapter-bridged group/forum messaging**: a `[always-on]` manifest section declaring a resident, owner-less, addressable endpoint binary the daemon boot-launches + supervises **continuously** (independent of agent liveness, register-triggered, one per `<adapter>[:profile]`), which self-manages its `#`-addressed channel endpoints via the existing `api bind` and may call the already-ungated `endpoint wak...\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n 3|Status: **FROZEN 2026-07-03 — field review complete.** Author: doyle. Inputs:\n*4|perri field seeds + spawn-fail taxonomy (2026-07-03, 16-part dispatch,\n 5|observed-vs-inferred flagged), doyle code recon of the spawn/ready seam\n 6|(anchors below, verified @b6d8357), perri design-review answers Q1–Q4\n 7|(2026-07-03; all four folded in — see §7).\n...\n 90|  - `NO_SESSION` — broker session vanished mid-wait (mode 6 shape)\n*91|- Deadline: **default 60 s** (field-confirmed: real binds land t+5–15 s;\n 92|  every observed bringup exceeding ~20 s was a wedge that never bound — the\n 93|  deadline separates cleanly at ≥30 s; **never configure below 30 s**),\n 94|  overridable per-adapter via a manifest key (the bringup-time contract\n...\n 206|\n*207|1. **Deadline default — ANSWERED:** 60 s safe with margin (real binds t+5–15 s;\n 208|   every >20 s bringup was a wedge, never a slow success). Floor 30 s; per-node\n 209|   override kept for cold-broker first-spawn / loaded boxes. Folded into §3.\n 210|2. **`pre_spawn` on resume — ANSWERED: YES.** Resume re-checks trust (INT-2:\n## F-035-RCA.md\n 3|\n*4|**Status:** RCA-first, **report before fixing** (doyle-dispatched 2026-07-09).\n 5|**REQ:** `REQ-SEND-WINDOW-DRAIN-HONOR` (registered, `required_stages = []` — activate on doyle's fix ruling).\n 6|**Investigator:** todlando. **Off:** `origin/main@2c05dc9` (v0.30.2).\n 7|\n## FAULT-MATRIX.md\n 26|| 7 | **Registry feed: corrupt record in the stream** | That record only — skipped | The rest of the feed (decoder does not wedge) | Lease makes loss safe; next update supersedes | `replicate.rs::decoder_survives_chunk_splits_and_corrupt_lines` |\n*27|| 8 | **Net disabled / endpoint bind fails at boot** | No WAN; net frames answer `enabled:false` / typed error | PTY hosting (daemon degrades to net-less broker) | Fix config, restart daemon | `broker.rs::dispatch_net_status` (None arm); `Daemon::run` degrade path |\n*28|| 9 | **Broker process restart (journal survives, conns don't)** | Live conns/streams lost | Effect journal (durable) | Deduped op whose resource is gone → typed \"retry with a fresh op_id\" error; brain re-dials; an `spt rc` operator viewport RECONNECTS on its own — centered banner, full establish re-drive inside a bounded window, then a plain-language give-up (REQ-RC-RECONNECT) <!-- [doc->REQ-RC-RECONNECT] --> | `broker.rs::dispatch_net_dial` / `dispatch_net_stream_open` restart arms; `rc.rs` reconne...\n 29|| 10 | **WAN msg feed replayed** (sender redrive, or receiver brain restart resubscribing from a stale cursor) | None — replayed records dedup | Spool state (each op exactly one row); already-delivered ops | Durable `wan_seen` op-id claims absorb the replay; spool path claims atomically with the row | `tests/wanmsg.rs::receiver_restart_replays_feed_without_double_delivery`; `spool.rs` wan unit tests |\n 30|| 11 | **WAN msg record forges its origin** (payload `origin_node`/`node` field) | None — forgery inert (unknown field) | Access-gate subject (handshake-proven `remote_id_hex`) | None needed (records carry no origin field by design — KH 7.5) | `tests/wanmsg.rs::wan_message_lands_exactly_once_under_transport_origin`; `wanmsg.rs::forged_origin_field_is_inert` |\n 31|| 12 | **Target brain killed mid-remote-attach** (operator typing into the dead window) | Pumping pauses until handoff | Broker-held QUIC stream, session + child, output log, effect journal; the operator's viewport | Successor re-serves the same stream from seq 0 (worst case): replayed input dedups at the PTY-write journal; re-transmitted output dedups at the operator's render cursor — every byte exactly once | `tests/attach.rs::attach_survives_target_brain_restart_exactly_once` |\n...\n 33|| 14 | **Stream lost mid-context-bundle-sync** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's context store (an incomplete bundle is never fetched — length-gated, scratch file deleted); the responder's store untouched | No resume protocol by design: re-pull fresh — the apply is ancestry-idempotent (an already-joined tip short-circuits; fetched tips quarantine under `refs/spt-sync/` so a partial fetch never clobbers `refs/heads/`) | `tests/sync.rs...\n*34|| 15 | **Partition: concurrent context writes on two nodes** | The file's auto-propagation pauses (conflict surfaced, not propagated) | BOTH versions — local file untouched on each node + the other's version as a durable tracked artifact (hazard 6.6); every other file keeps syncing | Elected reconciler (Active instance's node; lowest-id fallback) runs one bounded Psyche turn → merged write `join(vA,vB)+bump` dominates both parents → propagates as a plain accept, clears artifacts subnet-wide | `test...\n 35|| 16 | **Reconcile turn fails** (harness absent / timeout / garbage or empty output / 6.5-suppressed write) | That file stays in conflict-holding state | Both versions (artifacts + local file) — exactly where they were; the store, the sync loop | Retry at the next sync/activation; a fresher direct write re-merges against the new local version | `spt-live reconcile.rs::failed_turn_preserves_everything` / `suppressed_write_preserves_artifacts`; `turn.rs::empty_stdout_is_an_error` |\n 36|| 17 | **Registry ambiguity during reconciler election** (partition: two nodes both see Active / no Active anywhere) | Two textually-different merges of the same pair may both mint | Neither version is ever lost — merged writes carry joined vectors, so the two merges classify **concurrent** and re-surface as a new conflict pair (detected, not silently last-wins) | Deterministic tiebreak bounds the storm (lowest node id among Actives / non-Offline holders); the re-surfaced pair reconciles on the next pa...\n*37|| 18 | **Compromised relay node serves a tampered update artifact** (bytes corrupted at rest after the relay's own verification) | That one pull ends `Rejected(ArtifactMismatch)` at the puller | The puller's staged release + running binary (nothing staged, nothing applied — no code path from wire bytes to the cache except through `plan_verified`); the rest of the subnet pulls from honest peers | Pull from another peer; the per-node gate (REQ-UPD-2) runs at **every** hop, so one poisoned node never re-p...\n 38|| 19 | **Non-conforming peer offers a rollback/expired/off-channel release** | None — rejected pre-fetch, zero artifact bytes move | The puller's version (monotonic floor holds); the wire (no transfer ever starts) | None needed — the offer-then-fetch shape gates metadata before bytes (REQ-HAZARD-UPDATE-ROLLBACK) | `tests/propagate.rs::rollback_offer_is_rejected_before_any_fetch` |\n 39|| 20 | **Untrusted node queries for the staged update** | None — refused by not offering (fail-closed, the up-to-date shape: learns nothing, not even whether a release exists) | The staged release; the serve loop (one refusal, no state) | None needed; pair the node to entitle it | `tests/propagate.rs::untrusted_origin_gets_no_offer` |\n 40|| 21 | **Stream lost mid-update-artifact** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's staged release (staging is atomic, artifact-then-metadata commit point — a torn pull stages nothing); the scratch file is transient | No resume protocol by design: re-query fresh — the query is idempotent and chunks are positional | torn-pull shape shared with row 14 (`request_update` errors on EOF-before-`Done`); staging atomicity: `relcache.rs::torn_or_cor...\n## FLAKE-LEDGER.md\n 13|| 4 | `spt-live context::write_context_suppresses_stale_llm` | 1× gravity (2026-06-04 run 26967808321) | test calls `write_context` (reads SPT_HOME node identity + epoch stamp) WITHOUT the `with_home` lock — a parallel `with_home` test's env swap + temp-dir teardown races the stamp's epoch write into a deleted home (`NotFound`) | HARDENED (2026-06-04): test wrapped in `with_home` — serializes on the home lock like every other SPT_HOME-touching test |\n*14|| 5 | `spt-daemon attach.rs:600` re-serve (`output gap: got seq 1 want 0`) | 1× kitsubito Linux (2026-06-15 run 27545857429, PR #14 stale-base CI) | re-serve replays the buffered output stream on re-attach; under load the new subscriber observed `seq 1` before `seq 0` — an ordering/timing gap in the re-serve sequence assert, not a content error. Subsystem disjoint from the triggering change (PR #14 = digest-proof CLI key-fill, cannot touch attach sequencing) | OBSERVED 1×: did NOT reproduce on the cl...\n*15|| 6 | `spt-term tests/stream.rs:49 bounded_backpressure_stalls_an_unconsumed_reader` | 1× hfenduleam Windows (2026-06-15 run 27545857429, PR #14 stale-base CI) | asserts an unconsumed bounded stream stalls the writer-side reader; got `16 -> 82` (reader advanced past the bound) — a timing-sensitive backpressure assert racing the bound check under hosted-runner load. Subsystem disjoint from PR #14 (digest-proof CLI) | OBSERVED 1×: did NOT reproduce on the clean-base rerun (green both runners). Not hard...\n 16|| 7 | `spt-daemon::attach attach_survives_target_brain_restart_exactly_once` | 1× kitsubito (2026-06-16 run 27595180782 attempt 1, M11-W3) | **TIMEOUT, not an assertion fail** — SLOW>60>120>180>TERMINATING, nextest TIMEOUT at the 240s cap (`attach.rs:600`). A heavy multi-process brain-restart-exactly-once test on the slow shared kitsubito box under concurrent load (n1-gate on the same runner + a parallel `ci.yml` docs-publish run firing simultaneously). W3-INDEPENDENT: Windows green end-to-end; ALL tu...\n 17|| 8 | 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). H...\n 18|| 9 | `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 b...\n*19|| 10 | `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: gre...\n 20|| 11 | `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...\n 21|| 12 | `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-...\n 22|| 13 | `spt::bin cli::tests::probe_all_cap_batches_into_windows` | 1× hfenduleam local gate (2026-07-06, stack matrix @0f08fa9 leg-3 rerun) | fast unit (0.52s natural) FAILED at 1.7s under the 1614-test full-parallel pool — probe-batching window assert with a timing component starved under load; subsystem disjoint from the triggering stack (F-2 reporting / E-2 grid title / picker rider). Isolated ×5: green, 0.52–0.55s. Same night as #12 — the full-parallel local matrix under a busy box is the com...\n## GATEWAY-LIVENESS-DISPATCH.md\n*1|# Gateway liveness fix — DISPATCH (doyle → todlando, 2026-07-09) — RCA-FIRST\n 2|\n*3|Field bug (flynn, operator's live phone-comms link mobile-gw DOWN). Root LOCALIZED via flynn's 3-probe field data + doyle source trace. **The operator held an earlier dispatch on an incomplete root — this is the reconciled clean root. RCA-first: pin the exact site, report, then fix.**\n 4|\n 5|## Confirmed root — a READER DIVERGENCE (not the earlier psyche-gate theory)\n 6|At the SAME instant, for the SAME pid-alive locally-hosted gateway (mobile-gw, status field ABSENT in info.json, pid alive):\n...\n 15|- So the `--json` Suspended comes from the projection/rendering path dropping the pid-fallback the human local-roster applies. **Candidate sites to pin (STEP 1):**\n*16|  1. `resource_projection` (spt-net registry.rs:566) — does it re-derive/filter liveness (e.g. `is_registry_entry_alive`, status-based: liveness.rs:152, which for a status-ABSENT row = not ONLINE||UNBOUND → stale/offline) and DOWNGRADE the row to Suspended/offline, ignoring the advertised Active? (its test `resource_projection_filters_hidden_and_offline` @registry.rs:1821 says it DOES filter on liveness.)\n 17|  2. OR the registry ROW's advertised status is genuinely Active but the `--json` render maps it wrong.\n 18|  3. OR the human path applies a `reconcile_self_owned` local-truth override for locally-owned endpoints that the `--json` path skips (roster.rs:52 mentions reconcile_self_owned overriding stale subnet gossip).\n 19|- **DEFECT A (separate, CONFIRMED, stands):** `establish_perch`'s build (spt/src/api/startup.rs:214-302) carries `cwd/controllable/adapter/read_env` forward from the prior record but NOT `rest_state` → a re-bind writes a fresh InfoJson that WIPES the wake intent (flynn's tick11 rest_state:active vanish).\n## KNOWN-HAZARDS.md\n 20|- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\n*21|- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\n 22|\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...\n 38|- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.\n*39|- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.\n 40|\n 41|---\n 42|\n...\n 158|- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the \"node\" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the...\n*159|- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future upd...\n 160|- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.\n 161|- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Ghost registry row eviction\"); the AMBIGUOUS render fix rode along.\n 162|- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. \"No transitive gossip\" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So \"any future update for a node comes from that node itself, alive\" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transiti...\n...\n 228|### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`\n*229|- **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 230|- **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 231|- **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 232|- **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...\n 271|<!-- [doc->REQ-HAZARD-ATOMIC-TMP-COLLISION] -->\n*272|### 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file)  `[REQ-HAZARD-ATOMIC-TMP-COLLISION]`\n 273|- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `Bind...\n 274|- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind t...\n 275|- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\n...\n 279|### 5.16 Unlocked whole-record info.json write races a locked RMW → silent lost update  `[REQ-HAZARD-INFO-RMW-LOST-UPDATE]`\n*280|- **Failure:** `mutate_info` serializes its read→mutate→write under the per-perch `.info.lock` sentinel, but `establish_perch` (`spt::api::startup`) did read→conflict-check→`write_info` with **no lock**. At bind the two writers race (~700µs apart): the daemon RMW reads the PRE-BIND record, bind's `write_info` renames the full record in (`state=live_agent`, `controllable=Some(true)`, `session_id`), then the RMW writes its STALE pre-bind snapshot back plus a `status=online` stamp. The surviving `i...\n*281|- **Invariant:** every `info.json` writer serializes under the one per-perch `.info.lock`. A whole-record write (`write_info`) takes the lock exactly as the RMW (`mutate_info`) does — a unique tmp only makes concurrent writes *last-writer-wins*, which is safe ONLY if the writers are serialized (cross-ref 5.15). A multi-step read→check→write (bind's establish) must hold ONE lock acquisition across all three (a true compare-and-set) — check-then-write with the lock dropped in between still interlea...\n 282|- **spt-core mapping:** `spt_store::info` — `write_info` now acquires the sentinel then calls a private `write_info_unlocked` (and `mutate_info` — the public RMW primitive — calls the unlocked writer while holding its own lock, so no double-lock deadlock); `info::establish_locked(perch, build)` runs the bind's read→check→build→write as one locked CAS, called by `spt::api::startup::establish_perch`. The other read-modify-write callers were audited and the two that mutate a load-bearing field w...\n 283|- **Source:** RCA 2026-07-01 (doyle gate rig) — differential (baseline PASS / 77beeac+baseline-atomic PASS / fe385f5 2/3 FAIL) isolated the atomic rework as the delta; `reconcile_hosted_liveness` emitting ZERO `LIVENESS_RECONCILE_OFFLINE` lines for the dead victim over 20s pinned the silent-skip. The unique-tmp fix worked (bringup + serve fine) but exposed this deeper pairing.\n 284|\n...\n 310|- **spt-core mapping:** the daemon is the single writer; the harness-invoked mind never mutates drop files. Bake the ownership into the runtime contract.\n*311|- **Sister cite:** CHANGELOG v1.11.7; `src/live/context.rs:615` (removed delete).\n 312|\n 313|### 6.5 Direct-write precedence guard against stale LLM overwrites\n 314|- **Failure:** the LLM emits an older snapshot that clobbers a fresher direct write to a context file.\n...\n 388|- **Failure:** echo-commune (`run_bounded_stdin`) and the live-Psyche turn driver (D7.5) are **bounded LLM calls that block their calling thread** until the child answers or the timeout fires. Today each agent drives its own pulse from its **own process** (`spt/src/api/{live,startup}.rs`), so blocking is isolated. When the daemon hosts **N per-agent loops** (ADR-0004 target; the `run_pulse_loop` fan-out is currently def+test only), a **single serial driver** that calls these invocations inline lets one a...\n*389|- **Invariant:** each agent's bounded LLM-bearing work (echo-commune summarizer, Psyche turn) runs on its **own thread / off the shared scheduler** — no single-threaded driver iterating all agents may call a blocking invocation inline. One agent's slow/timed-out call must not delay another agent's next tick beyond tolerance.\n 390|- **spt-core mapping:** the daemon's multi-agent pulse/psyche hosting (ADR-0004, \"all Psyche/pulse loops\" consolidated); `run_pulse_loop` fan-out; echo-commune + the D7.5 Psyche driver.\n 391|- **Source:** grill-with-docs 2026-06-03 (forward invariant — the multi-agent fan-out is not yet wired). Distinct from ADR-0002 SERIOUS #6 (crash blast-radius, not scheduling latency).\n 392|\n...\n 403|- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop...\n*404|- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a heal...\n*405|- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the ...\n 406|- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.\n 407|\n 408|### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`\n...\n 416|<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\n*417|- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only v...\n*418|- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`...\n 419|- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIE...\n 420|- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.\n 421|\n...\n 437|- **Failure (hypothesized, v0.12.0):** a legitimately dead PTY child (crash/kill) plus an `rc` pump dropped without a clean detach (closed tab) was thought to make the broker's loopback forward `write_all` block forever on a full 64 KB duplex, park a worker in the 2-worker net runtime, saturate both, and stall every new attach / `endpoint run` (with `daemon stop` unable to join).\n*438|- **Invariant — DISPOSITION = PROVE-DON'T-CHANGE** (doyle GATE-PASS @e883f45, 2026-06-18): the post-L0 code ALREADY prevents the wedge; NO fail-fast / worker-count code was added. (1) `serve_attach` forwards fire-and-forget (`net_stream_send` `op_id=None`) and the broker-side `send_stream` is already deadline-bounded (`bounded_block_on`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s — hazard 7.8), not forever. (2) the loopback duplex is drained broker-INTERNALLY by the operator row's OWN read pump (`nethost.rs` ...\n 439|- **spt-core mapping:** int `crates/spt/tests/attach_wedge_e2e.rs` (REAL detached daemon + dummy-harness fixture): serve the victim (rc sees its tick), abruptly kill rc (dropped pump) + kill the PTY child → a NEW endpoint still comes online + is served (no wedge), the dead endpoint is offlined within a tick (`LIVENESS_RECONCILE_OFFLINE`), `daemon stop` bounded. Leans on 7.8 (the QUIC-op deadline) + 7.7 (viewer isolation).\n 440|- **Source:** v0.12.0 real-harness finding (operator) → v0.12.1 L2 @e883f45; doyle GATE-PASS 2026-06-18.\n 441|\n...\n 455|### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`\n*456|- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two ...\n 457|- **Invariant:** `apply_once` RELEASES the inner lock across `effect()` — reserve the key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable, mark applied). Crash-idempotency comes from the per-key reservation + the applied-set, not from holding the lock across the effect (the brain-crash guarantee is unchanged: the broker survives the brain, so reserve→effect→finalize runs uninterrupted on the broker threa...\n 458|- **spt-core mapping:** impl `effect.rs` `apply_once` (reserve/release/finalize) + `EffectKind::is_durable`; unit (`effect.rs`) a barrier proving the lock is NOT held across `effect()` (a different key's `apply_once` completes while one effect blocks) + `PtyWrite` writes no journal line while a durable kind (`NetSend`) does (the no-fsync proxy); int `crates/spt-daemon/tests/inject_control_wedge.rs` (real broker+PTY, a stalled consumer + sustained blocking input effect: a concurrent `spt rc` attach stays ...\n 459|- **Source:** v0.13.0 W1b (operator dogfooding post-W1 escape; doyle /diagnose + measurement 2026-06-19).\n 460|\n*461|### 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 462|- **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 463|- **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*464|- **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 465|- **spt-core mapping:** impl `livehost.rs` `reconcile_hosted_liveness` Gap-B clear + the additive `SessionInfo.controller_by` observability field (`msg.rs`, populated in `broker.rs` `KIND_SESSIONS`); unit `livehost.rs` `pull_liveness…` extended (offlined sessionless perch clears `driven_by`; live/relay perches untouched); int `crates/spt-daemon/tests/driven_by_selfheal.rs` `gap_b` (real broker: reconcile offlines AND clears `driven_by`) + the A1/A2 characterization (`inject_control_wedge.rs` w5_a1/w5_a2).\n 466|- **Cross-ref 7.29 (who MAY clear vs who MUST NOT):** the BRAIN reconcile here MUST NOT clear `driven_by` off `controller_by==None` (ambiguous with a live LOCAL controller). The BROKER stamp-convergence (7.29) MAY — it reads `has_controller()` DEFINITIVELY (its own controller slot), so it authoritatively clears a stale `driven_by` against a live session with no controller (the `has_controller()==false` leg). The invariant's owner for that leg moved to the broker; this brain-side ambiguity-safety is unc...\n 467|- **Source:** v0.13.0 W5 (repro-first, todlando; doyle-assigned 2026-06-19). A2 (idle wedged-remote leg) deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.\n...\n 485|- **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*486|- **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*487|- **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 488|- **Source:** v0.13.0 P1b (operator HITL re-open; doyle /diagnose on the enhanced rc+broker capture 2026-06-19). The real ship-blocker behind the ctrl+V wedge.\n 489|\n 490|<!-- [doc->REQ-RC-MOUSE-FORWARD] -->\n...\n 499|- **Failure (doyle instrumented RACEDIAG repro, kitsubito):** on a brain-restart re-serve the handoff brain registered as controller on the SAME session TWICE over the SAME socket — `Brain::handoff` eagerly `subscribe(prior.next_seq=1)` → `become_controller(from_seq=1)`, spawning writer-A (writes seq 1); then `serve_attach` re-handled the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, spawning writer-B (writes 0,1). `become_controller` dropped the prior `Co...\n*500|- **Invariant:** on a single brain↔broker connection exactly ONE `controller_writer` is ever the LIVE writer; a SUPERSEDED writer writes no further frames after the epoch bump it observes; and **every `controller_writer` emits a strictly ASCENDING seq stream** (sorted initial batch + ascending live frames). The CORRECTNESS guarantee that falls out: a snap-above consumer over any interleaving of ascending writers — where the surviving writer (`serve_attach`'s `attach_as(sid,0)`) offers the COMPLETE ra...\n 501|- **spt-core mapping:** impl `brain.rs` `Brain::handoff` (KEEP the eager `subscribe`; seed `session_cursors`) + `Brain::subscribe_with` (resume-mode dedup-cursor reset to `from_seq`, shared by `attach`/`attach_as` — the operator-stream boundary fix) + `broker.rs` `OutputLog.controller_epoch: Arc<AtomicU64>` / `become_controller` (atomic `fetch_add`, passes the new epoch + `Arc::clone` into the writer) / `controller_writer` (epoch gate read UNDER the lock on both loops) / `mark_controller_gone` + the `C...\n 502|- **Source:** v0.13.0 P1c (operator-ruled root-fix before ship; doyle root-cause via instrumented repro 2026-06-20; design corrected across two gate rounds — fix #1 reverted, then the kitsubito RACEDIAG pinned the residual to the consumer-side operator-stream boundary, fixed by the `subscribe_with` cursor reset on `attach_as` re-subscribe). The last v0.13.0 ship-blocker.\n 503|\n## MANIFEST.md\n 172|\n*173|Because they carry no session/event context, an adapter can build a path to its **own packed binary** and have spt-core *resolve* it without spt-core ever executing it — spt-core substitutes and **returns** the string; the adapter's own wrapper runs the result (resolve-not-execute; see ADR-0029). The canonical use is hook dispatch: an adapter stores a command template in `[strings]` (below), reads it once per session with `spt adapter get-string`, and runs the resolved command per-hook from a thin stat...\n 174|","truncated":true,"truncatedBy":"bytes","totalLines":604,"totalBytes":87333,"outputLines":202,"outputBytes":51133,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":604,"totalBytes":87333,"outputLines":202,"outputBytes":51133,"shownRange":{"start":1,"end":202},"nextOffset":203}}},"isError":false,"timestamp":1784247580042}}
{"type":"compaction","id":"fdf30e2e","parentId":"eeed5873","timestamp":"2026-07-17T00:19:40.160Z","summary":"You are resuming a prior conversation. Its earlier turns were archived to reclaim context and are reproduced under HISTORY below, oldest to newest. Read HISTORY in full, then continue from the live conversation that follows it.\n\nThe archived transcript is compact: each turn opens with a heading — `# User ¶`, `# Assistant ¶`, or `# Tool call ¶` — assistant reasoning is wrapped in _italics_, and tool output sits inside `<out>…</out>`.\n\nReading HISTORY:\n- Plain-text sections are the verbatim transcript — rely on them exactly.\n- Some middle sections are attached as images instead of text. Each image is a page of that same transcript and belongs at its place in the reading order, between marked delimiters. Within an image, a solid black cell marks a newline and runs of spaces collapse to one.\n  - A frame is one grid 196 characters wide and up to 71 rows tall: read left to right, top to bottom — there is no word wrap, so a word may break across rows.\n  - Text inside `<out>` is dim gray — that gray is archived tool output, not conversation.\n- When an exact earlier detail matters and a section reads unclearly, re-derive it from the workspace (re-read files, re-run commands) rather than guessing.\n\nFILES\n===================\nCONTEXT.md (Read)\n# crates/\n## spt-daemon/\ntests (Read)\n### src/\nattach.rs (Read)\nbrain.rs (Read)\nbrainproc.rs (Read)\nbroker.rs (Read)\nconn.rs (Read)\ndispatch.rs (Read)\nnethost.rs (Read)\n### tests/\nattach.rs (Read)\nbroker.rs (Read)\ncontrol_stamp_lifetime.rs (Read)\n## spt-net/src/net/\nattach.rs (Read)\n## spt-term/src/\npty.rs (Read)\n## spt/\n### src/\nrc.rs (Read)\n### tests/\nattach_wedge_e2e.rs (Read)\nresume_no_control_steal_e2e.rs (Read)\n# docs/\nDEBUG-ROLLOUT.md (Read)\nKNOWN-HAZARDS.md (Read)\n## adr/\n0008-live-activity-buffer-pty-digest.md (Read)\n[…2 files elided…]\n\nHISTORY\n===================","shortSummary":"Archived 174,753 chars of history onto 11 snapcompact frames (+27,833 chars as text)","firstKeptEntryId":"e16ca868","tokensBefore":224107,"details":{"readFiles":["CONTEXT.md","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/tests","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/broker.rs","crates/spt-daemon/tests/control_stamp_lifetime.rs","crates/spt-net/src/net/attach.rs","crates/spt-term/src/pty.rs","crates/spt/src/rc.rs","crates/spt/tests/attach_wedge_e2e.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","docs/DEBUG-ROLLOUT.md","docs/KNOWN-HAZARDS.md","docs/adr/0008-live-activity-buffer-pty-digest.md","docs/adr/0018-broker-brain-process-isolation-restoration.md","docs/adr/0031-server-side-screen-grid-render-repaint.md"],"modifiedFiles":[]},"fromExtension":false,"preserveData":{"snapcompact":{"frames":[{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"iVBORw0KGgoAAAANSUhEUgAABiAAAAYaAgMAAAAgkEnVAAAACVBMVEX///8AAACAgIBJX6m2AAEWmklEQVR42uxdS47jurJkES6A0MgNdM8JjwSvoh5wFlCD7v0INfIyNDTOKp8yI5NMUpQ/9el2n0vfe6ptiSKTDIm0laEI5x7o9etX673d+tmt9FcH4tLLz+ltLPccz/h3WB8UpWjIm6RUCHajlA5ac7DthRh4iLwbZhe9O8r749kv70Kk9372NkK3FasNh3YN5THnXLNr1Ocn7sHsJ353zscPM/3veKbPMfcz5L4fJ+oJRYraXT0AoQx6qMfTS4mQC07tvvp1j6fofNWIlAq+Kr8UmSwQuT0aagJiWJByy4C7Ce+p7mXgJnofKM7QGPCpBYJP72JxkNdWgi97rEdEnC+BTxlvOhZpU8A5E+UIXwBBkfiJ2wsZCFNHsKeIx/Fp9A1UZkPRuWEFhBniaX1F3AyEs0DQEB2W7voIIA4JiONZ3wd3CQgvZ2kGwnOXayCoNhd9GXQabjPoLSCiBcKtgDieGUGufSlWDk4BRB5F3jKFwzy440wXXHDL+/m8/Of5Qjws/+ODlsE+zDFiO5UZpmGif6dlsJftdNyJStK2ZShxGZ+WwCa6pGM44LKevVziKJ/bo0CGiSaN5SQVUOj9YcYV4flU9XyES7FpnVrLMFGstG0pNenQRHeUsmduBzW7CYM8SB8JQnoXg86V/G4yYOFqmOKIY3g03HAapO96RfBFxbVL/5d6j9Jn6inGmkbNy6lz5LhCCNRBavY407T+FmgoKZBx+R+AOE5jiBHbR97vHf3LQEz8fqSSIwfr+dJftnAgdJmP3LGAM5RrpvK5Pceh8xAtzSx/Z7wfg64RLhIox5ki0ti0zjFFzScNVRr47JzpxBtm+hy5DHUbNWOojstJotMT16NARAAx2KtmOb+XIZyGUwwoSaMQxyh9F9giRzr7dMZTvQHjNkaeB/iamqqp6Q1zLy/HNKUN57dAS01MlyempuV95O3LNizd9O9boIUVW7FtOAMI+hdALEdNVJamCDkDziif20MnaIiWsumKoLPL87ccej+mE0Zj0zqpNamFF8CQJ6xJvzQsi+z5LegV4Scv005eQTzqDliy11cELd8MNo114H7xMdQbO6FTpLICySRJbaDPg1Mgjm4DiCgHRjq3eObPQMQTfQq8LDOy6QvQG12qUwaCz9LRy7Fe5mgKLWKuplDn6DIQ0p7z8q0pcAcxXFj0AETgM7AEQuuk928a1cnRFWDWCAARuSUnEAd3GYjg1mtEkBgUCCy3UT5lIEL6KhBlJaA2pM/z5hVxOuHbSBqY8Q0zIboaeN6NAgSf64dZLspl72lZB+gMpuGnksu+cTjhMwGCciHSMTwJTLro0eVs2lvKYY2QhU7exwREtECE/L+AWMc0NUXaFspvTTw1UBkFQq/ONRBYoaKz35qkHxxDSNOWnZpkUCeZwSQWnhAwAtJnbgFHRwNE4Cl5OQeXOQ2L9TJVnKZA3+UHXnZxMQ2n4xlLMC000WGxXvbNhNBycS4ll4WIg1gWrnDkpXo4cX0T18MLrJc5e+DyRXv0jr4fLd/A9XfEkb+zAwh9DyBSbFIn1Ua10KlAC6KfQ/E7gj5TRFPA7wiqTYGgFUR+Bcx8QkzUN5pO8++IQbZRDNojx6NxnLFYSwkGgkoNcwaC6vXSZ+opxppGjf7nMPr2W+q/Z+du+bUUy6+vxfaYvgCWX2Wju/6KH/5l7W8q1arP9n3dN9/s730lPiPyq9CU+6pfslePaQ9Rv9f0B18diA5Ef3UgOhD91YHoQLgORAeiA7HdSn91IH7vcPZoW23o5y9uuwPxTiDyvaEodzmJj+CXu410RxP3Gf89D3ynkEpMnIIBn0Hux3IyejlieXektD/fE003surEsNwYLTYvd3tDkVxGeiFluIvPlv1xnCw1gjgUx5lu4dG9WYpUyABVCJHbB3PEsC7qMjneIB+5MdxlLV8NfgmllidqBW0ga3csgKC7vLhT61y6My/pENzxp4QcZfrTnXd5t5RF/h8l854l5Yr79AsQfNd+bgFh+B5T0XUKeAOIsALCsj+KeibEoAlZirQFBI+q5OgQbUz3PzONogJiyrszgyi3veaXUJ5E8++cvnWSx7ZXBI+ar5vO+Sbkv+ZRsrPOAhE5jij/Q87OCWijALFsv3ZFfAQIdwEIl4FY4r8CBAYp3gtEKDvmm/wSJNdWbUzeApFOHIeUzGEGk6AE4rSkyQ83AHFIQBwYCB7TCMbGmTkOB06GLFOdA5eBSAdgeOAzOsJJFuZjHOY8cR6LzxKx8DKcMiVmYplIejIBscQvR2S2yGEGy4PbZ+YIRXsQfsmSl+eajlIyxoOkfnzqC9cdUc+ASJZ6lJGiRDSecALy9mgjSj6O21QgJk2rygVGPAPiPCxrxISMG832lH6MpyNXiowSgDhOtO04DbKHylDZZf+Jm41pWCiXPCKXzeuIA5dBgXCURIxIghJpwyH7LOwRdDo4+xl5LU0+KhCcSRfoQdqhnOJb0Nl8lHrBMwluRPs4O5doRwUv0j6mLiH/HhEv8vBvwtfgclwP5gM664mRgqy4JnEl/TnpiABsN9srws/KsBIgJHM7HZxeEcSQeJPcssNK4HBFhMnMGtgzEgmBaWAjZ5snHRbU4eR6egtKK3jTK+IchOGD7PJwJmaIUhH4ikiJ/GJq4Xld2B+T0LrQoRATaectrfX4dMQifhYSwCTMkcS6ABBRgOBIo7BFKHLpi5YbzsHwV5QIISwVx6ylSYGgNgC2d+UasdQ8rYDwoJKlqUkX66tA8CAv/05YrAMDeRkIzO3U7pKvnd2Uuu5KIIiTYa8IC0RMaw3IDbjGMhBTBUSU85wz32mxNqyL4orIQPDJOdVAxNAGglkqDn3SNSIkQkG9WMdEdFAgQH2ixe4ELsTqW1MBRP2tidaTGOVbU6I04nLOZYTfwewPcCBANBEgRtqnQCChjkmImXVTBUQipqH+IJNUDcQwJSCCZ54J8yj4uEGijRkIYqnweTkqEKCQTeiLsFqog2OQ3mOxViB0aiLo0rcmAwTYKunbVExfFIYZPAOcV/o7gt7l3xG0VEWeeWkGbv2OkHOHf0dwHTJjT8RdYMYHlRG+B7M/mA9xYn4Fs0Z4seaFXFqV6JgB4SwTHdwLLNb0OwK8CubvcZwY1EF/8aRPxMcgnglm5qDMkcS64EV5Rgks1sTqE1JllL7w7wiOeymF3mOxDtzbvFg7IuTJ74jcxpHbBOuDf19Mrd8w7a+bd77Wv1XR0JolIgyPjfZi9T5utrjuiG0rFiVj1cY62rgR7bo2Z76+tlpuj0h0+YpwX/m6t/oW6+OW7r/vFa9G+3ltbbXR7zX9oWg7EB2IDkQHogPxO7vWgXiQrnUgHqRrHYgH6VoH4kG61oH4jwzXpfpua+t/lB31vwAEkxmiywoT9Suu/jbqyPtCuWWwj2VWR6zrcK6hUMHdDnwv02pctNgnlGbhcjPl5vz07/lf5kkgP7jcO6bH4c+4Q+wnwzSRF93Bxb5SqyOm2HJE/Jwd31Hlxx7nRm/42fV/z1NrNFajIEBkYYO67CUgMv8gNIFIahU3AhE2nkajHBcxHqzGRZN9wmUCSCiSDRB2CbNPkGdBFpnYEbVEhU/7cDNduRkx9dfnK4LTGgpECKEJBN2en1qjsRoFTie1gfBXgXBXgHDStL8ZCLcBBD1tmvK7HPMW+8TFGIoHcGXfhFSWEyCiqH60gIiaXko6HOWJ5xIDIwPhtoEA/aYajdUoIK+FgyKnOVQ5gh86hY4FpUnAalAdisgEgYGPIW6DlgMTg7ZwHZQFO4lcyemQNTSY2RHjWWo7yBHHOSlUzGBmIP3DV8QUg9W42GKf6DPiV4AQ1lFO5ByYduCFW4FUP7gZQ4rSc5uICK1iUClRdUzl7CgwMWJqjMZcjohTdQD+v3QhJMUNCmUU9kOQrHQImlTkR92jPryOcvxQ97KFcnGjPBLOqhgOKUrR0BhJVSPGN2SjRZsD+VufUp1BcsZYIzznEbPGRYt9Iml+5qEc84P5nFkkTge/Rzp/NnQDeXQ+SLrGS+xOuBmqvTEmDRJlYBwSS+/IufUgWjU6CvwUtfSnHA2UyiMil1RkwagjsyGUNwFtCxABlNZCJZiBhHmCgYAWRS5Hqhcq1ODleX4lCiQNDQET6fjhrBIkGYiBW6LaByffUUIsNC622CckInEwahnEqRB6A7V51jVCgRjOg7SmQJQtUQR6tan6hib+R+EkDWeU5LGpRkFP9XI0hnM5IsKFAMmOOBwLYrPs1sVaKJb8bhI6HiXYl0sOVAIFAuViIqO1gGD8Ha4xvSKS6sdsFuuZWmKiwlwDEUU5ZIt9InySamoSIEIGQpgmpJCB1tJibYGgCEogolMglKUXRe8D7I9yFNpAyFeLNCIiUyBwsJhCUo4Qja+oU5MDq0GoJhkITE06L4/Q4TCBi4gOMzYCzYg4glaAN+ZnqNROZMkiMzWhdtHjiIknKryZFvtEgWA6YtYMEZ4JlRsBBH+/AtNk1Ak5iHpNbim44PKKk+sbilLoWwgyNmMxCqY/5WiUI6J6EWchX80uCG9CtC38DAbGgXkT9D5ggWMxlSMktk65nGhTnCCYBbUK+Q7OjI3Iv1l4GWMdDv52fYI2B/OyRaGCFjtqgRc9JikS48FqXLTYJzrxReb46cApz4R/R5zwO4JqE6YJFlZqbcbviNwSuBnKZRG2I42Ms6XQNz/L2JzsKMT05aMejXJEVl8U41oS7lNfSV3t8hfi6tfKTb9j72Cf3P/LumRnxE/8ZR3bY3ALp+J3A/HfvsURv4A38mWvftOvA/GbIupAdCA6EB2IDkQHogPRgehAdCA6EI9PGfl9cX0c9MbL37ynA3EvEDfdKsMTdc7Z5++r11R+KPQm1E9BiAqccop031RYDaHKDZd2Fs0ISdtiWPUhbvbAMkOiS7wQdx8vBOrL/57pDm5WJsG9X1gS4B0SzpwZBGMEd3brUxeMEYmhkfNuARGdUR8JUlGzfKiAcCRrPVsgUCQElxxJVMl7GwhtLUk1SO7ClrCaGnWntQWfgCBeyOCSH8xNvBDN00QmBKgyiUkDhWgeeE/58TAUuh0pEtmnSagbgBgbQLgLQMRCb+KgyhxRk+MMhLNA+CtA1K0F27WkjbF9RRjvEgGC0pMHBeJmXggIA1kvnzJQ8AKIcsJGcRQwQOR21kAs+3i+SHyLWdQlmImQGRa4YA8JCDAumNsgHIoDMzpo35lZF7QtWCAojxcz+yLCeYRTLl4dSSbLGKmYIEVrA/gPYk0wOOWEkM4HH++USXFI7IwB/AmeOgZmoCgvxIsKx+28kBYQhxUQhxIIMnZwW4wRj4f2kaIcYQAQlIlgGBbgG5xImQOTFDEuwPEYQVpgxQra95ZSqaXehHdI0yc/idlxdo8vX3mc3DJGMgdi3Zq6PBylK6PIVEys5zEG+KK4oMnb7AATg6ZX46S8EO9UmOE2XgiTK+hJcyiWiDIJjQ6eOeejWJ0EQChjJAHeYIwoEEETjFCXyDlpFTxgvsHIph1OVC90DhT2SIRmAgMxiHWRXSMUCGWMgGQVRNHgCO8V12KCrFvz8gC7ci6UE/IWco2IOQh/A+ewF88YHh6ndATv7uOFRKEV5CuCyRNjlCoO4jJBW+waoYC3GCOSpY/C2IlJWEGUWdIVoYuQXBEun2UtIBz8GGIJhCpzRGcW6yQtEfNi3WCClK15ZnVYIJSjYYEQXomzQEQdaocrwgJxOy+kCUSi+jEPxk5gCQgVmVgzRhQIngCIRxCE8ESBKMOClSfAyHAn1Spa8IariuFQ2KmJ0bF6E/r1NTFGLBDJkWSLCVK25hMRJQHBUxPpfISQgBhd4l9EOzUB+qi8EB2g23kh9bcmBUIij61vTbmdFmMERjuyWEeoW5yUiaAMC/ipiGxQxGLNlBvDoQjMwuDrOcDPBEBkvYnjbIHA93PW4kiOJKxRs8EEKVuDeRgW68SpOEHnwwu/DzEHWRrxnR7HZfUM8EKO8728EGhq4HeEKpPk3xH6Tto8K2NE22kxRo6z+f5Xf+2LLn2na/+ucNde9+pNXGKMRPfZr8/nhVwaiTv2xQ0g3j80n3mLI7rPf/3l95rub77fa+pAvHt4OhAdiA5EB6ID0YHoQHQgOhC/cQg6EB8dngcF4m9i8JSxftW5+IdGpAPxh0ak5keAaTAk1YmY9rit22IXJXVZDeMcOUOHu5h2b+tVMzBy0VgcyZSF3fI/fu1lwH79pL/PUuaZ3z3/RMkfvE+PkJd8+vH6/Pr0storQDxzC/R/tLUU26eSu92ePi3//XxZ2vjx8vwqLa1qf3592aV9miaUUaz4EWAayFOWsZXgL493TZZHtsaQvK4mdSLK3sPA4Cz4FhAyHITHr1/LULgn3vckZZ743Y89gNhj++6pAcQyljSUFRA7AeIJw/+CbTUQTy/LINNf97Js3u/3u/1G7fvdS97nqiEr+RHCQrgAxOoiCK1a10DoE9fYv31F1AwMJPrXQDB35AVn6W4ZqitA0Ai57SuCBm+/excQ++Uq+El/BQj6b6P2vXv9LvsozwwVDPiB1PwIYSFIOnPNtKiPr3kXmdtxkOSJAnFIKhn3MTAoumNDIYS4I8sALRPBfv/j1b38+kUX/dNy+S/TAE1I/G8C4sW97Jcp5tvr8z/4uxy6e6ZP/zy/5qGiT1Tjz+Xvt1eaUn79ev6Hav221LYct2z7xlMPSi7HvvAVwf9ZIFq1790//yQgvKhgvDX5EWAa6BPFDaZFdXzNu8jcDjlyyXUR18EzN0SdVu5hYHihHtQKIZTuoSth6ST3Ua8Imgb4DN7RJMSQ7AmyJ/djmTy+7/bf+S+d4DS5f9/z4T9eZai+0/H7/f8tf7/vlrN/9+vXfk+1ft/RcctaQH9dKrlspX/o77P7v92PFxr0Hy/t2p9fqUQGwuWBrPkRNvnd1Nwojl/zLjK3Q49Ma8TYSvxfZ2AACGVzZIUQygE/0UQBINIVkYBYFmkA8UQg8ZSie/cMxDIoP+kdHfdt2Uv/8uA6AoL27ZZrSIHAcc8/d5jCpOSzzD789+X/cEX8pLO+Vbvb/Z+uEWHaAgLDtAYiOsu0qI5f8S4yt8McmRfrdzAwZLGeVwohxB152QsQeY3IQOx3GQiaLiwQzy80VHt+R0Ozf5ErIgFBn5cNNDW94Dj38p0W3eV9hsy97tM3qSepEYt1q3aXF2seSP6m9NbgRwwFC6HFtKiPr3kXmdsxZn5o8a3pPgaGANFQCCHKwtJDALHbrYHgSUiBeOF1/Um2YqiW+ZomDwCw56Eupiaqlqcmh6nJvXx73X/f7fbF1ISZ5oWBoBVgqWmp8/kl176AwbUv/zIQdBIw5w4qGPADKfkRUJ1QFkKLaVEfX/MuMrdDlnlWnyAGhP6OuI+BQWf+kfVCaoUQBmIZBloSn171dwQW62Xx5u0/+F0GYpmrd7R1+UtL6J6X01csp8sSnRbr539e+PMTFmtetL+90vcubouB0MU6A+F+0kS0/I7458fPZ1P7s9T+LL8jnusvims/kPepTkT3Z14f+WW9363fbR1ryt4c297Uvq+33d+5/y4QdBXU77aOzSVuB+LW2j8ViD/16jf9OhAdiA5EB6ID0YHoQHQgOhAdiA5EB+K3D9uW/ctn1f+7atou+cDwdSDMi+9Mvix3CJdk39ML5YroTqTclXz94eRmOdLomg7e3d+V56JNSiXuwLlwqFfCTGoDdPOdnlomVxRKC2U/FKeJ0xlPtmHLsmeiO7qsmzG9pw69y8ulqDZW3dCaSPeYalmyidMUcCwJ24syR1LNZ0UDR/odqBOlWnso9ZXrQtJij7wqv5E0hiY+mFBigHgyQJR4PHHC3r52xd4nKUMtIJXICXynjAvOGhVAsAJ59KIYkfxQ0hPPKOH0E57odPzmXXV46zMRcw4GNaGWafl3kmOZXCQP+w65pGPakSbBQogbeyidlupymj+S/3YZiO809i/K/GldEfWFcenzk9mmmV1O4DtlXBRARBlEeUy2sOEwgzgKrSTmx3Alb2iAuLUOKaWP6PpM8OFBxJYpJHmULANBD0gXQLB+h8ulNva4XNcFIL7xX7oifjLDgV7flpQGeAjEWyD2AiU7eLr555lIU8u/3zhl8lztX9IzVI+jKZBz6zvKKVPSRhkXPGHI4694AnlrENM1QJoXAfoeNRDnJW11Zx0bQEhNBogwrYGwJXm4J/YBkFIbe1yuS4BYVolXZhvsdI1YpiaiebzuOKW+/w4giLewJPu+c5qQLo0dOAmU28LUtGQFJaFY7n/i/C2mJrdXIHaGccEX+qgDh/kd72m+zn4oopTB++KJsoCkiiFzPStoiFvEeF8dNM8z/zDS/gyE1ORQyxtqkzaRuaTyuaQcPY9BS7X3QLVD60pXBFgFNCbfnADxff8TE8r/7faJFoW1AyWZB7HH5JWB4MwuMxTsfgvE00u+IjLjQhge/Jh9YCDw2DnOU+OHwu4vB8mOx+QaRuUPnISVrof76jjQuU1skYnrsUBQTWfU8ib2HwcVABOFAVMS533S5qBS7T3R5bry1PSSpqYXu1i3gdgzbwEDja9VxGsgIJ5fAATBavdnIH4w/UTXiMy4wNLHl+kcExA6YRg/FHV/kWnHAhEnnZosEDfWMQlJYeJ6aiCkljcxOIniJHMZCC21scfluvK3Jnxz2gu/QNgNBMSTTE3ENNinqYmviB0PNBNR6Igd7XUCRLU/AcEZ2/ytKTMu7NQ0KRBRB9H4oST3F/muZL81Rf3Go1PTHXVE+J0wEFG/NaWaRtRyOsngxezsFcWuQycxl7zvUGpjTwaC2tffEZhufryAX0ALLbMb+Fx/2dFizfzn3Q8hDdJAP73uwFzg3wY/Xp+Y20DH7zgza/cTe+JlR2WW9z/z74iScaGL9cTckci6SKCXZT8U59T9hV1KJjBD8DsCpLhl20Tckfvq4BkfrJQz1hsW9tKaTqglRtWZUZ8V2EktpaUkJj7V5oDyR2sPc+JTXasfXutk+NPmHntMnrzcDXWuf2t+7i9r/wl13FvTdsnPi+aGl3IT7uEo9FscD/LqQHQgOhAdiA5EB6ID8bnRdCA6EF89fB2IDkQH4kGBeF/Qj3rUI5R9Z00diK+L/KaaDMdguU84BXowfeK7gbj/eGT2Au40ckbLHed0t3A5ilKHdI8T7If3HEW3xG49yt8R1z3R0F3xd9R7Bs9jmHHfF8fqk36cM5wv1SSjfGY5AMMxoDxAEB6D3p8Pwl4o3RNMen4KmmqZkPC4+yh+XvrGo8IdZeMdZflR4VvL5l4m6XXNltOx9uhrY6AZcu9cyXiYRwOEEEViVoeX6kZBj46iI6JapMT3HHVwtx4lg/sFZX28vazppWTm6N8x5QI9qy9AgyG/b0aYHnYWIDLj4XQCEO4iEAeuDkfREQmI6T1HeXfrUdcGtywLrsRtZUO8vazppQHiUAABw4NDyO+bNdVACMdgmcHemF/wFojjQJXK09LyXDRmOtLQoOpw1JvM9nSUn991lLv9qMMdLYzhjmju6K/PZSbOzyELeFLuB09AJz72pO/bNWGUUUvBMSAgUNAL6rSEgL0Qg7AeoKGRjmLfFTlKr6P7jtIr4pajxjtauCuaeH+9zAZB9Ge4qhz4WB58eF+M+r5dE0ZZarEcA16seXCCGRxzqU52ycFR0wYQtx/lb27rLa1dt5R1d5RFBvu+etMQ5sV6SldEvVg3a0r6JADCGwoVgDidzDcOZ6qLqbpRj8rff7C+3H+Uv7mte1q4rywvwPfWO8bsk+IyX6QJRLsmV3xryhyDYdYvffm7MorpN2PW42O1DT1qYiYEOBL518c9Rx1vPuqeFu4rm39H3FHviX4BwCdF+SNg7im/I/+OaNcko4zfEe/lGDzqUY9Q9kM19VscXxf5XTV1IL4u8g7EHy3bgXiQsh2IBynbgXiQsh2IBynbgXiQsh2IBynbgXiQsp9U0++RF7m3vt8J/Of04MNtdyB+MxDbLIO1N4nIQRQv9ibhhxN/vFfPNL0goX2DDwpaukkRhJ5gSoOxqxxQisY3/i1KPlfxkg0CPRHI7ZBfBAnirzVGgthnMD8g+8Pwu6S1sMkyWHuTIKCnFCX9ZScGRw8n6oPt+W9jqItjdVsy2WDljxt8UKSKp8uwYih2BRAvGY9d6eIhfb0MxFOJFT0xK/IYSzuvrqkxkoCYstMMXj4BQfkIt8UyWHuT5AcVayDIQWR/wxnaBkJf7ENyiw/K7pYr4k4gpK8tlehLQHxziGe/e+LIBIiXPHJOHdovAMFltlgGa28SenCXHtZViXKaIuwVQaUhO64OJd9eIVv+7RXv+IhXI5rO754d1Dv0irjqg4I4/rGttNtatu9IVF32/VM5oEAzRFxQpEcv2Eo6+FqrTnIUh0QsNfBVibmArgu8qzRGGIKJfWBOg1AUSF6esnzwpCHXGAJrg2Ww9iZRVRl2UWB3EXiT6BpBpZ/YvkgdSvhaZSeS7xD9x/ko7/ayDRYAEA9y7gYfFHrK2/igiPJHo609PXG/W2rc5SOMAwo9ab/bJQeJ7xLLDq4oqVa5RNTwg4/YyQW3xPNKT5/Tk/2iml9pjKhdlZfH+dkiZQ4iRASagWcGwgbLYO1NQvL6z6wzs1sWTnT1aa8X/jfS8tjT4vV/yaGEBB9EZIIeqJepiephEB22oXvP6XvGNR8Uaonaz63sXbstFgP7SUflI4wDCvdD43nmE2zHvdtDR4RrzV8YyLwJNe8FB/LVwpq1zAh7Wb0qjRFdIsSRI6g/DAEB3ZABvKYtlsHam4QXJJouXvNZnYEgNY8dOY24/0sOJTt2LcHg7J0Cwao3dMgrtonux6sCcdUHRXRDbCvttnjQ2Hlo33JAkSEVF5TX7zuNbS9H8nqg3wZfWRgjHQXZFwWCPwOSSmNElwgH5Y9R/WFgCcRfjijzzdosTZbB2pvETE3kDMKXpwGCirLlyz//qEPJTgdG/rdLU8hul6YmFVrZbQCx9kHZszPJd9tKuy3It8hU8Z20RQoHFBr47ztxQVkW3tcd/0ulX7i+J6kbOiRpatrpFQHNkh2cAfd4V2uMyBJBQIxEHlDrqmpqGk5bLIO1NwkWSfYUXLaTu0gBxI6/Vb9imYJDyU4WZJ56+DhS6NB3z/JOFTzsT6CLPihiI2ZbabdFqiK0WPO+5YjKAYW1Qp7EBeWZznj+l0q/oL4X1P0si/UPifgZVzR9zxOHRfKdw7u1xogAEWmxdgHKgYG4H7Mu1sziqF8tlsHlX7u/fra272/6wbWq7ed7fVBuO+qS8cZ7dEiutVdHHMO9A/LhWxzvdQ55rw/KVwDx8R7UETd0aG6vrN9ren97/abfF0bYgehAdCA6EB2Irx2cDkQHogPRgehA/L1A/I4ge1y9w48ZV+YVSFZ1tnvzXapo/uYdcV3Q1rjucHA33/iKN2+PN9fULBkuxRXftSW/7HjGZv0yXr4a9cDPSmpU/iIQoQmEr1PkqYjtcLvfwbyLze23DUQo/sbLdW4CERp1h2pLuNJ+CFtH6rF+A4giyDyo9wDhNgZ56XDUQx4MiNg6XT8FCHcDEIeZckbgFVCaQpIWnOCO8cDuJsPkpAR9pgdcSecCgupHfs8lT7J9ZsaCPO67/OXHhulIEnBYOsx5EqoHKRJul/fiHbbS68hbIMHO4g9RP9O/kNk5SBlEhhq09sOMh4e1JoqurhN1cSKM41pHg22HVIOtrSyz1X7gvWfTeo4BESxAjMImOM6jmlo48gdh4x3SvQluUv7NGOgz5bfpYoMBRgAvKo58bJATgB/7zmYAYWInH9r965dcYaTfr3wG8frhCEJwiIEqGoX+5pZ9KR4HXyCouyAmiT6iBs7ERz5a+XJc0xhsnRF1ok+jxrWOhtpw8jmEsjYtc7l99PHNtI6YpX5WsRG3heRmEsAwWIKauOO0Z6LGBy7BQ7dsg3jB8TycMWzwScB2Kgkg8EA71f4mARMQmIqHM4Cg1kj2X1uPfD2mCxniDFQ6pHj4eqWOsdJTCDl6zrzLJ649A4EH0m2dxzOO5faDxrWOhhPJ59w/WxttHaTMdvuBj3gzrSPmZfyot+ekPCAmRyG7jWQgdCFBs1z5pOFFp+evWGLIdgBBhhhkiwEfkjEBoa5XGYiojiiwBHNuzkBwWRf1isAcOwMIKsfbi4HItY8FEFFb0zoRPdoPGlcjmmyT5ura+Pyew5X2SyBiWiuW/+YgUhHKZ1K234hgeWqSocIlS2wDXMrUHDdNdKkxZK2JUbcnIIIAEU6nZb4kIEgQhS+xpbRMNCF3FNeN8m9lmoDOy2EOCTqUgyrGYQ5pSNQhRQYi0BwdAq1wOuEUdUpb3P6oca2j4brF6WU5f4vaDrMps9F+AYRGkfqxbBllatLlkf1FZLGGO9RwWhqe0TixDajT7BDC5wAv1icP3YkT8z9kO1wOvSzbrEcRI9fkZ+3wUno5Vr4azIN4JNK/Pi3WXtjp1M5yukg8TmKQLwMumK8aqAFHLEc7biPVxNdqrhN95D6xPogAUUeDLzT4jGkz1xadKbPRPoCgLxe5de0H9/Z0AIvjOq/Au4+/9Gvbe3/Bxju3N75uX3zdGle8ufVbW06vu3kFfxkQt73eF9enAvF7X/1eU+/wXxlXB6ID0ePqQHQgely9ww8eVweiA9Hj+jsCe8DIvjSiDsQfiCjSn7CqPCfTQyq1+RoycwOl+f4kPEciWRpcqYB2snW91IGmzRZ7t+oOHkgr0hxnXI/D9q3CUEdSj1Wzg6bng2vVICPnbwQip739KjZOJOXtAoSkVljyfIIWRaMGb5LsMX8Ovgo4p6g0Mr+qq375cvzMEaEBRLgLiHARiFAeNWn7vgGEGbsNIKKhPjT4BxYI1wRiFJMKki+3QLTqSCfEBSAakd3wCg14aiD8JhD+NiA4outAuA0gcGEIj0GZDUi4ZLbFYUaqB8yDmpWBh1MHqQ6lkWYiLxEFIkzM7BCGBNqUBI+TR1snpEQHJw+9TggYnBFKtS81c4omRYbkigObQtkXOTEzoLzEyO0hFYOIZ6S6iF2xvJ8sw8JyVdCrwYwQ9h0Nn8QyQM68HfUjdUZ1oObMbTkkIXeKh7aLI06ynqDUHSdaM9ti5H2J/VCxMpDJ1suL6wIT5MxeIuwWElWLIqgIgjrvkDkSeA7Qq8dnjoaBOE6cmOUkJSUUKQuskXnJjo/M4QgmvemYI+JQXogXY9AIPK9A6PHIue8ohLrR8EKUq3Kc0zQ7pneT5uodl9WItLYjZSppHCOO8lKzclvA+YCdDlLRXlAKqglBPAWwLwzbIhTsh4qVQUdlICDdP7iUAedJRoEYmLcwJKmJ49lPukbgisBnjiZdERxTCOrrEDUyj7SlpTVI/eCIDOSMIhyL4YyBGGBKIMwNJ2wQfCVIPSy4KnxFkNGNjBC3wvvQDtWkEeXamJsRKfaYToCC20IAyByQgGBOhGhCUAYY7AvDtgiW/VCzMhwzEdIVgSZnwwlJQCxDS85zMylPSIrdZSBwND6rQQZnpJkzEkcFYpg0Ms9sDy/UAWFWcP3gpqB85lhoX72wNzIQujYJv6PgqnAcc0yqGeKcIlwT1GTH6k1ZGksESww4aspAYHSbQJQXHgPB3U1sC0xNCkjFyuBgQp6alAlSfmsCEHZqinlqCvlbEz5DEcQrEMSdOAU9zmlkOjVlLkeemtjjZBRfOSXDpakJ3BPtcZqa5NiSq2K+AeapKSrXBCdIGiupjesahZYzroAQzkc1NcHzIy9FAYuqy2wL4mdk/kHNyqCjyIfqKIsi2Bz2d4RnCySKUxfT0nMEXOkoE9uRWRzM15h48uJ2lutqBjduSDyQoyyLuLaCKxZrXiZRnmM8gT0orZ+8LL60n91dptzDkqviWot1UK4JeCUZCCWCenxZkKOIkIVYldtiF2sdu+YrXv21eMf3x3e+7C+EzDRhAaMborlP8+JjPJX2WCV9RPcFlf9OIOwrM034y+IN0fwebsqlsRrmT4mh32t6kIg6EB2IDsRjd7cD0YHoQHQgOhAdiA7Eg3f3fxSI/2Jw/3PcpQ7EB15VlrW8e1NtqZU26L5oWSIa8kF0q/rq46XGWG3FrTh9og2MENyzFTpCs7JaO6QAouJ/iEoG34Nf7oGeNc3Jd0rPdcv/slb0v+d33cwLrX4XrxUbYk0VqICo2REpddQAIlwBQiUQLHkgFEDkh2tjSDkPBsI3+iGP5xdthgYQQR5vT2Yak6bH0nOhq5ZTPiNcH+/QPL3Lks0x+QIgOG11KxBuGwgnOgCyRdghWxVeBqLSATHqLBYIJ0BULWcg/OcDkTgVEz6VnA2bEsmKFGA2aEpJgTjOif+w5O+U2XDc0MqotSgys+MIjkXS9aDhOCRGiHNghxAvZEjeJIfE8fAOfIqDqGzYSDMzBcwTPK6rTIs2ELblDMQwqZ5GoU6S9EW0X/pJlUvOSc+jHmOXOBUTYKo4GyZJmBUpMBzMVJBSqpwRkEhcMohJ2yJxIyqtCn3gW7QolNnhkAkPyvOQPPiJGCGR84P8yfA4AterHA8AgUi0DxqpxgQNESiQqHUA1by0ENhdhnNpx6luGa1rxjEE+6B9EO4I9EXQZkhcD5YBkETquFIa4dFInApcERVnI/MXolGkALMBKhLC2MClptneaLUtlBthtSoGCJ8YLQov24gXwdvwSaamJQM85iuC3isvBE4MKRMuQEgk0geNNDFTuBUv/BNlWhyEIHDgKeOgUhCmZS6T1giVnliOj6pCotoIaDM45XpAz0PZL2lqMmPslFNhF8bMaMj8Bbt0gdkg3KB5DQQxKIy2hV7qRqvCSaY5a1F43RaC0bpwsyuUMfJijYFxcwnEMmVMYBgJmaCIVGNyM7hY4J8kpsUkxlfgWUxx1bJn55kaCGWaWH0RtBkS10OjroHIY6xsIWL7TGvORsVfGJWFAe5GHNUGJMqlKleEYTZkbkSlVVFpUSRmB80q0pLpwupbk5ma7BUhizVYHWlqkkgN22IMQqvxmWlBcZ9ckjdqfmuKebE2ehpTUidJQkbUZnDK9eB9I6amhtIIT02ylMlFVHE27GJNe6BIAb4GMRWclIoOxwQhO+ZOZ25EpVUxl1oUidlx8rOXOpzLqhnKCBHuB0+P2ZskBuV44IQEq0MXQ400MVOYx3FkzsrRMC3wO0K/gJS/IxR4Zi5CaMVpaWGaGH0RtEksFyzU2AeOhwJRjvH2z4ubXu1v1KBsvqM69+/5Y0fEsNWXG2O68afaZ6iTfOqrzVMQEvMfjCc+UEx/+NXvNfVO/1UxdSA6ED2mDkQHosfUO/3gMXUgOhA9pr8uvMceuE+PrgPxGNGFmm9Br6Hx7p7bgnH1V98N6xtrxmKhiKBtrhFsCXoSO++y76v6ShuHqgTugt7Wp9a44dW6EThcqWcwPWoCUfqh1GYYl25SBretaiFsjU0gQhlBKFU3rA2LpIU4b2C6GG8BwmpzZIH3Vk9K1ZGq92bcQgOIsAFOqOQfSiAkcfX1QGD4/O1AxJVhS7AlyhvxMdTxXwZCWxw3gHDXgIhWxuSDQBhti4qnQdwIKGgo5SrzFtgtJFhnEOtA0la14PqpvpM6tJi0k3Fn4eRJ9DYyaXPID+U6TsfIA7rKjsDjtVlBw3BHTll7Q2OfWPeDlTyC9mVQRkjieaw4FydV8sjjdjScE9XjOKr6hqTVoF5SjqIkuk5gcYyZ21DxNKLqV6jPQ3YD0SRgdgaxDiRtVYuobI1RzwOTiDXuLFMACSBHppoaVAZpW6Qr8T6zI2KASghYEs5yR0wKWGOfgnfiB3PKhgZMyJCeKLOk4FxEl4wOdNyCy5yTINGE9Dg+TZiqXlKOIs0Ny+iNsK9JfIuap4HcrU+6E9YNJLjshpLT++pA0la1ULkDTANDyREx7ixvCkTIqhvQ1KAyKA1KC95ndkR+8t875U5oplz9VHLsb8Fr3nnUfQObhcBVJaQ8urN9ikPlwTKoeoiMEqLByDBHhIF4E40SO4oDxpl7YLxEap5GuiKEi2DdQLgzY3YGKR1I2qoWJRCq/ZFJXXoWWSBUdQOaGjERDCArUbMjSiDS3qgEgDJ2A0RmKs4AAj2pgaBYhkmVPDInhD6PwjkJDtEEjRykDMRVjaIlQ7jsJbLiacgaoboT1g0ksD6GZTJYB5K2qoXSZuIIvkg9Nak7Czml6NSUVTfUlIlKE58vTU0xsyOoJfisMNSj4Y7o1GRiP528siyss0sUOk8IoQBCORdJySOPG31m7zGp5S35wvh0SnKvqlHEJA1+h8teIiuexik6MCXARbBuIJ71MawziHUgaataQHmDlDDAF1kt1uLOQos76WjkyI5J/YK4EDQqR9HUoN8OmR1B6lBRbYWcLs7q9oITJ8dO9EhpMRhnl1OQdz4xS/j7jXF38bP1YEFkUb4ewDtFx+xo7NBEy8SMIvVS+R3u0i/E6C693q80EW8stxXZxzUuPhB7aEcXP97We4H4erWLrcg+3vL7a8hH3grEzW31e00PEl0HogPx10bWgehAdCA6EB2IDkQH4sEj60D06O4JqdvXfCCma8/A+A7Eb4ppSu/aN6l4f6w2HqcgleU7KsdJ0+rHdAyOy0fTuwafQ7YP9Z2yGKq44mbH4oVta1ZIqQESV3van6pXIf4Rr5VxpSJCOXZl+bBdR6y283PN9PxyDFn0IAFBuamwAUSTzyHbvRFyYBH3yReBrFUswuYeEX8vgLD2IzmuRlpftvvmfBDWAzRpOqiuIxMmfOVNg7H7OBBhAwh5dHgbCLcBhEtA+NzOXUD4S1eE6IPcB4S7E4hLhd0WEFkdIjmkFKoRmaOQeR6qNsHn/ESVUSZYJdAZCM6ZwHkkO56s+RyH2WyZ4deSrwmWT2fpBTdlRZCVUojErQwN5YhIrJyUKb1YUqTi8gKXE9R6lmgkERsP4hezdm1RxRAkppDNRB3q8jK4QxaN55gyc4TbTGO3vFd1CLAs8Ni2VY3IHIXM8xAvEV4PUFnICUa+IsTWY4rCzNjic4SQFDiQiJx8AuK4dM9xXtlDO0YVQRITwqX0LH0YhQeiVw9i5WTlVHqxpEiR9+a4tNa3FB+AGINOTbVrC1gleDh/EscW1AErBlEHyVn2kjkSIbshV0QwogSD09yyVY0YJGwtQTwKVpAAM8NcEUG0NQgI4YVEJQRs8TkS/+EM1sPReXNFvAXiOQCI7IBiVCzYPwVtI+5gOCKIldL3R1d6saRImWWx9NawVN6CM6pM6IGlzWTXFvAwgrOUBdTh07TN7BSnvJOCsLBcpeaKOFt1iHlMSX6jGjHnK8LN6qXlEmh2jYhc2k3WYcQCseZzZGcTWUsmb9YIWGxgjYiuSvPjy9QsLjDOZe4T9sjZyEdOpRdLilSUOOKU+RUGiLEEonZtQewWCF4jmAODePiKEDUOzy1bINycxy7aK4KYCOI+YlQjMkdBeR7gyfE1y9+avAKhLI7sxpKcR7b4HCErcLiY6tNvTacTXdJUoypmlEwI5ZzA2WWJi6c65YjQ56xZY71YUqSixAHCQRA6jsY3nLQH3mV9DzM18WgEjkK4I1MQ8AOXJ7etcmoyzJGIMRAgRpfVIYiJAPcRqxqROQqJ5yFCUFSWfkfQmablmPuQgGDPrVmtZ1p8DmxVZxIoP7G/CBQ1IvEcPH/KiiCGUSGuJXB2iQ48EOWIRNboABOj9GLRSNWmzPIrpuT04gK0Sbw4ntSuLcn55ZS4IxF1HOFrROwTVR05HeXLiDJH6Kg8dsxNaX85dZt71t/UH+OXddz4N+0P98VVf72P74jn2jG+PVbvA+JRbnHEK5/bXizbr/r3bvwCID55rPq9pgeJqQPRgfhzne5APEinOxAP0ukOxIN0ugPxIJ3uQDxIpzsQD9Lpx4rp93ftsck/fyy6DsSnATEoQyFeV44oX4H5GuJJckP5W16xqKOoSzK7v34N72hraL0PhfaH9IMf8D/jnmnM5Y1yAXFTyvtQQfgYx0RPsPuju+bVwmyXlKSfXJOtEDcZTR65uGCD0eBLFYsWiHcDMeHRd0p3uqKtds2ZIJBZGPlvWAGRTMSTl0ruiy9FVaZ8Z9bnEgTCVPJEvHnY+aJXyxqIPMj+6lknQLgWENcG/WuB8DIEefy86dFlIA4NINwaiGokkOLJ/JVQRHPVq0WBoHSIh7oFcxUoyzVMdNmchQdRcxj40p0AxNoHhfKxqj6RGRalYkfW8BjSI7A128O6sEi4TEwYxJpedS8cPCnmpNhB0Us77HFCfVy2DXhs16mahq0hZJgoH0n6+sIvYbeXk0neMBAyCupGMw0cmfJEAvM/BkhRTFe9WhQIYiF4ySOrFodn/fi3xGcoOQxi/bE0fpwbPijRJ/UJsRqRY7Jih7IwQko9uhXbw7qwINyBr4go8+GYs8UOSV3oZHhJ43M7MSRCQnLKEDUNW4MBgvOflJsbRW7CJzsHBQIzvHGjmRiE2fBE4sgpU8o2XvNqMUC8BS8su5jJBBOCBP+g5DAwN0GuiLUPCgGhPJDMmygVO+A0EnnABuM7UrA9jAuLvSKw10na/njWiSwkNkhmaigQshDzwKqahq0hA+EnF+i/KK4vGJVjNj2jOALGIbnRTM7hikg+MlFz12666tVigXATgBjFZcjJduUflByGyE4sGYjSByUw6+INV0RScKoUO2ZxIxEuhmuxPawLi1kjorNAUHSQZgmJDZKZGiUQbo6hZI9oDRmIAM6WTid6eroSCIyDz34wskYknkgCgn1YLnu1ZCBOJzehQp6lT3px56mp5DDoZJGAKHxQGIilzsOsDIv62JBYGCFdg5gqLNujcGGxQGC+s0QWJyAyj4Oj13YiWBh2alI1DVtDnsAi5vKo33Hy1KQMk6w+koFgGkCiTSQg1PHlslcLHSJeJssWLLhY9HWxpgVX+AcFhwHciWOemgofFO9QZ3TKsNBjs2KHsjBCchTB4mnZHoULS/odcRQGBjQ0pqTHx3VBJ2OJXpka8E45Fgu4qmnYGnAUlaF+L/9NymIR/RD6DyWmTPZUN5rAq9fRWc8W9vSaweK67NWy1iLkmVbpX/b177n9Nbb1SzJ7nMTVlnY5/bTecqnFbfeV1u+h1rayBr/B+r6tRLz2y/pe9Y/oYlM9It4MxLVjPvbq95oeZFg6EB2IDsSfbbED8SAtdiAepMUOxIO02IF4kBY7EA/SYgfiQVr8K4D4k4P21SSe3zeoX95SB+KulmJ7b+MmVbT7wrpkrLaU2hfDbVGta+PXkqCZ1uGXCiA1o+SWPjW2pvu8diNl7vwt0cfWvdRGZNHTY+RJj6MSJKjD88X2NRD+JiBqDkXZwYxqMO3mBI/WGWJ5zFp4IoTtcfarIfdbPU2ZD1uH1QdpYeh1w6S+LtaRpRHZFPQJ2hqIy+eJd++6Iowjir9wmtZAuDUQMYbymAYQ1zpy2xXxLiBcBqJybsmckrKVmBJW3JJxCoE+hKjky6OzTpkK9PRy7U9CyhKqRUF12/2UnSsdUTzk16UVjK0yHVLKJtV7FD4Hq4PMCFzULOSYtQJIUA2MufZ4KSOlpAyUMvRBW9vTnB23Wh8YslbduTYWcOcM48E4slAyKbis8zEIK0Vy3cz/GGbjFOI5lUcElDE9TC72AAEKF7U/yYRn61mLwhX7oa1ROqJ4SR8iYQhJFWU6JM5FqjdIbXqVeHl0XPU5WgogYJSsGSN1pJGyuiE9er7u6QS+iNX6ABCtum1teLA9SobYJ9WRpYfmYfokMTEl/kcwTiE0RHinHieReQ/ZRaH2J3kLyvp4q/ZDo8I6orDxBmvPoBVcEZpgV85Frje7OYDf4cSRRPU52gog6oxSe7yUkdLCH42NR9lTc0UYrQ8BolF3ru14VqUD8EnyyAVhAMBFJkpbAIJ6svQwO4UsVYFlgCtCGQZzC4joLgOBRL51RAEQVBtaqYGITj1T1kAoB4q4Hkmfo6EAEpJCVO3xUka6xDLhWhQCQdnTtEZYrQ+sEa26TW08uIhaPYpqIOAis9Sb1giHlrJTCA9R0DWivGCjmZqyP8npJFoW4cROWmb/aC1CEnsCA5+WM8N0CNkzJStWyEUuwwVUVJ+jpQCSgAi1x4uNFHXRWgBnlbqnBohC60MYfKu6c20c/fKtiYEYg+F4ZCAyPU+/NbFYy2icQo5MRlQnFTUkU6aCLtbWn4SWUnxi0Ru7/xTYqc06ooALoa0451xmOiTORaoXcUHxAyocfhJpLdbnaCmAKBA1Y6SMVHh6vDK5sO6p4Z0brQ/8jmjVnWvj6CN5vIBPoiM3TEGcVvJinX9H0GpktDjiFenCaz9m1sfcq31x3+seBZAWOyRKjNtckNuit7VtReo3ItGRK1oqGf2f8bpX++Le18duccRVr6/Vf722z+rJlw/VIwHxAMPTgXiQ4elAPMjwdCAeZHg6EA8yPB2IBxmeDsSDDE8H4kGGpwPxIMPzB1p6kKAeq8MPTL/pQPDrDp7CSk2CcmBHcZSejGbFqo3Zfso6ILhRHFaD5W+5jXNDRF9Z9629Xfe0qd1xF09hrSbhJslNadoUfiqpZq0t2IFIz6RNyTWikbL/jIi+su7beiuvsqeVdseNQLgytFJNQrPCwYR2lc2QdUAOrmA03DhYt0f0lXULw+VG7kbRU6vd8U6eQq0mwaEdKiAusRnOM86NgdUueFASo6GM6VMi+sq6BYjbuBu2p1a7Qy66+3gKDTUJZIVPUdPzs7vCZngL+SH0HJ5vcCc+JaKvrPuG3sbE3Sh6arQ73Ht4Cg01CYQ2xkQkcFfYDBmIweXwwGgoY/qUiL6y7nRF3MLdsD212h3uXTyFtZrEitGhoW2xGTIQxGjwrmA0FDF9RkRfWbcF4jp3w1eLtWp36NR0L09h9T1CeBIRPAl8j7jEZrBTUw4PjIYyps+I6CvrvqW3MXE3bE+tdod7D09hrSahPAn6Zq1f5C6zGSbxTmGlj0h1Z0ZDGdNnRPSVdd/S28zdsD212h3u+us6T+GSLkbjy+qF8vGmX7/vi+gr676/t/G9tzii+z2v20OLD1X3+3ra7zU9SE87EB2IDkQHogPRgehA/La6OxAPUncH4kHq7kA8SN3/OSAeO7iHHrgOxCe/bnT+uNHjl158L1OYGsOFkrHurDAbKDGztHqmSCxLohXB0N4R8n3NhhZI43G1eKlQKNVFtgtutlhsXTEE7nH+2JKMCI1OwEO9pcERiyOj6UdIQGir0OqOsV2+0PiY1mHlZwP9esRCXWejDzbyC0D4QsEirPocGsM/pE9FRLc5f9wKhHfZy6Rs3l8HgtNW9GfMQIQtINwlIOKnAxE3p4S7gTjYvfc6f6h2R4v/cEifod0RWM+AGoaPCTxK6HN2Msk1IqFynJXZQG0eCiBwvC1/oFpE6cMJK2MKTv5l1xceAfFkccrgECeXU13nINGjJI0NqXygBtLySOoiUka9X0jrg5JOuRfa57OJVuOI8ZxGAtGT3cedzh+q3dHiP4zLZTuKj4rnIVYgRHuCU5EUpPVCSTVykjI4ZTYsMUzxRFryiMvBvaQsz/nokfU5nLAyJoluwmP6fP5GeWQfEePIKMYFtk7+JAnTyLoiQa8z9tXQ2Jy4rbDBwjJik5AJci/QZxfgouGS3AQe/H/j47NOSXB3O3+odkeL/0AaHPisF2gCgsgl5+EcXEq1Jy8UWyM9BK7MBm6dREbcwdm1y5YPksYnfQ6XPC+c/EsaHZDb0lxzZGYGjsyeKrnO6KAjgnfiY4ErgnumsQ16bERfEYXtBfo8sK/M4BIQToGICYjIcd7t/KHaHU3+Q1raW0BE8QbKQOhcn2sEEJiG1UAhIpJi0LR8SHwKrBElEOBrlEBE8VMJrlVndIndlIDAnI6epdjk+glTofVheiF95iuCNEQuAcFaIvc6f6h2R4v/IAJCQcM3QCDQUZcsAYKFJVKNgS/qJBxhvjVFfGuKZlKU8joEYwaCeBX4d5meyIRhKqcmTEsqSVTWyftHa6tDkyzXwPojKbYxAcGwMatjtL2QPo9vAapQIZRTkwWCR+9e5w/V7mjxHw6sxAE/FCzWpJuBhQuiWBkILKb8BU5q9FybnzOzAQQv+ovfEeLXYsoH4WVTO05YGQRZkIV0gi2Vy3RKtDEoR6Oqk/kXHOUguiJ+FmMq1hhJsZ3w5cE7eL+wydrJ9kL6fFpWKqfbNQ7eKqsldEpaaoT3On9svPxNpVrfBD/712v8xBreE9vH2/9Dr//aLY74uN353wLir311IHpn/+rYOhAdiB5bB6ID0WPrnX3w2DoQjxjYn7a6udT++2L7S66BDsTnvmBlMZefag6Cb9wjXLua8P3NooytR1OmfNSSVEFqiOqVp9P40JxnRqnt2zXUotst/+PXXobt10/6+yxlnvnd80+U/PHKG3fruvb6Ju17fjVA7LQA/bPbF4fu6iPL9+2aZdN+BURSjpBPddI/hLW2RMtMI0rCR4+0T5NRNkrv/LtZGRFBRNgHegC2AEJKzeVNYZPQJyD26CDh8esXdeyJdz1JkSd+92MPIPYYjN2TGbHdxnBRWQPES8ZDh29n/ln+7Hblx4s1bwNRffKryya4m4CoTmBbTwyHBISPWhRS+7TPF1eEl1K0NdThZiBe0NPdMuBXgNi7/X5Xn6+fAIS072zl+zuBGBKbAQkO9UMhIDh5w+ke7NeEyWFWRkbL1SQqh0KO9MJU4CmME+qUGWPPIGFEIGPOBh0ChExfE0rR1syCYBYGuaGQM8vS4jLML8+v+/0y6bz8+vXjJw398+vz6xNPAfRvAuLFvex3tO35n2f3bflLJX4s/6Nt35bp4mXH+/6hqWP5b/n069e3VxzxREcsZb+9UhtU/lmOpPaXrcu/+PxDtl2u2XF9aaqiExopQ/U4oZQfMRPofBQVloj9zO9QewHmIaxdTSjzNQZ7pHeq5OKgCMV695T/xKqDlj3pxwuNTB4OZ0UlKoWcm6YaR07EjpzIpBbpStjv+FTf6xWxfObJZ9m4AxDPe4Lsyf14pW3770/u+3KM4+mEZpH97vuOhms58vsele3o+llq3OUjqPR3nPC7HX9CS3Q0xYHP9Pfpas1UQK+M5V/iVkj+1SFbDRZGkNQ3nrDHfr4iOPFOy2wMbVcTl2QXYgKCk6002dCT98w18uROJYw+zIW0T4Eg7QqenLiUAcJpRhwMClwROwUiXREJiGWRBhBPBNJS8pv78fP5JwaPgHj+SVuXLTxd/N/ux0/aTrX8+Pn08uMnHZWPoNJ7mXl2fOyyj94RMDwV6efd1Zodl9MvFNK1EgjVqtDhRPpd1oI5JgZH29XEAkFHeuEQYsn1ohNF4JVABKbc6BIdZTWgUqCprIGIskbsBYi8RmQg9rsMxJ4+ozSG9cXJAC5bZLj2vH0vQ03fw3YOR7iX73K20yDy3h2vGK/033d+j8873natZl4y9NvTq/CWCiBGDLp3ULFQ3l9IlBllcGy5mtD/8pECRPJtiU7/X5wCbH4DMbs8NaEUS0tllkUIST8M35peAMRutwaCpxQF4oXW9T1PEd+5DJVYRnBHW3ia+Ocf3uswgdC/DAQf4V6+vVLpvU5NDp/2O2r/2+tu+Refd9h2pWZcycvVwKcDcxnyYq1+KTzdiP2Y8hvyYh2EDdFyNZE95kisLFh+weym64R+HegvCtRL+5ZtkypX0GKNUgREZkEQR0L9UfA74sXxYvqqvyOwWP/gZfH5nx/8LgEhiyatFbxwLsc90ZZXHqI9733FkrpMM68EBI5YdnJpAPG8nPf0icpS+3v3tPyLz0/YdqXm55cd/554dvZ3xcVX3GB2XHc1iXdzQvzG+63XR35Z73ftkna7PWp/IY795Sh/mi/EP/Vr9S1HFq9t7ZRrQNykuvKh10eA2DoP7fbPAeJai588DP1e0x97dSA6EB2IDkQHogPRgehAdCA6EH+mRx2ITzzqizv6d5wd/9EoOxD55T+wdzvKdNxzUWC358TiT7d/oqSS45Q7v/YizgDZfi3Pd5Oi48do4VQtghKcFVrurCqXICl9LHdPJ3oElp6epuP+Pf97Jq/qCH3kM+kLH0WyNt7UqZjqXxJJJkp3wRq69dSarcUenT5NqVjrZuZUNxMvA6FRaq24C5gYIftnfHylW+uUatHU+K4JRFRN8cjZO1aG56clJ88P5dK/WlIfqKXME2mac+aNnzsNkpuYWHUjYdcUe6g/h+RNjWTebUCApbIeT63Fuex0UsIStoBwrhKhiBeuGBOl2wDiG5/+T5RpARAv2P8kKhltIORBef7LqgNnPBCv3CVXSq7ElCoK3qiATEEEH94JBP2Xo7wIRFWTd66o5fOBWF0RsfB0IpaDMg/AT2BgXpAEpiww7VeOBCwoWHsCLAoOycuT2V4+uUmvCNe8IiIMY64AAVUMauU4q/KHY8WLo6h4lDoaBgiJcklzxfN85knvYHRD5PFkpi3AB0V0MWYLhLaUdTvQ4lGigJoIRXDgWgAnajvj0WeMkGwj5gm9Rww5StkvaXTOxYIfRUyHJQ+VgNgZjoR6H7wR9wPKL7EAIioQQTkHEWQY55LSxxmPv7Pe/JyBoAzdWziyNAutHoEZG3j6fwwJOJrWJOFU6mig/uhylEvSN77xg/XHGRobuR5iqcilmZ55dqYWPNSODPwouh1okTs9Onlkf8TD7PIsv5dY3tDOhDhHSfyOHEcQDYNkdYU08I+fy8D/1FR4mqp2e3NFZI6EGGAsDQ1nZlHIIq1A4JNeoNF5c6lmRQ0oYWAiW5ZGJg1g3xvOx3N0SdsgDqKUAQmu5TMfLdeY0dE4QGnD5Sjp6DcRX4HGRq6HHVUmBoJr4yvC1KJxDjLBYkiHlGEflin3LaswiByRmnjwHncUumnKwEuGHqdLSCQ+jmBPqfbESUhTE5M/HNaFl8yRyI4i0KGQyXIKE5qyCk3OxWIqTUofIikRp/XU9KZiXQYI4ocIC4Qu8AlaGiUQUepHFzXKBMRoSA9SjwUiuuycpbXIsp2mKkn6zmNIAhFuDQSmXAUiTkohzUCMCQiNUla5NDXt7BWxBw8EQOwzR2KSk/10Wq5FDKQAgW8/MXvLOVeRL5PShwIRMXdZlsdSL5UbFQiZmpLiB9c5hqAUn0JHIyoQGqVOTQNaHbN+iBB7YhBDGolwzLXI1JTURXRqOsyss0GKPeEKEBx7jhNAHHBahRylUIWUNkjMAwAB9gP9jnD4yrrLHAk6GI4iZAwGXoZjEszyS2ACQ+M463WQvzE554zSBy51ciTJQOB3BE135E0CJhPEqogfAhaI8DhOzBpxtY4Gaj2ec5TET6FzBOwp0tjI9QRWwfCsD6KZdQCGWrAge26fj5LFOkKp8ETbJ4k6iDYHDwzHwjojOM6ptskRPRQVkxwleuquvW7/Zd3S7lu9Pqz0kc7emyIuj/mMV/xg+XjTWH4IiN/zun7urKP8TC5J/KTj+72mvzXKDkQHogPRgehAdCA6EB2IDkQHogPRgfi7aDp/jxrMl0XqP6HE/SU7EG7l/yFp6Qv3aaZLOeHiyMlxao3SROty9Zbgcmo9p9j/Pce6tOYHp8QfmWxf6nqFtxGc/YfexWFjNCQtIsWP879nDErEfmOZMTTIAFEqoERUaI3w1lnrV1u4ulBvaRlhNAe4vOdJcuYxlwvFkfWTc2sgvGsCMVn+SCJ/rMxCuLgwO1bGIpNuCcbfBEwUCwTdsFcgvDqoTNreFhDeZGi2ZobQGvgVEJcOvBOIlEhaAeHWQDDHIXMdtoHI/JHSPSW2ow71iXc81yeYAsHZlAIIeSDfuRYQJSsDjyZLSzcAkVgKcnmr0oY2EpMrCDmvKG9CWBQXWBLLf0GdV5wMJFI/SJioq4nwGkS3g9kQk0tMjMx1UH4E+4+ckTKq+SMaB9xTWrwNTjShrQkPEuOK4IecqV/MvzjC9yTQFQG+BlJGyzUzyQjRaJ00KiptuCNZq2SijLuXLP6BvVR0jLN7i7BSorAUfEpuJ90MAUI8EAL7/QhvQlgUY07O1ywJECT0QXUDxKTGBlG8RkZ+gJ2SokRuWephIHAeZa6D8iNGCB1wErPmj6T2SGO/ydsY2eCANUImZT5RkpfSmpiaiJ8SxGaBp9JR1UroeGS2R4zWqB4umEQTd4Qe/Q8oFSefgODPSKmKHwu8Z4SVosl6LwIM+MQXpQECXinepOsx+WyzJJhUiYSns0Ac2YZgEGoAWlRJB3GuYCDAcchcB3UsoXaWs/mMGEr+SBRpCfirtHgb4t/AQPA1DcUPiikBAZaIT31MaiU60C6ftj61Z7gj1Ocz+B5Hl4HA1MYp3uTlAp7ICojoUmrfrBFRfHIuANFgSURh4AURw0prBP07CjMjiUskxQ92PHKJ45C5DlmKQs5W4Rha/ojE5rJpR83b0KOHyX4V8Wz3oUCAJYIzPpvAtYFg6xAHMYzEHcH4zOBx5MUaVwSPTfIzQlkAEVRXw7usdhGTgxwETg4zXaLe8Cbs1NRiSagfSuJI6LcmSq+fMjMDBJM0zPEwY0oCxyFzHXx2x6N2+D9X8UeoipD8VVq8DezTE8/JYOsWmiLptAo8HqALwEmFTzUzIjo1wcOF6jTcERpY5n5M4ijjxWlMr1/1YyGvlSCsFFXVYC2m5Cmii9uExZxdQdh5RXkTulhvsySwpA5TYihEmqyE0RCg3xFlOQ8uKX6c+MKelOOQuQ4++bwQx4O5FzRQBX+E6odTynHe4m3Qvuz5gsFW4S/S+JgCWCLHWb7MnFStRIGAvdkRvI6o7RnuCNXN3I8pqVzhJ4e6rqgfS5SyOp7m9e+5/YU2uksvMrJs1+M3ym9vjc5qfNz2e/Um/siqLX8lpvu4H9cjjTdscfdXYl/3siTa5bE1Ft36qlsc6whu2fIAQPzuV7/p14H4YMQdiAeJuAPxIBF3IB4k4g7Eg0TcgXiQiDsQDxJxB+JBIu7B/o9H+wld27pHdD/34/6Xv7DnDwMRq8/H6Tjj3iw8pdulDD+hVLCIZl8oE+xRS0/6OTpwHmg7CbCXd93onmm6uVi2ss5nV0cez2VlVdtNZY2GSEPqbYsl4sqHykppg3B5tFfKICmZk4tG4/Ieca6ElTSDL8IxwgmT2WeACHYwUnnzTKqoits+qku7bTrcCEQUVENT1EHMfKqe10DoyKaUaBOIcAEIv2pU6wjhOhDBAFF1/x1ACNMhmCvGApHYFXENxPhuIMY2ELE+l1dAFLyMPLLvBcJtA+Gcakmcja4FpVUmSQ/VQJBTCkpRst8J88M79VixChYgfnkRXSejemU6HGU/poajKFckzgPn87D3LGocS5QCBEd2QorlaJkgXBKaGJx24U/M7BAgNP1l2wYfBDwW6GyoskaONsfAfUsZt+zxQvs1yXRImiHqVVO3YPqUZeoDJysp5am6FrhOJmYiuKSZAY0K2u5F/UKe8VbGTxBuglGwEJcgBXPMTIcgTIZE6IJyxZQ5G7weTfzoeBTZhZMCwclLcERcZoJAyyCIrkZMj8rTkcfpLWAGsLHhiidmB642qAqosoZVHkkxhGBy0JpaDgF2D+iLbB+zV03dQq5vTGOss298S7QAYhVAbQMPax9EL0ZFGnwSZ2C7gTOSiNmZJStYOMklwyGFnrZXpkMQJoNev9TWMXMe9AIXIEBZRNactvDj8lHtPZQJsrxzx3N+2J5KiP3HSHINjvkSObag5VLWHoOjOgJWeURjGOwagcfiuU1qS89LjUhHZN2C6VOKXmffdEWIWxCzGYRxMF0CIttzZNJAyOoVE4AghxQCQpkOITEjNPilhLkiMo/kTdQ48mINvY5BlEAyE4QjTXPw8o4/CcOEgYAZSZIr0pUpMbtqIIzyiMTgZgsE18AOM2HSFTQYJY8kWlG1UPTJGSBojiOmhuha8PVDahuicBETEJG2e1G/wNWFjoIfoZekKlgwEMXUpEyHwBerM5+gzaGdnAogMOEoEEtkVN5ZHQ6RGpELHlNOYPkUYZgsRznmS+TYpvxlZDzMMkzBAqHR2hhCaExNBMRSC9ERkjJIZn+sWsj1jdlTBms3sTG468xzYC+sKMvhxDoZLJ7D2hnxqNoYJ6hSyBI2CzfBKFhQ1bp0Y7FOnAxevnQwtLxyHhQIrFWoEb8jRAdkxlKYdTgO7PICdoYu1l4WQdHXiI75Ejm2SRd1ovdwH0ibIytr5GhzDOwbIzHmxRpWcUTazBFl9se6hVyfWayvf/n7vNdn/la9lWNxW7n4KdHGd+6rCt1U9GGAuJVjcVu5+MXRPhhjo9/06137z0fbgehA9Gg7EB2IHm0HogPRo+1A9GAflVDzsDSfdUCfxcn4nwPCz5lloW/iZmmrJhHdKsEOXkT7Ntt0ub5o6wgtEQipJus8bMVZbk5p+/Uuy8WIUrvchg+tysMFjZB1vxLnJWyFVxwZQhJDkJBDEg9fn9Wa2vTbQKwavmTDkXkN14HA08BTUirZjHNqAREaQPgkLBESEKDKTEEeSi4i9hc0QrQ3+Xk+5bzYDpeEgejMSRLcFjOiDYUrhq5iOnwuEJUaRwuIG669i0C4Coh5PLgDroy4AcSGRsh6nGKD09QCwmUgBqeeIYmj4aAWodoWpV7HkVU5CPFKgQJC5oXbiKpu0B7LhoDCh3fqjwIWBkTZqZ61Ggck36egPJOjqINYDge8UfJnRAN+ilG/OKk+yCDqGMq9oPwjpibWvDFaGbx/2tYIEa0SKH3MekUM2JbGUVgi0gcZW8fi9VCYUM8Q5WjQFgCoD6ZbvQ55dHyyTAfHXAU+6wq3EVXdoD2WDQE9g6whAF7IiETm2FLjwBUBXRAEh+MThyPG5Fyin4/8oD6y8ZQfg6ZItt3wDi4WITmhsBTG/CZ2MA56GaobMm1rhCCJSsneUSYt4khyenTMSV1lbqBW1XNg1RIkw9UzRKkB6OBwhrZFrdfBQLijs0yH4RxVj6BwGxkSUaBgL4jCh3dH8UeJaSgRyVqNA1cEdEFkaiJ9AJeoA/HIvBNvPjuXdQfQ6pAYIADi6Cwb5C1kAg2nkEvdkOmSRgiPCgusiBZIhAMZ9mdahnI6VCeElDqWEgpEydHwSf8oCH/D6nUACFwRxr0kAWHdRqC6ka8IYS+4dEXIXJmBQCRrNQ69IrRDiJMWVVAH2OGFppzJfF4BEYMyQNIV4TL3YkpA8OAi8qQb4i9qhPAkP/lEckDvdX8BxDganRAZ1QQETwjK0RAgxmQwU+h1BFF4UakGTEjJdaRwGwHJC2tEycgg3oh3SgIbFYix1LYwzA+OGxyOIBe7dSkJkzJH8meJS4h06GUUOo4AES0bRIE4nSA7BPZFSP2/pBESEotLtECi8FvGmIlKIpZR1sqTM82dx+QZohwNuH/QliCESavX4WeocgzOupfEkBZr4zYiqhv65TCxIaDwwRJVabFGHcx+aKhxyHfvqDwTxMm0UI4S1E6MgH52TjkbaBWCLIM4smjPM/diCmBoRCZWcUQu64YEt60RMrCiCJga0AKh3xG80J9E2WRWNgpRrLNPjDpWfuAVb/6FGe/Ymr4o8uuWFuLN9cr+cHsyvyx3LaobOSPh1tZvfn0VEO5Lgbjn/LsPiNtq1hniDwDx/pr7Tb+7AutAPEhgHYgHCawD8SCBdSAeJLAOxIME1oF4kMA6EA8SWAfiwV9/U8B/PY/pHj2Lj3M8/CeVeTwgkuxt+RedUZZF3lOoRuAm/9SuOFZ6FlzHZFs1rym3ss5fL21GfhJuqkvW/ZB4J3ky0PZnvjwCtVLIZrlVnPn+Urw62Fo2rsfKhbaQQqkunPcUqhFG0sRuLMgFqXNh1aktWkENBITe4Qiv6Pv67C9oM96UQW0hvAeIcAGINGpNGpKVejA38UuZCHM23w+EuxOIxPLwV4HwG0AsQxqdZJctEyI7lfjrQLgbgKg8UT4LCHcLEJTMaStagBExGF8SOiMD8xUo7aOeJlaPota7yGwHl7Qv9MjMYzjPeYJqMTqgs38QptARMVJN0/I//fcEpX15rHiCOv2Q2BxBOBy5d9pHVc2oPVEyfyNzVA6mpzpqDgolsv0g8WdVD/VpARCH2Y63MjuipkNbihYi4cCcgzFALcMxFyKwQr56mlg9ClfpXWS2w3EWSRhwGEbRAWA/EtbJ0BINRseBT4FRuHPQGPDi4qD/kjrHmHxQkoOJ9EfjjiZZqzwVVc2wnijW6ySIBwpFApeWkvnhImnsR9muj7FnVQ91pXDOJUeaMaeHA/NLEkGgoWihjAhJuJ/B24DxxFvIniZWj6LWu8hsh3RFiDYF+B3wI3kL5opoMDrYiiZdEYjRZxOOZCoCVoiXJV00D5aohxS3KnSomcZgVDOMJ0rhdQLFkWh8Y3KN1GNqD/YfOnI4WjUMAAR4I9kIRRkmympJQES3UrSY8xUh7IopA5E9TZzRo6j1LjLbIc3fs2pUEDsiCJ/DzOxhzeiQsCejdjG1gAArBL50GYgl6lnjVoWO7GqSVTMsI8V6nWQKjJ7ruUZdNWU8ct2uBkLXEzvefF3MCsS4pWiRp6bDLNNE1A6RUod6mlg9ilrvIrMdErUyKGOELmP4kbwZZzJMAJbRAeMo860pwJppPTUJKyS6bCUjNbk8rFMeWlESEdUM64livU5UGUcmyTFUzA8KirglOuwu8TVkC3xakt1OPvIwJ2YHFuu2ooWQK5mDcUgKFoH5CqTUoZ4mVo+i1ruwHAz91g0uBfgd8COZjBSFLomZ0cGRxPQ7woFTwZGxl4ou1sS4UM8XSL0cE/9E404KHbKoc+2immE9UazXCZcWjgqplgRXMj/4ao08KjJyrBoyq6qH+LTM8KcBo0TcYRKz43h2t7yiu/VVO6ts/1r997amL76Kr693xdl62Vjjh0eiFe2Ha3t/87/3tkH80NFfDcRX1/ZAQPzvxNqB6ED0WDsQ/4VXB6J37j8daweiA9Fj7UD0kP+nB/l/Ewi/ufU2RsgnuL9wGoVepR+Ia33SMjnNri4nVG6o6lgzQAqmRVWnvo9Fu5RX+/d8PPMTy6S/zQSN2I7uwuCsiAGVWUt2dSletHWiVCbv42QW2CCx7tsUVr1t8EYu+aqkx7lMWjtUQPgLQMh9dpdVJyqNiRUQFf8ilO9LIMT2AHf4YwnEWgIivBeIyRU11pY6+V8vbJC4Hva0RcejwRtZcUSsrUi4DkTjihC+w1gB4e8A4pYrwiO9I1lkFYp/FxAlQ+P9QBgplTYQ+rdx+q9USiogwGLIzAS4fqgSBRQviGkhfIc58x3U5QT6E/RYbuYorBkgJdNikESSeo0MwpOwXAu/tAAgDgJEmJDZMt4lK87FgR8eVrcXUu8wEc9Dakl9ZHS7skqyBLuT6Zf9Xlgkfkn2TPTAfN03gMlKIcIQCa7ki3ByTXkga3+XaB4Et44qb6LA4UQx4y09PE7Z3cR3EJcTcUcYcUWMoc0AKZkW+lB94ElD1UAsk4R0QuLJQ9XiBCCyYUDyLllxLqiGKWWkp2W/UfQI6nSSXTO0zpB4FVIurVMiG0HkBFEKqPumQKB/Y+KNOPGOgZmDVx7Iiu/istWEKkaoowo0OIi/QIoXb0HZDYPhZqjLSUwqAIbdsGKAlEyLrKNB2hXK2CiZJAA3EoNj5HicyfwaLkTJuQhy4qD+t1B6uWTvF/WRUX+UYHgVIX2pOECNJEJjIxlTVX0DEMhNR5ez5M4Npl9exjeu/F0MENFVjiqpOVK8UCCWUnOpwNECIroWA2TFtJhtgMrYsEyStFhPUou2OlruSc25UCBQ/1uovFxcYmTIFSEECJVvmYXwpFPThClR5RoyELZvWCNaQGSJCDU1UZ8Xy3fhb01makqOKm+qwDHGxtQ0GT8SA8SoF3+bAVIzLWRqwlmfpybDJElARAvEyIn8xKJYcS6oBnZQ4b3EsDCKHk6dTrKPTGKiqG6M6mOgj9w2PFPyYl33jQRRwObw5opVBogAgX6O4vNS8F343MyLdXZUgf0ZtCuOuvilxVr5DsnlJIjqxGTYDSsGSMm0OIpNGIWV1UBKJgkUj9QuiYSCsLA6w4VYcy6CyHrJ3qhAiM2UOJ1kHxlwRLI6xlLPFFy5WPOkssSJxZrUM1bsFgEtM0kyA8RJhFDqAM2k5Lu867X9O/U6UyG6z3jFcF+drYjjhWMv/VL2G1KN8d3xv/t1zy2OrwGi1Lz4fCDe17f3xv9bgHiMV7/p1yPu3fq7I+5A9G71iDsQHYgecQeiA9Ej/g8O8O8Yjz8wKrbJ38Ze6EA0G9YU+6bCRgHDlNkL5SsWn/TOCpsUnAdRL+Y99Edu/9OLHbNlWyzrSt4UDXZIek2bMbhVxpjLnrdKHZcEAaWTdFQcO3pTtaypLFGHjRhb7ckYzJseNqEFxFTW3D7z9fnNa0Aks+/CgB7Q+AIItwIirDrp7wLC5xgaQPjtUhMB4QsgxOk8JMZKUhnZBkKtSlILoe1hUwOhXIepVfNquMN9QBwaQLhPBSJeiqHh9eO3SwkQ6p7i5P+YAzIQ/goQ9fiFLQ8bq4hhfUKU10AcA1KTUKUN8Sc5I1vsRPViKHQoVK1Cp6VsDeMomza6oMwGylvrQ7wJiJhYFparcVL2xZodckhciEAP9Z4RnZRwydflhKMQPWWol/fTVim5FqZBXFRAI8P5K4wVimayvJUcIzgbWjO7uTiVcm972BSKGNYnBKlIeD+EJO8PjxF7RUD1ImtcWLUKlvsIBRCR05xR3VbkUXUxPBAgJk2aFlyNUdkXa3aIcC/ETUWjG4PyQzRRyY/Yp/20Z9guNQMIdU9h6tjs2G9FzQiWaCbLW8kxQm8E3JYxJX2ZojC3PWwKRQzrEwK+A6tXCBCwtgAnoQSCGRhJ4yKrVWBdsEAsyzt1e0LCnBKfmb+hQByd8jlKroayL1rsEJzhdLyNTvkh6utyPAc7TVDZaauUXhGqJ6JrRDY64SgnmVqqGDW5G8W8x3JW2h42hSKGUc6YMHNhDx0qShvK2UlTk1AHksaFVavgVzE18ciGIDyHWLi5mCtC2VYFV0NXlzU7ZAT1jEcfoGSJF+GWwBVFuHVSIsbkcdQoNUFbR/VEzGJtWCdpsa5izCyPDIRyVtoeNoUihlHOmISFkFkNkw7OYa6/NSnLAcILWa1Crxo0rewHdskQ/kWemmRSnIQ1Ma64GqOyL9bsENG04DUo2qlJeBNK78IwmC99MnE0SwkQ2TVGThcFIkXdihGclMMsQIQ8NYUND5tCEcMoZ0SwEKB74Xlek8X6lL9n+EmETELWuLBqFbpYi1rGBO8QcCAM6VA8VLjEJKyJ04qrcVL2xZodEkUNZFk4DRCHxJsQX5cTlkoLhDtvlVIrqyEJu4jvCY+ZRs1tNmIU/ouDwwqrbcx5sW552BRf3q79hoR+RrxYpr3XG+6Du7nEJa5D64vqx37nr9VBaDyuucb4u8fihtdtP+bjB/be97rEdYhf1OZ6PN5/i+OLgfjfeT3ETb/+6kB0IDoQHYgOxLrh/upAdCBsw/3VgehA3BrU76v9enuPf9p8SYQdiD80ZlYxI7qcQrf+IerETDoZ9Mju8nnSZ85YQWNCqYmTIMrfkHuw4pBN3IjjVIZMmbx1J2r+w3JvNG7wJ8rXJQUM5pXQ47t0PxQR2723jEwuGosjp1uACLbTjZfeCQ1JWUKBCImXEOwjuZE1+vHsqGamRK7BUx7eJk1CfkrZIy9fAMEt8BbvLIOk5j8QEK20fXmUq9gUeZMMGx5j1lRNRNlgPFuujQylS8MVIPzWaF9/ls7a0QgQUZNULj0DnXJplO5Jj9vSdm+ycKSuPkqHU9f56ejLQLhq8Er+wxYQq/41traAGE2mMFw0PS9HBkm0NRDxvitC+QtJpUJYDV5F2uVRWeUyBKe8BMr3GiCmy0AcVkAcCiCG1DqS60uCkl1T4EfS4j9QN9f8ifqomk1xZoZJVtvIQBykN8qruHVkKKZjw5EmuNwr1ecYTH0Hw/hwifeQxPQ10+pTkhNMCeUyBIf0Ivmf0JrAvISzpNNn/UQZO+wDEMv703GC4AgfRbWeAMQyL7M7lz5sjlQiAeGdui+0+A8ERIM/UR1Vsyne0sP0quRxnCgeUv04TgACvIpbR8Y7Tf+XjjTcB0MhGFPCFKyZMdk3jJnmMZzB2ojpfPYO2hw8VC5zGRAAGBMHl4026OhDvj6CvSKIWLAMleji8FF4/t9eEedBnFkKIFwe0pr/ACBq/kR51JpNQUCIi4kqeegaMZbp/FtHBkAoRyPriHCvKMFKcluJ+ABWC6K1bJWU1BaVCtNc1GF3uCJi3qJMnYmmHJ2M1M1nA4igQEDbwkxgHDJpYIgzi6wRYdoCQt1aWvyJ6qgVm4KAEBeTENeL9QqI6yMji/W8cqShb37kHrT0KwMhXiyyRq6BUOJKOn/0IuIqok8pdAFC/UKWC9oAEdvfmhQIWUNi61uT6nyISpECIX4ka/4DM/Aa/In6qJpN8WaocOk7XPGtSXkVt42MANFwpCFDNXs9xjw1OY1KGR8OahdwFgnQ5JDv91h+/AxliUwqybyEiAlhVqEe/jtl7Qp8Kyc9PqhpJB54sL8jmGNEF7HofAT6vcBnk+clEH4ka/7DwFfEmj9RH1WzKSb2afFpsSY+CP+WSb8jlFdx28g4dt3y89qRhoDQxbrUHwnChtHF+g5BCL/9VexTVCWu/bJeMyziHb9bo/szr8sRxq+o9KuB+FhE7+xyB+KzgfhTr37TrwPRgehAdCA6EB2IDkQHogPxmyLqQHQg/t4gH5L28pv63oH4/L5Hl7Ul8pb6NdR7wsobxZSzdVv1DLWEt4n1VWsf1sxARrahmXHxBmW0Navt+i3lVqWGQktkdWzQSFc7S0WMVuK8dIyWYs3n4nwxVjFtC07dRtpAfKpmxuyamhlrIw0b82qAJzsSmZ1wHQjfBMJvAJEoEdG5FRBbA1wD4d4BBJ+um1fEZ2hm+OiamhlbQLjfAkRxRcSyGic8B8qlquk8tDmmNnfhkNIcoorhwFlASVHXkGNKbxVxO5mgl0EMh6/TzMDz32vNDPTvPKsSiDqwZG4FqfmrNog3ni2ULDrw33W5IXE3IG1P+Uw8nqxbsy8NdEY0UtE1AeMksgeIh6MBcyHAYGhzF9RxJIoqBjQ6RAVAHmGPyTMke6s4UfWArAjVn6+Iz9fMAMllrZlBcizwk0i1GG4FFEXkMfoRUKZ0auT3UdkftpzwMSIYIEgs44F9J1uzLw10RlKk3Lrk16PLwwwuREiGHGvugjqOHEUVAxod8FkZZPJBHWArqLcK1+pEu4I1MMzU9OmaGTG2NTNIveN4fpOTZlDjjqSTgeFUdoW3ni0xyN91ueGcrG/guhF1NIezKJiIL00UnZEUaYhbQEQ5T3SGr7kL6jgSnQKRGBoz8xiEWBaUrZC8VbQlx3oZlJP+Os0MVhpraGagLwoEahlD1snQAbZiD9Jrc0XU5YT0wFolJRC6dcxECR6tFGkBhE48o3IhaFoZpjZ3QUlgeWoKUy6lUxOrcYTCW2UMqR6WXgjF1PTJmhkqcFVrZhDPA1NTqiXAdStPTTFPOVP2bCFiBq0RoVEOBDZolZCjik5NPA68lfRJZGQipiaJNCgf5A2sAyhiKBeCmRGuzV1IjiMzVDGOYmaWF2ss5PAjMd4qJ6h6JL0MA8Tna2bI6bbSzCCeB3nDDLkW4h0lwiRzK0IS8orZs4VHxcFrpi4HSie+zGAxxmiyNw22OvWlAQMlM2L4NBXGyeUfN8Wr5FKsmRW3vPTr7ft/B3+uZka8ud347l7HRrwP83qUWxzxC0p2IP6SVweiA9GB+Kq+dyAepO8diAfpewfiQfregXiQvncgHqTvHYgH6fsdjdzX7GMO59ecNr9+fWFvPwOIrftCf86/5TcCcYng0GJr5IztUJS6RVVj4xXBxQjk5tIOZ7ocXVzFVkdPd09XpS91/nzkxCs9DVedFHP5ie/SNisv2whOOSdHALFb/mdL+cZxqjHhLwJR6ky0xRyCa45RNcxegbiS5i8/Z5WNNhAFnSFkl43YGKS6r56zn5x0CuXeYO/Ty1Olvjplil4HrTFxTiYGYr8Cwje6+ncD4ddAuGtAlPUaIAohGb9ySamBGC4C4TMQLwsUfOHO4CgIz8EoYjjWmMjMDKcsBPwvcRiUt+HlMfS1qobhS/C746z+LKa+2Uu2jaPa4FtYLQ2UOMzKEWnxMGx8eERYa1KGxoSHhU+JdSLpIzBZcHIACNXngKB84NZI74NGT4HQVBqJAVS9Vm7KpJwTz1PTk3Mvz6/7vWRggzZpFDHS1JQe10652mAShcLbGIMC0VDVSHwJvsBDSP4spr5ggdjiW1gtjcglxpTPXvMr4Ojisw9MwJAFwzCZ0JcYxdqCeSkRj8zzrD8DCM4HcsqWUqHEEQncPlLw8uy1fZB/KnudWCeTck7cTEDs3JPb7xYg+EH1GGRWs4oYCgRxJwZhT7BqACf1k5KE8DZiAmKtqpH5EvSOGR1xcLY+6FUYIDb4FoWWhgNUXpxfWjwMG5/jloMoZShD4w2P6kfoa4iJj7jD2CvCifKBqA3IdKRA6BVBSeaBwR9c2evETZmUc6JXxA5AROEoKM8hK2IoEHyu4un4GQMn7I4sAyQUBGjGrFU1Ml8C79jLY7b1MQdE1gjntvkWhZaGS1eEzPg1v0KByJJdQXxiMsPkjcsOU3QKBE08Ma+OUw3EMF0Cgvs+uime6l7riFSL9ctegMDURBhOwuNIihie/4I7cXlqAkuBkuUtVQ3lS3jhXwSux1dTEwExTDo1tfkWVksDU1OqtcXDcDY+dpoSC6vMMGHehejSYDKNyc5HgRikD3JFyGJtgAgZCGZ+nRYgYt1rVRNRzol+a3oBEFh42VUlLZJQxKA5lhnVp2PiaOh+u1gfhdWwzMis2bFW1VC+xHKuMf8CHAz6ZOqbmXeRF+sm38JqadAx9JXmKLySBg/D2fgce734GUoZytAA72IQfY0gfi3iz8K/IxxHBecZUQLhxZpYIqz3gdGbXTqWMEKUttdgnYTEOdHfES/u+fX5H3fpN8i9rxj8hV/Wcf0dfV3Dhd+w8Y6t90du38dQxldGFYP9qp9ZHfldUduqn5lz8mW3OOisuATELa8/A4T93YyrYzuqSz4v6xJxo4d/xb2mx3v9j97060B0IDoQX9HlDkQHogPRgehAdCA6EB2IzwXiUjOfFcIjDrCN6SHi60A8RHx7vVh+0t9n2frM755/gmfw45U35jT3bquuq53eaSH6Z1ce0K5/470emut4LivbL2lI9+Mn/Y/+fXp9enFLPuzphbbs9jvHfctjECAVcZyOa+WPm18NLZKozjO4d0s3B0Ur2aa1ic2xJOz2FCK9nmT7E7/7sQcQewS8e9oAQjkhe6nBjuhTDcRLxkMHcWf+2a0Gf1fstG2VQDwVoe32zxLaXmqhN8/0l///5JhDkcZAgUCyqn617hWX7iGWqhAKo4+cqJKUcHrg2Ahp0EBdAWJJIe13l6+I1eCsCt4ExL4JhG18fw8Q31BszwX2e67oG/3dJyBe8hhQPgiDFG+8p9syNroMBFxkzK30fPAy6bz8+kUX7xMlKF6pM/g3AfGypPN2tO35n2f3jZMYy7vl88/lv2+vVP7H8r9n/bSU4U//LGXpnaPtv359e0UdTw7bl1I/He97lqOJz0DHVvW/YO8PKWHbomNlWuKYtRzqZmCW6KkkUpKOU/W6hTLGxKDAGJA+BgYJSiBIMx0qTxR6HYRhcpw1HXSgzw6P+SIZdZyVO5INEeDagjZUHYSYIcxpWWLb6xWxpOx48llC3QGI5z0F+7TARdv235/cd0rruf13mq7+b/nv+04uezoF+ROVQUKcNoOl8H23tLHLddD27zjFdyiFtqnkjloz9Ut6ncs8VW3tdNJMx6McZjOK+2kZZG5pGfz9stZRmadXbCEOBcfJY+BdVsgY9XH9GCpPFGKGBGGYhGRCEMIbdEIw5eBR/KC6Buo8Q64t7CNAAIo6iPBGAnUqXREJiGWRBhBPBNJS5tuy2D3/lK4SELvnZcGjgaJePP+kMsv+dM4tg/mTOkh7UC/Vk+ug7XuZa1Bq2Ufv+Niyfqpb9+6qtnbmy8WyHFOrDNsuT1UMHk6PdEVgduMrQoB4gT6GnK0izbAs2jFUniiYT8AwCeKtQvyMN4aKGC/KFhnO6TpIkhlEE9Irgqcu5MGJQ2LXiAzEfpeB2NNncA0wiC84//ZOgcBg0ZYMxALmC4YkA4E66AzFeUvw68AtAbzSf98FiFz/HuvJ645LlG3RNkFi2fdELUh9aWpKQOwrILBGAIgnUfIogaB8cxCOSQkEGCZBvFWIn/EW7FIcjDncskaI8wwYVnaxBhCs2EFX8XpqoglEgXghVuCettHWPa+N9B5TBwOxjN6O9qeu7nQAlz0GCK6DJ4nvOqkwCPxpvyM+w7fX3a6s/8Vh7w4lTFuYtJar4aWYmnb2igDkclJwL2nk9wrEHlPoLglIOBE+UsJNcKUnigCBqSmo4kgCYtQJB7wNAUKcZ8pvTVJS9Ezc06v+jtCljhe+f37wuwTEjpfqf2iteMZiuiynL8t7Hoh/lu/otJ8/UZknXk53vIcWTwCBOug7C20HEFqKyhOfYTmvq/pfHPY+oYRpi4aXo5HF+kcqt9MvIqj/lSc1/h2xrBIv/MUAX1m5XxgDAgK/I9S+TNgchScKgADDxAv3g0pN7MBCjBfwOsDbgJyNOs/gdwSWc1UHIWaI1yz3fb+s97ubvttV5Ww9+0tHudtfTzcfs7/at/YY3GE6c9t33dUX3SuDfWmffmG89irLfQUQH31dB+I6Y+PLArpn32e18ade/aZfB6ID0YHoQHQgOhBfUE8H4gvb6EB0IDoQHYi/bNi+JhKt9eGFVDoQf/RFDITj/O95CjmVvvEqbmH5qdCmKPaRQ/R7n+uJztmkvmvWdPdtOYpWa821XxbWXbUdY3P0GmXD1khf2K93yltA+Avdp+dkQ2jvQ8TRbG4l21t7wh1AbHMsGvIRsQWEb/dzC4gpmnZKqYkWEN6tZSt0v98GIsbMabjlioilDcpq3yUg3HUglF1xAYjWIG61J/oIXGuu3V+9NreBcFeBcBeAyJfJrE4pCkSYMqdhqFgM8rfQygAQqk1R7eOIDjNEysFwQGvQsRg4yXLI+h8zPFpULSNHckjOJYgDR9FjvEuSBj2YskIHKWRk2XgVW9dotdZcu2dVDpo0wOBI7A15cDi3DbGHg3I3qH6nvjHQ24C+iXitnNQJ5igPSaqku8R0GtLjk5LuE56bCJlkTkPJYpC/lVaGs9oUxT6sEaNwHo4zHoSn1vCwONU7sSdJUIWNaNUyciQj7BJI44LrwVH0YPtEJ09KQkKPANYbqv6hD85rtFprrt2zKge4R8dZ2kp2DLbtxFBi7oak/xN9jBkgXBbtwdRAbBcgbYJINSZYMUBrA0wEMBCiBKichqFiMehfq5WhOhYCRLFP1ojEeRhckNYUCC2bXEaiVcuw7AreIloagBqaG5QvRu1ZoQP8C1X/gBFDjlZrzbV7pgO45JOBjHVyijFtDw6lwN2gv14NqQQ44m/Qg/LHsz2NgyzRFJc3MbFuxznN8Oo7okAop4FUNSyLIf/NWhmi7JKAqPc5Nffg+oK0RmXD5JkRMRogwmTVMiy7IsClx2UgwJMAEDo3mytC2uPrZs6xu1Rrrr0EYhQgojrF2LZnJ+y9wEoeJRDit+T4inDZo0ldZtTVLMeUlE7o4g6ZgSBAJE5DKFkMekGVWhkFEOW+SDP3uKTVQ9LVQGsJiHA6HWbV7oA3SVbLyJHI4CSRIFbR4KnpdIIfEKuJiEIHqGHqvgIJiOz6orXm2j2rcigQh9n6yZRtCzNDuBsQFMpT02EWmaIoYzceZgHCONUQw0liGgUUx1ZdzFVgBoIFwi7WymKQv4VWhgKh2hR235G1vJYFMHg1SAMz4jQJC25gjRrV/8BCltUyciTsqcLbeEKlowIWa0hX0ZKYFTrg4aLuK04Wa5lWJ6011+5BfHTwbInJT0YX69w2L6wR78HlyNRL0t44JP4GH3GK/IUhcv/VqeaYYzqpmVzz9Zm/OKP5Gnu5zEcjiZv1la37q7+s4wd7u7Ulbsb0G4DIX5K/Gojt+urW/xQQt4/IlwHx3le/6deB6EB0IL64+x2IB+l+B+JBut+BeJDudyAepPsdiAfpfgfiITr/NbE8/pB+ILIOxB8AIiJdIbn/5IqSqAOxLH7hRlXJYsC/Q+sIdVKYbAt6FFgk9O5fVsS4xidZ0wC4CQh/NyPXfbEah/qVeSnIMq5f2oMcc6zq2+7BUI+mpH+CZSLkQzXdEm4Awq+AgJ/7FhAhtoAAiwT1ld3wVUvhAhDLEE11c0ktQ/bFYn9chZh5KZk748sS2gOJOVRA1DwU24gvYtoGQrgNl4HwV4BwF4GIYQsIdNxXLI5WNb7Bx+Bz9Hw8r4HI+/wtQORBDe3W89PSiHkTiEYPfD2acCQhfXd9T1kx5Tao78hxnpRzkBgaypqwHJBhVp4D/0vMhhNzOCK2qt+JsilqhoSySBBqyeIYuB7UC84HRbDmY+hZXypmZNUL2se5vIhUzzG1L0L0wiexninoT9LNcPB00R5ozMc59eXU4qE4cXxRHgeL3HP/lxZGeVCbgBiFXZC5Deo7EoTjYBkacj4UHBDN0YLB4CF6QPujbBW/kyB5vZohoSwSAFGyOOBm4uB1EpC89A0+Bg8iZzKtYkZWvaB9HOuU7RXQvqZTR56VMi+F1wj2QxHdDJm3YmKw5NGCggf1c81DobweBQDNDiRIqVfcLziSQBuCGQVnOlS5Deo7EiS1bhkaWJxLDsjxnEwz5Ol6oSZE4SyI3wl39OxWDIm0sjhlSlieBXVjYNYERQlWxJqPkScPq5hhVC90iuJ+D5JtPzJjA6wVyZkbhwjV19AahrIH6bSNmtuOKpdieyBAiGaHur7AwQPzm8x2enVkboP6jmQgDENj1vnTckC2gWDOgvid6Pm0YkhUQMSCZ6HEAi/0HDp6zcfIQFjFDKN6obP48p6YFKKbQXuoRzNsPEpeiupraA0xFD1YATFMMax5KBkIHXX0iq+I0WhIsPvHOJwyt0F9R0I4nWr2hrAmSg5I4j2gMa/7o3I6oniLFFOTZWe4PBWVLA4BgtUrML3FJh/DAGEUM4zqhSyVfJWeEqskGqKQENzsFcFuLKmGONrJNQER0hXhVKXD9sAAMeapKTLnRKiApAlBFYJdYLgNQgz0alhmGBrCmig4IIOhNxKvj+uj/SclGMLvRIGoGRLaKYKYZXcMi4OZEKJeQfV4WaxrPkYGwipmlKoXIN6Q94mySrg/qQd+trwUWiPo+FwDcUTMYq2nDpOChsTiqHkocG/hqSlxN6hX0VUsDvkiGW79lRrdV73ygF6Oxb/zl7W/8MX0M1623hxZjDeOIzbwF66HeX3tLY6v0tqw9ebI4Nd1AxC3dv6/A8Qj9OBrDu1AdCAeJ+YORAeiA9GB6EB0IDoQHYgOxN89mg8U0MMzmToQD/OK+W3FSMgvf3M9+WjlQFzgY4T2py32xEbU+j7oOz/ZPDoYJVfZHje0KEfxjXN+Yg+PSE5LLnwiZXxOLp+dpLraYzlsjUGogAjVwb4CIqz+roGQtHx8LxDl3UrfpJBcAiLEDIQyStpsD+9qR5R1gCGNkxw1IYehQFC+K0RkNjQKvx7bYjyLAb8NCPd+IEICYkvfYwuIcKnYdSBisFeEZ67KNtvjUkslEHLUFKcxQAFBHh2OkhiTJk3m7joQlM45zOAy8CXD+hGZv7HW4sCjtUfRuyjVNTKng87mozA3UEul73FSZQskSFT7AiyPfEXkR2wHp+oapMLhHLgkWZujrE2zZyWjZJjWbA/hhZygwgFdDfQo/3eUbeg3rgjyhTgwEAcFYjJATPxQM/NF7FgeJMWmyh1gxiz1E84jP2s+CuRjQHpzNKnDaLQ4INPA+6NoBSR1DaQ+VYsC/AlwICybAQ+Cwx5AH1pX7QvI/eMEoSNz8tU7VdfAOfhGzSb2h3nqf3Qpdv1PGSUttoc8sh7ZESVl2alTo9QZNIct/cZYL+VdPHE69kS+Whijo2QowYoB5HYsg0xdqlPAp1sISpQiVgImguEcQ2YgrLU4RNMCmVc+xKprDFIbtChk4Oi8Xuoo9T2guQG9i+RfIsn3wawRA08Eg6wRqB+Av3HUyv5Q/ghqy6obYFlkIsOa7aH8DtUPwEPpquCRVUI4hx1DmjDZKQUZ/NGJNsfxfBCJE6EaTTKdpbEMwgNR5Q5OoJ4FiDGxf1zarfyNWotDNC3MFVGoa8zK9MhXhPw3l/oefEWI3kVigggQbjZrxCyKGFisZwsEl07sj6gtA4jRsiwsEDXbA0AMkwJBRwizY4lk3ASCnVKCRqNt07YMRFqs01gG4YHkK2KA5EVg5Yk4hsTKsFNFS4tDgBC9i1JdA1OTalGEpE9DW0p9D1ojktqFMkEwtKN2NelZ6NQkEYBR8gahlaTNYWsj7Q87NSmjRMkGbn1FGLEfZpNhJEJWCeFtovAhQHAPTqcCiMRp8sk3xY5lEB6IKnfwaUKcEGYjMCsBUwJ4Fpm/sdbi4G1UXrgMVl1DFuvEYhhm5UBYNgMvvgGldHlV7Yvg8mLNR6bF+qh8jBldnYIwIkSbw9Z24MUvx6uMkqHB9qDjjzP0MvQLBveImSdZJQTbfPJV0d8RiEanxeUv/44gxxT1TbFjyVffnJU7dDSLr4DR3fO6r/TqN+0X8Se2vvpqzJuiD+Fzell/Qb0uUWdafl9jjw3EFj9jC4it8vf28v4x/M2uLf1e08O8OhAdiIeLog9BB6ID0YF4wCj6EHQgOhAdiAeM4sGCe8xB+YNRfVXT/sq+DsS66SqtHzdKRpEoN2UkQ9w8YlptSflk2ncvo2JI/AnKiMmj5ok7QXc0aeu//JkcUmjr8TyxYcK/Z7o3+q/wKkodkBxbYzRKN4zi39jYVpYckl1DayyvAhEqeYT8Phq+ww1AVH30FgjnNvUz1pQEvZOZ+BMpAZS5E07M7Ze8RYxBZaYjP6qfH2KWVE2hAyIRBL+KSiUsQiEUEa4CkUpIJmeFaJucUfMrvgoIVwCxzahocUNwROJP5Exc4k4IEMbQHk9qz6MCgcy12pVcuCLsaFRA+PcA4W4AouZXZA2J2v3kyKwDebx1duIpAmWNQbgJpa6Gc5UehlO+BqdsprZ+BkpnfxXmPUBFwyl/ogBiugwEVPVpOA60FTochQ6I8DfE2yRHdZ4RMUV0VI7JlMenUhGJWYlES2QgyrFcs2TkofJ8pmQNiZKfAbLTGLx5UB2MjwgPkSlmJ5KIepAkzHoYePyds8DRbepnSAY3+avQzMPqLzzp4Eia/TnrZrgTNFiUHcNnz89EU77sLeAJ7eWIEwGBfmYdEBfA35CH0M1ovLG9AFFd+IoIYIPk8SlVRJDBVu2S0Tz2zzNGMZZrlgxzNKxrScngsO4n4DV4o6EBxscAhY6keKH8DDRn9TAURFpeMQm09DPggSJuL0jXMxBQ+8AUdXAx1ZW4E7zVcYSHrF3AZhuyRpA6woRpMOuAUL3qK1GOxpucdohmOMc022N8ShWRI0+cSDjnEumKKMZyzZJhhoR1LSkZHIX7iZ3xZoPlHINoWYicA/EUUshGD0M7HJ0C0dLPgAeKzMpEUGC7D0q5p5LEn3CHmjvBjA4HmsOUgZiCVRxRhSVndEAyEOVovAVvdDh0OszjU6mIUK/p2ppziYMZMTuWa5ZMpXehyhux5X7CxkXeciNG+U/oYupEEkRyp9bDwNQUZWpqMSqCkrBG9VcxQIwGCKLinEruBKtryLDGWHxrKoCopia1WzJTk0aFqQnKG0F6yp4oDZ8VFXwBxzCN4CnDnMfS1pFOZyxPmV+RNSRW7ids5aW0R56aRFlD3s3qROKlnloPA8ocUbxWhg39DHigqL8Kv4MGBqt94Hyhy1oXQ+VOYKIa5rxY6++IyDFhPQmuXKwD1wuXGCzIOSr+BXKC8oYXbgr/jpLxqVREAngZol2SeBvHlZOMrSOzVNxn/paMF/bBhDJbUcaPtvtO+sG1RwsuRxXvG4FwVx2/B4ivbPczXx+LKro/1vTf1+7DRtWB6EB0IDoQHYhH6HIH4kG63IF4kC53IB6kyx2IB+ny3xbVowX1PxtPB+KPxBNbDUkivdKOKNQ66N5kqz4cA2eUW1tslyCzgYkFz9syEuv2s5HH9TaGG6Ibip6vx6eynChqyI4rF9pvUQZy+r7lZ6IFDMUAec2VlkoSW78yzNv3Q3MSfsm/sOB5BqIgNqy6qdSc0LDoqD/7i227MkbjghIuABGK6NUlxiiKxEbdF4EQRsNFIDZ4F7cC4a4DsXRmHmPBsAgXhjc/Ivg+IPxFIJwFQuO5AkQWeAnN1pxqbFheQVbayIwGdQBpqXUESehw4qZwDUEzud5sX49WLYui1qso2BB4utpZvollRWT3E33AV4HQGig9dZAYjb4Hp218Uhg5NLkayl7h1I9xQbHjw14zJ3WOGZJ6h3VcIQ0RVRRZjZNqbBS8gqQrYP1MQOJyDbWOIz+jjIRh6RqCFnK95rF0ns4si4KSh1JPMhHQNmgQ3pYsnOWbWFZEdj/RR95Vz0NrmCRhG1if3uVksGS5E2MkQJvDuMkoeyM9Tq8uKHZ8JG0szjFO1DvwCY4rZA8UgiqKrMYpSTtY5kFS2qj9TPiKqNQ6jucgShoGCHEN0RbAV+CcsrAWVC8gsyiCK/UqLBvCBWaDFHoeJcNEvR4Gp2ocaWriGkBhGIQmR9ogylOBuwlfdZQ6lsismwySrNTv0gUlFPwX6IuIbMA52BFN9Bx2oODWVuNUAhFdfUVYPxOovddqHdEJP6ECIgbrjOKYkcGKGcxayEBoel1m1KxXMRuuAwSqLO+jYpgoEND5KIGIToFw4lHB3BOpJTGPGAiNzLrJZLuT0gXFjo+SGMag7BA7QiUQsTVOAKLiFSSljdLPJLq2WkdI7AtXuYZ4A/VqanIliyK4Uq+i5JMoEJlvUjBMjA0NJCzStyapgfh+zDDJ/BPhViQWRxqYUChoBPY8gddM6YJix0enJlGKGoONXoA4MRFC1DyKcXIgUsaaV5CUNqyfCeSzWmodIbEvVq4hkzqjOHEayf+jaCyLIrhSr6JkQxAQ9Dsi800swyS7n0DnI/+O0BooLmaYmEhQC9gbyhgRPZApH8lMDfGaKV1Q7PhgsV7GxjiupLZ1HYkhKYqU43ThayS2lL8cb1EirHX4Pvd14y/ZD8tL+OZoWL2OuBHP9RFqjdOlgo2GblGN2HIN+buAaI+G7f9HgbhznPq9pgeJpwPRgejx9I53IHo8veMdiB5P73gHosfzPxXSH43qj7XcgfhoyzG9K+7QlMyGqzdvwqZNR4PdcJz89cpjtffm6AazqR1V6dRC91L/PeM+UkwV6n2lgfU96G+641T1qu37gpZdU8ujLfY7hUZ/QnX/0W902ytxoALCkAkaSfUYbwBi2gZiKzqjPbzS1fBF+7ZpX6emCoMTSfgEY5ijCh2hirQI0gLhvx4IZ4EwChZXgAhfA4T5tOniEoqBABDjLUDEgvxzCxDR2MoUQIytHnu+NA/CfVChc8gnCGODEhncmNWZEJUNJyoVJ8uvKPxWGuwGOKWUah4li0K5H1nr43p0YFfw470R/ihFVIl5kZ1aoPfhWaMDSX6J8mRlXMhLYMnLTRqlZ00R5bQ48U7JnJAz14KWqS5ibqheB+XvjudDGIwSB0ZffErGxH0IbCoVkkoAXErEkIJ5Ftlt5C14dVoYC78U67fSYDfgmWjlR1CJULEo1KRA916PDkM64kn/SOlHisCyUJR5kZ1aOJ9JdZ30ihC+yWiNjsgCYRn0SR1lYICAiBQIZaBQJW8cFVpG0pQj4qPH9Bi8jkfS7/CSbwY/gVQyBuZjpDwue56oMwhYDeo2okDQsYWeh/FbabEb4JSi/AjoadQsCl/svSU6zauj22CdVCwUjSkq20OAMFlz8FgyEEvKNWCdAXFgiUF4KdFMTcFwQt5YnwQtZ78W6/SCjDkfcR6cuhJNlL4fVXZnaZg2ByPXECsbr+w2kq4IR0AYPkPhQLRmN6gSRkguKBmIxKKY7N5bolsDQSITloWSY3Jge4TVYi08lmJpKoBA/jnrcigQPPvPoDCMyT9GgVB1ErqOcHqllmaGhTscaB7LFz8mA++Gk/VTGSblWWS3EQZCJo5Cz8P6rTTYDVAbSy4qY8ySQC4R1Sa791p0Bog0NcWC9ZGZF9apJYQCCGVdjPhCwEPuopwHyvvIU5NMe1NmoARmlVBftWU6A31ipajTS1KYGiGfwe/97FitIi2HJ+I2wJBLl8cgU0npNkJaFaxSwcui8UuxfisNdgOcUoQfwe2JJ0piUeBkyXuvRZd/EQxwTjnhvWWhKPMiO7Usgzpjjy8VNLitYaL/8a+I5d/g1FFGvFnoP5SYMgPFM6uE+qpAUJ+OSZ1EnV6G1BJxTsSna4udEZuygluv+pdkXf7a8fe+YrilTMVCuasFXebvLxFd+5f1v+dW3GYLvG7q131OH78biJsYJfNHgPjASeLucW75ZEeVfq/pQVruQHQgOhCP0eUOxIN0uQPxIF3uQDxIlzsQD9LlDsSDdLkD8XDBfbz+r4nw4UDpQFx4xeJTS5VigIvK3DwsrEtv154S6lciiZs1rF5F9vnXr6EVR0o/47k60vXwVhojKyuQ1odr64roq9obzNbY7kvIQFiNjFLPoRZIaN1LVJdy/YRsga3aJvNXkut3AxGuAhEaggzwLPLOuLfUZSbcZEY+1uyfdEhgpBCCPaz2dQk1dyJvndp98QUQqXdTTuLeB0TZrwoIdysQcfsUvwcI9y4goiQ6bDKpBqKmjYRiFJT3UZ8IGJPhTiCKK+KQOBnwLNG740PSoVAghMUgaXpR0TjRUcdZFT2ga1H7igziq5KYDZm7wBoZkGg4zKWfC9QzlP+AWpPjirZ30uhhHuWdqm3kOPiB3ORe4SRhjzgOs1oZ8JBMNLkhSaWOLkethZJAIstO7xGJKnNAmj5OGo1qi1BfqNZaweTAozo4FWBa9sAVZBQfFJ80LDS1p4EyRyHo1OTVTWSEaL8qekgNYlOQ1Tocmxcos8FwFygjKv+OofRzEfWMKXE8wiisDdPeqNErEJp2zXEo2yIBERMvJeT+YUioBvR+NMoIIajhwSjuLPLYelLmwNY4jaL4MYrjDPUFQJQKJup2pC0veyQjzEAMzlwRLluziGhOYlegU3JcVFaFptZJH0NTq6rCQdwGelxdfFIMd+GNfVkg0QCmQ6meoX4lQiQIx3PRnkQxOAVCS2scmW2hffNTuiLOMdRXhFcnl4B/0W/YUGmvMZdQ/SoIASGJQRxelChA7UcBolQwiTUQoQDCzWBH8Gsur4gSCDeNm0AIO2IuXEPCW1LSKLkLb3LOvgVlRpTqGcmvRKN0ayCINaFrREzuYIhDSTB5jcj6F6I2Uy/WHDGuCHV3iW4NhDAA89YpnkbDORmDAkRAlAomVPoCEFHYEbqkgZm2AoK/IcA7Raam4A3HIbIfSqWkMb4Fy2zI3IXTiTQunHhj2aNEPSMmjocRGErtjVDKQJQyNVVxZCIYfVdiqKK84zhaQFCdQXkZMpzenPvCBhtVmUOAYK5KyGM3YlLjuAoFkxIIr1OTSkINwo7Ii3UU1gIWa2bJQb8vwjuF+BJgVWSOg7IxSnWPKVhmQ+YuED2SmQ1RnVZK9Qz1K8ECD9aGae8EpQzoZ/z6dZxVbSPHkaiRy+8I8DLwO2J5f2JycfE74iitRXF08fOQFmhw+TKrgxmATre6CK6K8j3AGaF9y1eAqVQwidzqcRaVkynFaL4u3sjd2N6z/bK/MAtdiw9KNujEcf0XrF/94kHb6vOSa4jOur/UR+b3towdFSspia/V69JlJNWr5BTELwPCtvNRHkNs1H/rq2y7BOL9kZTv+72mB4mwA9GB6ED8zn53IB6k3x2IB+l3B+JB+t2BeJB+dyAepN8diK+ttX0Ybd2u8PLe3zP0+v5xiSx3RtaB+ONAROcs58Ds4K3nS0Ac9bjapyIlmpzdsbrN11K0KBkTg9ZBt7CnILwNvkeaYgir7npuseZ0XFMSjo13VR2iI3GpoqxPjoeqW09VDbneUikC9zCr+4ET7qmjoopGoFhPknhPaZOyUHZnt6VM21GLG4+XYAc0PbG25I6RT6B2Q9K6oBh81d0gT336BhDbshb2c3TrceLdUxuI0ADCO7BGKENZYLn1FF4Gwt0FhC+A8FkRnguZVKQBwmeTF0m51HX6anhSyNMhJWrQNQuEvxkIdwEI/ylAjAUQvgGE6VWtFCEZVquVQemLCUMNZgKcQMBIYLHxCTDJo7WcQlHGhTwEm1gV4FloqcGdhSFBGcKDYWgg+aI+KaT6Ad4DWCWcc56o3ZAYG3zXn7dlzxZYaNScDlyBA8u6H+bM2NBxwOlynksWSVmH8ljy2NVqHMu2kBkmiOyY9EWgCTIIEJQoq5UiJkxNmnVF1hpEE5oIkL4MwrcIkmBdjqFn7cWEwDNXQXkOPj1SDvaCLeU5My3P8sMkRRga8CBRn5S3lGzX0hA7oa45qZtiwDaXUqrB5dZLTgdJFZEC/yjl1FYhgMUSqU1XsEjKOhKPRfav1TiM5sakswr0AlL6d8r6JW6lFCFADM4l3kS+IoL4nUhO2sH7xMnENTiTWg+Z5+CYwXM8izOJKUVAgCEB3QHiPVhrEXWFyEAMLgOBWZccSUhZA1cEEy/ZswVOL8hSrzkdBAS7tMCvQiIABwRXxBuzMI4bvBDNVOv+tRqH0dyYnMvRYsw47ZzWiAGDYZUi0mI9p4S9WSMCsyRKIGJerOcSiJiGlPkh7EyyBDSXQMhcKwIQ14DAWa+UBo6Irtg5msWaWCLizMJrRM3p4LqpFodzN8mXCGPEif9JySKp6piEJiD712ocefx0jUS0lC2XOtMV4WZXK0V4MTBK6i/COEhA8KVlVDkixEcICPUnAf1KeQ52aiLemy1lpqbItY7K0PDON4FANFj8sEaI7kYAECyEMqpnS5Ahs5wOGAYwEOF0Au9CGBtTtrVhIEbLIql4IUooitnGplTjKLVCJFrU4ISJEoupqVSKAOfgKLZdohmRfkcE0ZtAs1DlIJ8V/I7APlpmvfIdTrpYw5FlOWuiLUVOVMKQmJhzcVKGBhZr9UmZ2NkLIl0uLhFjHy72E+rG7wjaps4tgTwjuUXL6RjknOd2IngXPi3HIX0XWiI7WRZJxQuJh1n5LwCiVuPI4yf7+VeP6o0s76Yovaoy56oUoZyDknFw6bdzLPb6lbRb+8tgmxXR2tbmPmi7NsJ6W/uo7U+f9YoXoxXG4aX+ETJfeYvjs7v98Vscnx3RpVo1shtZK/1e0++ItgPRgehAdCA6EB2IDkQHogPRgXhPdzsQHYhPr/yhBUC+fggep4sdiM9+tdRhY0tfgxIvykw4VtIOEz8Bd6xuZCW2QusxrZDbiOavE1OEvLelFeJuePBLI7QPVDUHAK3M9Zbb+rJS8Qj2mI22qipkDH1dz1TzHTL3QJkJnBIqgIhh7ZiT7qaGxjNjZlsJhE+P0BZxmh76u4DwzYHNtcZ0PoZiy219CbU2hXGY8asux6Lt8nUjEC4BkfgbRmPjChCxfU5fAMJdAsLdCgRH6C/ebb0biOjcViClSMkNQAjjwYFhAYUKcViZ8fhp5i8Qz8JyD8BMABhZmwXcr6yfcZxV0YP5DpPyJ8B+OIPpMWtAJTciA6FaIXg4WCasmes6JWeVS0BMoNUIt2RWifeD9N5qh3D2WvxXsnz99b4kvZLsJuPySOLxZ+2ZtuUFgaTVAuYEBRs5n6eaF5m/MMIFwmZaJ+VvqF4HNf/GYI8hu5aExNTQBCHxJ8B+eBNrAhzvCm4EhU9DgW6AL4JHvDUxyXWNyVnFaR6v/p9GCM+VpFoQVWcjVtoh2QED7he39YUYJdDlUDcZLw+4G+sRYYpoW16tpI7MeNA8MZvEnEmtQjUvMn8BlVjugepfqMYGXkt+ecJlGZ1yJBA8eBXKnwCT4S0zRWRasNwIL3MeHUHcBwABLoXjbepuEldfD8orIiQNEeoxjvYTlDPAArGOKtkTJgFxQ1/gNOOMm4z4wlgPmKhl0Rau+eFM3ZzdLLhMMvDTKJoXfIUIfyGuroi8WFsZpreg82MdPHQqlD8RXQYiu6KU3IgMRGSuCU9Nk6pjSF3wbLk4+2uEMtXNUfoZRVEJW7KjSnlFDNNtfclmHxaIpB4yWqZISJOuaHPkqUn1IeiSZYcVuoSmzF8AF88Z7gGYCQSEWSJIQ6NggYSkXxG5QuVPgP1gpyZ6ldwINZTxwrHAiQKNC3RLVC5CIu5sAIEIvXUzwdTkzNQUzECK/0rM4FztC5xm9HiZmpyqh8BLRZki2pZXaZZBpahmcGSIcRHhsEKqFjHzF1jIhEM6Zn0O+R1hgaCLj2rJriVBfUjmwP4mR1mOmeuSVUBksbbcCCyWrFcxZ1Es4noIZ8LB3QQaHbcBUS7WQfs3l9oh6gkDX8bb+gKnGT3eMb9EORvqAaNMEW3rmAS4mt+pty7zuPrc/mV92YnkY6/3KHe855d1/FCLnxP3zUD8iVsc71Hu+BgQn+N58snOKX8eiPdG+R9/dSA6EHdG2YF4kCg7EA8SZQfiQaLsQDxIlB2IB4nyP/7qQPxPBFnGdinSj/XiIcbg9iD8jdsuHe0/ENt/GoghJdYjf46yXfkT+uLtU/W52la81re4JlPHDb4qAwZot/yPX3sZrl8/6e+zlHrmd88/UfLHK2/ctWqmck+vV4DYLc1Ia8uHfVFo13i3/T5FeLn91IbfACI/ZFsOkObi4uY5rwWhKO7NuOe6sxZHbVASi9qW2PboHuHx6xeF/cR7n6TUE7/7sQcQewzF7qkxNlRyt6MtT8W+XQnES8ZDB2ln/tmtBn/n6u07E+Gl9tdAcPqqAUTjMcRLQFRj6mp3ktjYexWIF5yjFP4VIPZuv9/VZ6YdCHyqT+ybgdg3gbBN7i8DUbe/BoKzJi3+BCUJVdeCpwtJimSXE2wVtxOnSRQwGKCsAYUNKoenj5mgdQInYmCJc60jcxsOkgj69WsJ/+X5db9fJp2XX79+/KQOPb8+v1K38G8C4sW97He07fmfZ/dt+Uslfiyff75+e+XjlkHgLe7nsnU5ispyiV+/vr3iyCc6ctn27ZXaen79JiWW8i/0iSa/pX6ulT67l1wLlUCZbynCS+1/4788bb06fcDdVdoS4E/ow9jQ6nDMoQoufQ7q2kPPvmczASdpQrAX8Gy/c6L1gattBCfCJzmIkkeh3IrlGlg6sN/xqb7XK2L5zJPPsnEHIJ73BNnTMrS0bf/9yX1fjqESNBf83+77DtcO7aca/48nsR3XRH+Xmnf5SJpBvuMU3+34E1rcL/XsqI7vNAlSHd+XrbkWRPadD9EIL7avUyn1JHMw2vwJBwaHDNfA3AX9TJoXmHIG0b048pbh7JyqbuDKkqkpA8HMCwvE4KybCjLDpK8hHaWI0xWRgFgWaQDxRCAtJb+5Hz+ff8qQ7B3OwZ/oNs/Ry34MBNVEn7gETXrmyOefO5lx9nx8LscDyyV+cK30Oe+lCU23IcJr7eevHAyE+py0+BPCX0jnbcieIOyMoiVEXkU0PJwoZKyAGPP6k4CAXsxs3VQkpc9rxF6AyGtEBmK/y0Ds6TNKYzhptn+lIcln5I6Hls9IXQmWEu6VgMCR7uW7nN80VHseUpSj/74LEMv/HIDYm1r2KyCute/0WxSmJiTWW/wJMYAZ9Vwm/kRIn8FZcKJRYQxnxjQ1yXSFuWkMSJIr84L2hkByEK7iUSi3gpbgFwCx262B4KlEgXihdX1P22jrnrtKh+2//98OczSurt3OTg1cYsdA8JHu5dsy9eikwSB8RzmK49srIUHlMDXR1lzLCyDa7XYKxKX28Wm5Gl54ajomn5MWf0IW69OgS+rJQ0lD/EzSYn2Cqocs1icnChnEXqAavSzWuDSUeUF7lz0sqVPyKJRbQV9KXxwvoq/6OwKLNS+ay/Yf/C4BseOl+h+ai5+5xNPy+YVBoZI73vLj9YX3PvEy+8SL9c7hyAVLx63tsIju+BOVozj2/EuAyj1zHTT4uRaOdNn29IpF+XL7zy/0Cb8veNG+9Kvy0u/gf8/u7leLyRAvKB1+5Jf13nxP3F+Jyx59qey1eqpaf+bj8nv7dbao83f+vG8xGa6Qhzc/XSopXwn/MBB3Htdv+v11N/06EB2IDkQHogPRgehAdCA6EB2IDkQH4t4Q/w520N/DYbor0g7Eb4u0ZBvs9nwzar/cJXyhe8GclOfXXmgG/EQ0PZg64a7qJA8i8j2jeOSnzjgfwc7Ry+dJn0D793yEGvLy+d9lH+kbUzaDjjmeae8Ukkz66jXcpDHgTJTpuLteFI1nxknkz7FRJpZxFVohif8Stb5/z1NoxV5FWia5d3sGhlJ6ewKFk/JOWRR6ILUhj07S44jyybEOtyaNKB9EW/VxcX16k7tImYioYhFIQlHCdioIC/pKwukrHY9wFYhQaS7X5iCNQabA4yUgSmEIa1OC95K9mbQ+z2oMa9eWK0B8o217TnAAiBfsfxK+hwKhgzqFQxOIyLL6kipy6h0hQHC481gDsWy5BERs71kBIVEqENeOWAOhUb4bCFcCEeM6AxDKSIltoCwEMBeY9rEXjtALZY0ziwL2GQqEEyBksgrm8W15btsCcVgBcTrVQNAz2toBfag2K2DU2hfZvwS8DzBQcpTQxKDcOfu+0BHumB7WzRokaA01WiBKXstaRwM6G/LEuXmvj7Krsgf5EbgJR+X/jibS85y4B0ji4QpZ8lvKEXqRJKKwKPRR9+PENBh+AP4NVDJRf6E1gZ/EPmMqonKU7VuCOzEzhMQaOGQyJKD5lCUmpiM/bL9s4XQh029aah6F9kX2LwFDBDoXOUooAIgpgjxeHlxpKKD8FOWlOOkBDWXJa1nraMAzxkxH8v44A4hR9EFEVokpMOpSE9iXQCN9C+7Hz2Xgf2qSXKeq/f7pJV8RmUURkj4HluMxKBC0wPFZ4PSKOLJixsHpFUFZ7INetkH8TFx5RfAWvSLOtfBCrX2R/UvQHnQuTJQGCPBLPD+kz1eG0SCRCSmgTerB8YweWl7LWkcDnjH5Kkjvs9xEUH2r4IT/EobUpxzpAsSekvGJraBT0w8hlGCNyCwKDJgzrI98RXB/JmKBHPScAyskqXJ48idJQPCZ7bYX67hS86i1L4x/CZZYZqCYKBMQtIf4JbxY09U2R6tBkoDg/6Ys+2B5LWsdjZAIFjolWyDcZIGIYktCzJcxAaGREhA6Ne3sFUEpdDAm6FtTZlGo1IN8a+LATycvn7hx2noyQMTiW1PM35quAjHWChi19kX2L0FJ6FyYKPmLnTeeMZDtKacmET3J/JOoQJS8lrWORpDrPCtqJCBCDcQE/5kRvjJwccmRLkAooZBYCACCGdX7Zcp6cfjKusssion1J0TFbJJfDBGESpbpiSzDIwPD8j0h/46AEgabIs0ZiOU3BGkynWlutb8jVK4qK2DU2hfZvwTuJdC5yFFCE+M4H0X6i+frYrEW9xYs7MGJcAnFTVGWvJa1jgbcVY5zVtTA+yi/qVBKgYD/DLusiItLjnS6/mXuXb+sP0W74vZX3Iz4Pcf/vnj/c7c44mbEjx1vv9f0N0bagehAdCA6EB2IDkQHogPRgehAdCA6EH9TpI8a6F8zgP6TSl7usH9Xi58LhL8Ymb9zH25q8zvNjNXeIPGW+PKtvulKSVQXgilJm3C3suhwSq43uBhT1WK0WebojPNJckpBtvA4XevdsGovSAw0MiauqXEUC3m7UEXojJ5J2BijmBDSx9VDsBoapTBDWL3LsYYN/FdDyJwMb2oCELEQjrBA+E3oUxyTs2mrrOlvDDqQXorRVlBSASqrjqInnHcJK/ZFfVQohq7l5h02RqcBRDrCb4RavtsCYuM14czx5lBqYVwDkVgOm5dyLj/VV8QKCMnzxfABIGr2xRYQcT1EcfXGXk6SaMGlwhLmJ8ivQ0ODslfKWkAShaYvllbndIh4rIjQ+NGoc2SWhDIezrP6osSJ9DjQ5sR1MhMDeePEzbB8DHV10doF0BSR4/zeIfEy1PelcEopgFCNkNy7hmdK0hnBVUwJHcu+aB0lguvct+FEESn7Q7VIZKwLRgpSfJRXnrBGUMIPOa2QPFWy0wj5CgXOpDkk88FviCK9XyhrZJaEMh7egvqiRLYbUCA8t7AcdYoFN8PyMbwofmjtGHqNSIEYEy9DXS4KpxTOAyoQqhFifFRWnim6dQwAgtYYy75oHaXqCrx3DJIgVqaJ2VcwUjg7e6RFhgflQPyFoEpKQ14jgnqQeLHfIMZCUH5DHNJxqqxhWRLMBARrI8AXBXvQ5ltIJBcmnGVuhmWNwNUlyKKoU1iOyIEDks549X0ZTPzlFRETEHlv7ZmiW6MAAR+VHFfrKDu9iqlNPJ4T1yvvKxgpYB9MTsyWwGBIklZzBgLzbgZC3UIAhJujs0BULIkZM/EbEvyUVj95p+YdBggEmrgZljXiRfFDag8yCC4D4SYLRPJ9mW38W0BE1/ZMyUAoLaBkX7SO0tgyEGFKpLvRLB8lIyWm6QL+PTrF6MSDbwrRTk3M3FAjo+CKqSmsp6aooAUBghiAbNKErhALJHMrLDfD8jG8KH5kxgVdxDki+kjsjsQmimZqyk4p8q1JawnV3tozxfZEpsKCfdE6CoM96dRkOCSqRTJlIBIjRXgS7Cwy2cUaXiCeF+vsNEJzpMdSeAIjgr9fn3Sx9tViLSyJpZYgCxMvn3w06XfgEqWaMBTikyLcDMvHgKuL0MOklclEhPOBPg1S13FWXnYZ//F85G9tqhFifFRWnim6FYOFbZZ90TrKDjZFDA5JDFmLZJKp1DJSqi9o69fn/GZ9Ty2x+cs6fko8H6vlfbc4/j1vu6q8R9fkt73if/Be0xe7qjxeh/834+pAdCB6XB2IDkSPq3f4wePqQHQgelwdiD58ZSuf1dpn1/eB12cxIe7pin/3/q8H4s+xRCZXsh6KG1Q1r6P+nF5hnVrXPfbDkNq024a6/OQ26tFW6tb+5QdzCxPkVQWD3R4SAHV9U4OXEmydx6aHNj9ZPvG93zNpjsT0hFzcGIvGKHl5p0DkxH2ovDhCuB0IXzUeXFtTo0rZh0uhrgcOoga+Em4IzopKBBPPVSBqlkjaYlkhq14iJxHzA78KRFgB4VudbAOxPSqbkIY1x8F9DRDcSm5tGwh3GxBVfasiLSAa4UFxQIBAzi76JhDFG7A4DrMyIZCCOUiCg7gc+BtwCze5qQThZyhDAXUtW06Ge3FSLkeqb9Ykj3fWjaXmTqC8ljhImslu11ZyayQ1lBUwDhTZSTga6vGiiaLMXeGtU8iMEcvQKFki6F3BCmEgal0Pz2wUC0SYSi4M1XOQcTnOUPmQVOkYshRPEOYFpfw8syvor1jQxChuKvQ5hKxPAY7gctxouBdR04xan0ssDW8yx2vuBMonzwpJVx7nvF1bya3pFTFKWhKP2hNHA23F6EQrwWfuChMWJsMYsQyNkiXCozJaVgg0N2pdD89sFFElYSB8xYWhekYZFxWXcNE8W8/nyhnCBuAd+BS40ALi4PJT98SiUH0KPmf4AXfDvaBHvoWxgfpI6wIsDY8H5Le4Exh4KUH7sJDn7dpKbk2vCOFkJCDg7cLpXVYHUcGGY6rRMkYsQ6NkieDxfcsKOQixs9T1gDuGqJIIEM5ZLoyFDlTOBhA85EnsZw2Emy0Qqn/h0uIVDfdimJjBMfNZqh4swtLw1q9lzZ0IKiikQFA9dru2kltLa4QoEYAiEMTbBdGELFfC3BULRF1fzRKJlpGCNuCsV+l6+Kw7sgIipnpGA4TLU5N6nnimREl3xxoI77IUlgCR9Ck4VFKaGK1ihyJ/mFEf9DVCgN9KBq/JnchTk4uWacHnm7aSWzNA6NTksm0URzMGM1Qx6tRETIp1fTVLJMeUeB9xrethvjVF/dYkU1PRh4NIYqgAEZYamtNm+WZ88sq8ODkxJ+P1gP1VQK4EEAN4Hi6zEFhp4lRoYQiXIwoVEQu2n+G3kr4srLgTaF9LBKeLtW4fJm3FMir0mpZl/aQMjiSWdfKiyKHcFSzWNKCt+kqWCB9xsqyQzP7IjJX8OwKqJJ7ZhTUXZhkp7i/GPdE0b315w6PefuXfpvlb9g2Hmdd1ToO/6Zd1yZmI4Zaov+YWx/rLfHQfehGytwORf3d+sNl3Dlz5u/cyg+J332v6/BG5AMTvaOU/eNOvA/H7ou9A/Lb6OhAPUl8H4kHq60A8SH0diAeprwPxIPV1IB6kvr9+gP/mmH5TNI8NxPv5IPe9Lulr3B6NK25Tlq8V0yCaf4eCFBDvCj0222mVgZcJEpNpa7xU85YqRuM1aZ6NmBXtig2LpHG7UqKZ6n1exQKqaNpOLkPBX4mNyCsgggGC76cXQPjGyPrmOIdaC6EBhE3GiyFI3jqh7pYCSEsVY/tszA+ob8laGPJCKJUcimiKfemZ1lU07TPdN8cu3AaEUyCMn8q6MvcpQGSKSijOl5YCiHebjJFNIMZPBsKeiGU0twCRfV9c8vjglAerWRSuIbyPctmGnXEQXsZBEiH07jwn9Q6T8sn1HIX/oawJ8CwOc82KABCoCZyILQUQGpZaFWNb/0MhPQgQVxQ4TtoT5ZRoNPW+IZ8YVTRZPaTdRp6gWBMkqscHJwEn+VRyDjiFadgZI/MPXAg5aRredFvlyJDcR0RLQ1kTnDqMYyhZEXBjcVLTGCyTolYAoeBrVYxt/Y80K5/0ithW4FgyZyO4JqqhkaNBcjXv80K2WUeT1UPaTBUcg7IBuhaZn/AWlG9R6lfQwdaphBOS5yCyD8S8eEvb4F2iihml+0hIyXreE9esCPUy0dr9pgIIzsHMubis/5GAGNPUdEmBgxUyhrNSHnI09T5fXBFWoyOrh7TbMFcE1zGPIfMT3oLyN0r9CgCRnUq8VC2KNE6BEJ4ELrzZ1hNES6MGomZFqIVG4o1MWwogruJcXNH/WC/WlxQ4QpQSQkRI0dT7fLFGWAZIVg9pt2HWiJnRZoaFTE3EZ4hhrV+BqUn5DWNWxjg5UeR4022h9CgJtr4xJNaEAlGxIpLflTIuyJekrQCC7ym1Ksam/ofzScdjxalYs0jGKNMoVqwcTb3Pm28+ZTQ6Rltt5AWcpyr4eyg/gScLPpMr/YrZsjMOzD9gLY0A/Y3gpoBtXhZMXay1Hq9aGsKaYB7ISfcpK4InST6HqCahtG0ogOCbe6mKcUH/Q2ye4LxyVYHjBK6J6HqYaOp9x8RhqaNR9ZCtNvL3LmiC3PzzrKmMscGMuLXOrddHYqq/Gn48qu3jSr5JO5qbdTY+0umv0pb4SEz/9Wj6Tb8ORAfigTvdgehAdCA6EA/Y6Q5EB6ID0YF4wE7/DwPx3w/1QaP7Cv2JDsT1V9v7w1k3klj4LrfVOIaiOlXGMF0tkv2xbjnYFqt4qk+x0fawimO9Z7vHWyoia4WSeKmu1c58t7g40edNIBreH4XuRAlECL4RRZk69+4SEKEJRG5xy9Eluuv2G1nnIhhvlSLKQtmhBqLuU6iAqCgAtvhUSlyEqHL05ViEDTmTtveHGRbO5MXWAXcBYXgO7wHCvQOIlkPKBhApumtAXBi9qeK1ZAn86pBSroe9Sza9PwzD4kApwon2YUuQrHR2QCm4CxO6kPkUQ+GRkpkR0KU4JP0Pw+mI9J7+6uPB2X3lmg8KVEUkOgECiSPmjqRHai3XxDq4aJ9Uu8MqlHCqyImWx8zOKCdVDAGdQFvgus9k/zRM6LMyV+DNotGyuskbHEia3h8F2+EUhcMRArJ2yzQRrQPKcTbcBQEi8ylc4ZGSmRFIQ45J/8O0GPl9FC2EaN1Xrvmg5Efn89QkydSoqV6dcrU26+CifRLKQKFQIhlpfdw9QvRhDDl5ytO2sEdYG2VCn7lPGFv2ZhE+B405vEu2vD8M24GytClI8DKWoCfrgFJwFwQIy+EomSDKjMjKAhV/BBAUQGTTj8ssjMFlnYsCCBbtYcmSs3q/5NpydLlPbHlWKZQoEKKsEX1K8kLpA7UMZy3hJ3VwQm84kczeLBLtOap3yZb3h2E78GItV4TM0gAiO6BY7kLWd9EZPRYeKZkZEeVsG2v+SHVF0MVdAsGl2gyJOetcrIEYrfeW4ZoYFkbu07xWKEE/wQ2JYsghe2WNCFBA4BJ0hhrmFVUwo9fBKI84eJe0vT9YVMSyHYQ6ELL9TbQOKJa74J16kKSOjtYjJTMjmNsRDrPqdZgWl6ppjQhB2R3ZfeWaD0qepEKop6YDKDdjzTWxDi7aJ9WbyQolPgOB+idMTcp/WYDA7DLKiRWjbT+qn1LIjjTEoIF3Sdv7A06Ehs1xRhBBWBFQ6sgOKJa7AIcWVcZgNY2T5TlkZgRzO8ijyKkrS+JXUAkuR6oWYJmo+8o1HxQW58KyPcNbJfE8ljgQZc01sSoi2iehixqFEhbBOh+FG8JqG8LpELJp9FiIT0o+ZULapEoiQWS8gsuONFF/aJRMA/X+iM3vue2t6+0tFoXR6Vjtu9baZS2Ne15xI4b1L+tWTLffcWgp/t0UWPm19p7Sl7Z/FhCfxxjZivQ2IG5/PbB/Sr/X1Lv610fXgehA9Og6EB2IHl3vageiR9e7+hdF9zcE+IEY3y+G8htkVOpHsj+nqa9Vy7hHB6PVt98NxG1RVl4ipQqHMiJMfnhuVVIwJ1buKLK3yPXmslHrDGtNizYjY0uVo+HSItuhobHlzDIZb4hY9nLT+qPmrFwclWmbpbIyrNAnJksgvPFZabutNJgTddW6N7Q9RaJLD45W2hW+LaLQ8HG5bA6iGhpbziyTJLSMiEXwV4DwFdukflfGs0WOKEcr2i2lCkcJRHWc2XIbEH4LCNcGYouRsVbluApE0bfamaUBhFtdEdWRvsEsqcfnAhBJjWO2KhfWS6TkXigjQv5ruK1Y5oS4oSTGRMm4oDQLmBrGPeXkpE56TxnhgRkVFSOjcmupVTm2XVqQZDJANJ1ZoEpC/T/WrBVpeX2kcla21EOGFA+AsCwVJ4ylEERHI9kXGC+RknvhoT8hbhAttxXLnAB/IzMmSsYFP8DOqhySRGFVECd1skJIBC/kraq3dmupVTkuubTgOD3J2s4snK8GxSIphUhqeFSfjPpIn5glbfWQKKOXtEpMTl7ChrpEcPmZfuslUnIv5Iw8Syq14bZimRPK38B8VzMucHkGYUSoJ4syRZDjBS/kraq3dmspdTAuu7QMbCVQ9q12ZmEGx1mmpmBZK6hlaBzpk2dKWz0kOoze4DIQ0QIxirpEBqL0ErEqHIm1xjoebbcVy5wILmswrRkXGYiYzuXE7HAKRGRGRVlv7dZSOaFccGlZhmqOru5b7czyBlKCs2oiGYjl87w+Mrm5bKiHqK6JmzeuiMMMgZOscjEZLxE1zZCDRwFidJtuK5Y5Af5GnppKxgW0NUQexakni+E6ydREjIqKkVG5taxVObZdWlSpw7ltZxZWJWEvpqQm4uqpqT5SOStb6iE6emZqSiwVXtoiXEtOWeXCeokMlnsRSA2DGRFiZtZyW7HMCfA38mJdMi5osebP6p7CniwAQhRCTig3hSYjI7m11Kocl1xacBzzJzadWXjqO1H/lS+SWCtpsa6P9M7ql6zVQ4YZo5eExQxLxX5X3XjFm3473ua20jrufa/areVajG1FDP/Hb3HEzQ8Xmrz8usVt5fNetVvL33qvKb6nyUd+/Sdv+n1WJzsQHYgORAeiA9GB6EB0IDoQHYgOxP8uEL93aB6xa4joj8flPxD87z3yvwsEUg1Te2c0f9faFzmprveFllvAM6Vd+Gm4fFRwKa17nLTDlhni6f5lM4KSC3LhmA1dhdtaQfpI6QK67Tg3x8sk3HNrw4aTzD0bp+3rIRphg1rrouBFpGFDHig/n+dVx0KLJCCsUkWQPMQ6Wl+oalw4Zktp5EIrNixKh9ZANJ/pA20o1KPkN4bXu9WwvQ8IdxkI5UXIVrIl4AedQ3FUG4iCBREu+5OEXG7jmE2lke1WaiC0R/cDobIp/hIQSM4s/zOKGqqcwamL5Pph9/CYhYO4l1gNDn3k1bIbMsMnUm1LoOYoJFpOJRClUoU8ye+21Dc07XThmKbSyOVWjkg7Mb+E+jKIY0oGwjqmZKUOn1JHMVhHGvFj4XLJuWbKeHFW2Bv1iaycIY93Tyhn9wCIUdxLrH8IdCYqXkQC4jDTTOxiPsqn9GhxRRilCtX98JvqG3L0pWPaSiMXWwlQ4ODrlyDQHrHzy4zcpJN+ICZEUj7+nh1psKpwBGFS5szktD5oXJSKGsq8IHZEBsLuoS1IjItkRNLggM6EL9gNGQgKYoFJhCaOZ59YGNUVYZQqQAPwiSPR5oKU6harY5pKI5eOwHgQEIMT9pP0yKWJVsdikJhQu/RI89cpby5XBDNh3oQ5U1wRklkzihqqnMHDNWWuUN7TAkL8Q2Y90w0vIk9NDEQwR3mjcJGBKJUq8hBFt8UFqdUtqmOaSiOXjsB4EBBuzou1fRoc/i88FrMZL7YeiYhyXAPBDjZwPirXJChs+EJRQ5UzuKoaCN5D4J1O6l5iNTgwEXjDj8iMVZ8oLfkonZpkGpuQ0C+UKuwQJb+UkgvCAguXjlkpjVw9gscD1DeKyBdsFh6RiLHQqUm1RBYgZGoCDSKmqQnjSCokcK4JlhDKywov1llRQ5UzePGRk3UQFRfZI4Ec05KkGhxYGmnWs8oW3MJ0VObFOR8lmhUnKTGBNVIoVfAyeoSLC9XU4oK4St2iPmalNHL1CB4POKeATDa4CoiAsUiCXPq1Iepi7YJ1pNHxJt4GnGtUsSRV6Ivv2Mp3iBe+tm7vyy/7O9S3qcOuVaJkjdTsC78RkX1dPgZKI/ccgYjabJa4EcPmF9xaYtF+gb214ut7toC47/X1txLuVcnYvsURL4xKu5XofvOr32t6qOB/75EdiA5EB6ID0YHoQHQgOhAdiA5EB+J3APH1nd5ugfZcbv/ysY8H1Z+soAPxeUDgSTm80v2RmPMI0fzdeIWau+BUGeJMd/ELTYygdf/6dURC3rUaSLyJYm+0e+ge/4Tt+i+cX44z0jv2pdUflzur/55bN+RK1seSvgpglcRGPQXnI+R47JNMxxmMFX7u70x3nemeK6ed+A7yv2e+e+04d0d32kMwZhUmReMvAlGYZoQ6Ze6d6lMQEN4KRwStm/JeZqindf3rvdHugSkAxkKBEg1ze381FDYaUMtHP8NGj1AzspehCUTB+TBA5JxkSCmvlIPRR4Nd4UWPUQdjonINuQmI6vSd1ldEBqLQxBAg/FUg/CYQmVGhJ07+d2QgxhDT0IYCQh2Ca0BEcY/YAMKFgqyj8VgXlwqI5X+jKOpHyWUXQAxyGJQnInusED8CHAO6kOB2ohwIfbB2yMwOekS14i7QNh48yrdFo8zBZalu5L0oCQRGg18xHKhE3ntImiGWUUEZL9LpyEAcGIhDAgIKHhQ/0sL5iih9UywTBbm1g0g/ZFeYQ5PzQaU1nsDbmHw2rYE4GCAONRBQpUjGFzMyqpzol7MQHipLjlb4DvnCAifCc3K05C5wcnBCvi1OWcsDZccgciIzp0VDzIwQw3DgqyntRRR8lhpGBZV8E10QsCTg/BJPNL9jC9wcKH7mYEww8qh9UywTBQ/dx1M03i2ISvknlvNBpTWewNso1+fiUmZCSXo30CdS6p+OnOOOJ89rWQSLg9cIPnfAZnCc2iZ+RUxAQAfgmNxKSIlicGW+uuQuYK9eEUdjpaFlwYrwQhQAsGuGg92r75YhnDKjgkq+BVy5skaw8wv9dckdhSgGRyUZ5CvCWd+UzEQBG2P5O8bC4eV4TrSHgvMRQ44npFPaTYdEkmBqHV0NY74i6IQ8yFp2cAYI+doDjsFSzYjz55wEGSZ1K4kOKhiaisfAWu4C9qY1orwigtaNNUIdVZbhXTEc7N5RZSGcZVTQfr0i1ou1uqMw12PKQhSyRszWNyUzUcyEYrxbRLFkBQS2aDwZCM88jwMoNZMAYaYmjmKSqWmyQKSJh9lNE83LaWpi/lO6yM2aT3MnpqGSu4ApSICIPq0RKMsrwiRAiKMKQVkyHIZiL5xcRlw7iVFxGQi5nhFrZN6iuSJK3xTLRJGpCd+agvZTr8ma80E91XhCGklIvcQTWKYb35qifKaEOFQpsgMJLZcDsfHSdwrIWIF3ABbFIDJbuPCYo1BxF47M6ZDfEUEVNLQsu7LI7wh1VFlqqBkOxV44uRyYdZEZFTQkkwECzi/2dwQUPDhWXqxpttbF2vqmWCaKEmFonVEtDlEskS8jBefjlE8MpmkG1uHgaWsw7+rfEdDuwJxz4Rdh++tqzaJYCwrSDxX7ObeQy+LytL+sW1+RY7E3Nvfc84rhFg7IfeOho7IZz60+MPcCcb1czV2wLcTi32tAuE0g3ndLYYu78XEg+r2mh3l1IDoQHYgOhOtAdCA6EL877g7Ezcd2IDoQHYjfF/eDjOO64vc09fhMoMtEmY/E/0l970D8ZiBIUBzKwkiGTMud0n/Px6mshgSn8WRl6Z5yTSFjSZfEJSNlbgIq26J93OXabivf2h7z28IkI8SksrF6oc8XetxUM8jskPv6zsHE9OxxXG4p4+lOG1qQLbAhaNtS2NiyMAMFEwuiTfXwWnWcu1jbeqj8evBW9WRKRGgCUbso5YeRMQqbPW7qe5RA3N53lx+1HZFLileAcGHV8StAFDSI9q3o9wLRft0LhL8MxFaPm1fEWAJxc98NEEjqhUmB0ATQQdLk/KzxbNUrOM92USGDn8aMTCKYlW1ByZms6XFmdggmR2VRbNWW2221npklXj6jFNV9SKks+LVkPoaqbNjeioA69/lCj5v6HoegiiT39d0pz4BYBcQ10AfPiVGUHy1XIEr1CqhMXFLIoGCWOmfMaW9Bz4qs6fEmKVjLotiqTZ7zFx5F3XpmlnhnlTBoIEdxg4ji15L5GKqyYXubDQZqVZCizaa+BzE/8P6+vlPykvNmKlhgrgjmahzPmp3lK6JQr9BB2lbIkLMiQPfi/9u7mhzHcR6qElKA4FUG6NoLWRk5RQ3QB6hFz32MWuUYXgZzys/kI0XKlvNTf109nz2Y6sSWKEovlmzz+dHYFqbpQc50Mn0oi2LNmjBEymevnOGZJVG+QflCfe1CKnaMj6EqG763zOAQGYsLPW7qeyByT8fv6zu6HGyxtjUic8aSrJw2XiNq9Yryaw1rChnsTAppHuQ3TQ9yJowKRNaBa1rLqf7rlTOyY5awHNHIeUvGvgDBnI0gDKVgQChbxHrL7fc6Cqs9XtH3UILAfX1fAGFXTcrKkJxHvHDV6hXFrVWFDPwq5EQO7vQMqmMhp2fFomjnXik+9jMgZlNTrqamw2ikOKWOeT6GqmzUvQUXJSxUQaoet/U9xJN7+46USqG+j5hObSY7YvkivQq9j6jVK3R1WVfIwDyJpU3ZFuSuanpwgrCT8keURdHOvaI+dqec6tZ9fhXst8UaOR4hchWVTOn4GKqy4XvLi2dCny/0uKnvQcf4+J19D2vbtTvrmqmxxo5YuVGaXfJ95pav7GupbHDf0rzPl3q81Pf4sL5/xSOOHD5/uwWIpp/p3j5fYq+8o+/bs6b3+r899NuA+DRnNiA2IN5r+YMc3IB4r+UPcnAD4r2WP8jBDYj3Wv4gBzcg3mv5gxzcgHiv5T9sWxuO93WmTj7jv33NINWtXGrz24C2AXGNlZBne1JVdhZHrvkOaWkttBkWbS6FsShq1YuGgaXllKqhr7/xm3uighGanvHDudxJnmnuF+XC4P45FQ9KaZDXBpeZMLvpP972+nP4RX8fpcwjf3r8NR2rg+GLp4ODBc/XgIh176UjqcpNkl0XYxOIJZfCsSgq1Qv7EIuPtWXVAbGhr7+R3VSelsaGZwzKmKXr3K8RIZwKiJHDSdWvw/WbvN8DCMLjn392+xAe+NCDFHngT0/7BRDhDUC0DawDEVaAiGtAzFQvFm0N3dLyAoi4BGJdHJpeQA+xcDAY9Bwlc7oCEblEzPN+Bw/E8wQFA/FwHYjCnGiwEqIFZopGBf5FwAP5QqgOktEfWNuCY8ij8hdMzaJmQVzjUhiLola90OMUfAHLIw8zfoXogPih7wYPC4CA8gfXQiYUYViIREyGfAuCQkkTbUiQiVT9qQSdVseKx0JWOA/L1OI0zM+PL/v900t4/uefp1809I8vjy80/PjXAVGYEw1WQuTzkjVzSsYQ/KtBS60DLYzEr8tjaoJkSe/ULGoWxDUuheSgWKheKMMjJOzD+/vesuqAeCCqM2LkuBxzOwZYyZiaegmxskqGrArkAQc980HWCC2T8SZ7SMHzWGCPJAOmc2Aa6P1uv5/mp72eEdP3HQ3/tHMHIB4BRGFONFgJytvozrmoTsARU6dAnS5gEpkDUatZ1CyIy1wKBWKueqHEgo71Qjh/z8yyaX0YEPVExct3gspClKwyWr+TaSrnOGjoH0D0kjCkEz4GlXBABFVMIHukGEJnxE6BKGdEAWJapAHEg5wRwVJZLFgJg3Ipcq0+pISDoHWwaHkgVBvJ1CxqFsQ1LoVnUXjVC+2w8KBIOuJUW1YdkMuLtQeCYsmlvkxNTKEpQFD+PCHMpCDXW0cWUGkDkWWN2AsQtkYYEPtdDYQyJxasBAaC1HlSkQBSjQqbmlydCojkgEgLCky6hUuxAIJpX4WRIZ8nH3NtGTog0IMqjCz3rVw1Tf+dTgZfdNMO/+CLAEqQtG2xmpqoBMvOOCaJ6n5gagr7ZwCx2y2B2O1/OCAcc2LBSuAr5RNfx5+Em3DSTCK6WGudTs4IWv5otYiF92BqFjXz4hqXQlgUC9ULZXhQ3ha2lOGdWYYOCNvjPC3Funwr9xHEuMhWS9oYJetihpIGFmsCouOUPoUhMlAJAiKWxZr8YisnUmei+4hnWpR/PrzofQQW62nx5v1P/ImBaG9gJawd+ZjNXzyuZSxZ7rmeN9UywrhzYPZtbjffqJgR7/Lkgx5x3Jt15L1A/M5HHJ/T1+1Z091AfGavNiA2IDYgNiA2IDYgNiA2IDYgbmt/A2IDYgPitwLxZxBIvJd/JOVlA2L+fCmGj9ymOBE9AeRPsu3d3zD7vF+z4ru102L0z26/LDn7NPu8bN9sPNbG9gg6PjxPAcdfD8/07/RoeXr2jE/Tc85fsu+F9k1PoANzK6QFpN+Ahv71YaXwZ5Vjhb/l9fJkd/z3PCSt082j/o47MgGxh2O7h6tAPMinnfv70ALi2fDQQdy5f3aLwXcWLwLxUOG22xMw0+N+bmjqyJ6gmv6+4BPtkH2B9hFwzK0IyqxQIOiNZ0tnomPVEJPIDghEIf24Ls4YimEoEBH5ZCogohvj/X537xmxm/+Mbwdi3wTCu7C/B4i/uDJX3+PfvyjytddP+yCfAu0TIMS7B8mDggE71CMUKlUOAyLdD4SBdwmI5ymct6NgxePPx/DX9Hdy+2X692W/f5z+pRN/v6fv9I1LTPueXhDeoM+oNZXa/fMPyk2BEC73NNWgOC3XfEEdYjX8hcDIT/r+i+2GZ2fxuW4fLfK0xLwHLQfbDwbEM/594DMEn8LurxfZF7CPYscPwZgVlAdFItQ8UQg/hPO4HEXbA+EgPSN8dhkCwnQ+Quhc9pVuVCDSIEcovi6aIsqHiSJ/z8yaJ3KWA3c/dvRz/LHjs/yZI3wc6vtB36dvXGIXEBLf7QI6Oe37QZ2fgoE7s0TTzQ/8xMnObidBwv1usk9t/qBJ8e8dfX9wFh9m7e906iz1UQ6z2QP/KAyIpxfwJgQI9pz2TYAUIHaOWaEJFbBGxJKiAdFa1fbAL5hj14PPLkNAmM6Hy0vEehtsd9QUJuCZMBOASvVBlEYQO+eZ8q9poXv8JZ3c88BN/z9Ne3e8PFIXHpm0RiXod7ijGtPfx6BRWFoUqYtm6fHXTmYcqknftM6ex5BKPP36GxA7i7tZ+ztHUJxamf7fSxmdquxMKP8KENNCvSs/FndGGLNC6Ws4IyBycTxTyLk7pxIFJz0NPSN8dhllqnBulhA044vobURbGZhagKg41EQQ46fMNfSt4yViinDvy6/t8VmBoL3cWfyWXhSIvc7+L7QA6gBMczB+axiO5x/yuyXYdOCoDv3/Q4CgWgBi7yzuZ+2jlQBPeJJRe/XU5IGYwOJPdELN9zG3ojArcshlLlcgMpHHLDeRcjeCcl1cdpnBdD44K4vwW5IMfBsIl6tF5S1GWiL4Fz9NLj90mH9gIPb6yy1TUyhTk0xVZQB0asLnHzTPT5VkUuGB/YE6xGr464WnvB9U62/Zaxafq/YxaU1nw3M1Ne3qM2K6enrmyz/mSfAKPe1h0HbKnQhylK6ajFkxCHXWnRGSWkcId0kGtPdnhMr7KBCoNUuH01dAKM+kFwWbPojkS+5Z1IKB2PFS/ZPWikdeHne0nE2fd1gif2IRppkdS+cDf3+QhXsqKYv1LsASXfkwd2GHRXbH36gOsRr2dFVPi/VU63lHNZ+dxeeqfRpgXBKEskyjHIAAH+LhmaY03Ec8veA+YprIpuVc7jHIU763wH2EMSsICKwRxNxANhTwSIi/Am0PXqxPWLCnNaJkl6FvXucj6MIrehvIIRMlCRt4JmznBNGVJJlnQmrEzPe78ObN36fuL5Tb32Hz4cY6+5tbueXOOs+zTH7q1uSQ6KXidwHi47dbgPhsBssnt7Y99PuGXdyA2IDYgNiA2IDYgNiA2IDYgNiA2ID4fwPiezhY02H+L0frvwhEnP3bPnrr/q8DQiJMvC0fSl141Sm3dydjLIDdgOBiXYv+dmW/emHe1ImYu6Yfec2tQT4NZZevPYiSiGkbZP6/lB6aagt5OVqLTXuUL/9I4iJcnhpAxGIp+T0Lz9KasEKqf5MWcrEOoa7tXwIRXSJm5UTE24AYlt7mVCuJ+JBoAcIMpGVCkXQTECW4NP9lzukJTSCE3RAajdd7LgMRLgERVoCw85K9MG9ilRE73nNGrACx6McFIMI6EH60WkCEG4DoipYEvep64L/GblDGAfEbQpWXhPagNOrWuhv4rjlOOlW7KOwGA0K1PzrOoAK3O81JMoh4g3gTg6l6dMqJCLpH9TDAozgUNgapg0SZIvjFXSlNWvpdgSNBi+Okfviphb6ragfrd8hx9NaPFrVmfA70Q2XwD8U7zdyCXC3nMejUJGG+zKoZOQVjNyjjgPgNIfu8JLQHpYMGD73uRkb4MAZT8ZDpSCaZo7y/b9ofKSBXCkWz9DV4fWE9BmVZeFUPtRpKnhXlUfQIeTIb4zUpx0iVjLLLyqK/euJb9Akv8adaqUeyPejxQZRHtLfmX+bW5DV50RUp75WTldRXmV6Qq+U1FSCEgSDx2BSM3aCMA+4u6XSUvCTynn4BAryGorvB3zXHiasRwG6IRabHVAn0BxFEEUO9MG98/hYNxUfkTjkj7YLlTNK+dOcCRIDqyCwZSJm0YmFkpMYc/1ryUrymzqamyR83WtwaPEys2tEFTedB5x55iQwwiHkjV4sDQhgI7owwdoMHIgfTz5DfugCRhNeguhvyXXKcWI1sfIkLQGT2SL0wb3z+FgNC+RO8P6iEBQ/EoKIU4OTldB2IMF4HIowGRHb+kZTWq3iYoBQyKhBhlPOVy/UCBJUzIFKZmhLN+ik5dkNv04pMTW6iQWkBAqaVxzCYcEmupiawGyDGojlITDTIZ0dRL8ybOE9sU6Ymy7OiSh8pKBujAEGqI2kORMrd4IDoNeGTXVygtKh29KTfUdYVbs2NVqLW3Bk5S8ZDmVR8phfkahEgjoWBQLyFzOwFYzco44DzcmWfl4T3cGlNXcZKXzIjRlHaEB6E1ODl6yR8CWoxhKL9QZaRK0X+G9QL84aVZMaaEyGXGietq0ofzKRgNsYgaXzQw1DahJUsE00KqqIRRTSuG3Sxxqqnx1kWC1Bxa260RmpNFTwsr0theJxy8JlekKtlWFcqbN8r5nzjpeLN26X68V131jrtvceLW9kb9/iXb+j5VSD0F3LbQL5/ez8QX8PG+BQg3t717wXE7/DzzzK9AfFNTG9AfBPTGxDfxPQGxDcxvQHxTUxvQHwT0xsQ38T0BsTm4h83fH9mZ+/VyXi7b/GOVj5hc1kkypYb+2adnYfVGyyINVu1lkVoZrhw5YfK8LD0Ru3kmW+Z2+oa3Z33LM9tL1oxnkl7y+5ftOhUQDxDo+ZmpMXvIAevYuAHIy5srQFh2VDSDIiaelBrWVwFYsVv+zYk36YHwkKx6QIQc+9arUQNES1GrraQbURzt/arXwMiXgSiZavBb0h1K7cAUR6/fzQQqsMRLLHN7UCEVSAuzSoNIKACggwkjqFRsqCYwoRkTBnoMfihpLI3xoYwJsJhbsvxG6BgcZBQzkFK0yfRtjgdKotNLYuT50mgDIGkBC7PQ9Hvmn0FQJjXh9HrcHT6Em4AJ0XbOkh+F4SC1DsJ8oR5KwDiMCqvhIJGymSpR2xIqsOhb3hL1NcYGrFkQXFRZGRMYaf7ZMkGUjDGBPKbzNkexm8Ap4JYEMRdSFI6FbYF0mSoxaWWhbJJEDNGq6xiMQRWs0BfLOqu38GXwI5X53XvuRfDbGoqbfX6Mnv2/eVAaNapyVoBEH1SfYHkmCz1iA1JcsGkskZApcgYGjGYTkRRmMicQ2SQZhOO+Uwpmt+kttU5/oW+r695JmLQN/TlSPa5V5ZaFsomyUwGQKuU5+XIx7pgQOQCBGlqEH8kybL4mjTtBu01346zNaK0lZA9JTEnxbwjy3FQIKwVmZoSeCVUBjljImeJ8SP2KlF58F0wRY01Q8OAmM55VZjIlENEZzT66WbJoKKZUkp+k8oW5fJRfsMSiL4Coht87pWGlkXyQGTdE+iMyMKqWJwRkh8GveDuqyd8thamyuyMqIL+yO8yWE1YNiBcKwUIHrWpTBa9Dc7B4kZMgchBVUD84Fk+kJIFJSWbmuAqZVopTI3kqCt9XNjy/IYCBMvjJJ5LlcWAVDDeYkPLos+LqYn3ZWVw1DwUmUAG9YRsgH2hxK9ah6MAkXxbKgGUBu9dmZpmrcQChJuaQjU1JWWE5DI1yRmBJcQYGmBdgF9QFCZOwocYkGklapIwYUwcmCtxHMF/82wP4zdAnQKMkMh5WPi6gdPbdJKxxSw2tCykbjcq5fFQaiG/S81DwXfIUrG/Ay46tA0FAmfbkZO8gdHi2zpofpfg+6v8j0Urg1DgnC6HMlnqEaOfpF4KQQXELgYvXXVV18PXmRFm69Lda93grTlNrm/5gu9++/i7/g+5z14yNFZLjrfbuh2Ij+Nb5BvLbc+aNt+2zv4pvm1AbEBsvm1AbEBsvm2d/ea+bUBsQGy+/ZEufo6H346+swHxfbq5JmkwE22wJzsV2+JTtuw+eQ/zrFy34m+QfO3HYe251rr2R744EhC8n7effcmUaqGFtZGqaRJvAcKzLVKzUmtvDJdqeKZEWgHC76/fBbVdsQJCY4HLFte1P7KzbLoZjt800EvC/niaAYES9wOxpi1xEQh9/H07EOEKEGEdCPHwFiC8y6Zn3wRiRfsjXxwJCoseS8L1NhBUIl4CAiyF46h5QQQIr8QxEudAcomcVPNCuRuqwaFsixC6Yu08tpgV3aht1noe4GUc7JhjSsz5HubhsahbULvkjXJGzvIaMhgjPB0NBsRxtNa0xXXtj5qngmAZ63hI7+n3juNTUCooX0PUOE4csKISQ1ELEcl3jBKzUvA2PKtPJEQYSRLFtCWIKzFAuSKLUoXIqgj/QnKLFLZFgJZFFg2MeRw5SKASrAev54GJFC++i5qBMCVU9cL4Ho4doswKBF0zppReNDGIM9InVfDQ2DdigEmUMazFde2PmqeCF+w5rJyF88Ld5NIZMTr1NyCgyzFXbR8cEZkoJz9fkwTpeeiIu1CmJtOWIK7Eq4tcQ3GjOytlQHOL5ELRgpYFsTAMCGNWUCs5KOvB63kEUfmANzVTYs73qNkhrG7B3k+/UHA/EjQxEDuOhRsihAAhMCCXibV4Qfuj4ql08nOLAfwNXaS5NxncjCS5WaC+YfHHTvooYxgAxPTZgIBgAoAwbYm+BOsBRC65p+rcIgYEtCyyaGA0uEasmNEXINwZwft1WDxTYs73mLFDxG5iH/V39yoKIHJGyEzvgQBLo3dArGp/VDwV6GsQEOBviIusO9INUTmLSX/m2QeCR5NtMSByKvlAUijTjs8zQpyIHuwHcDDwS6ZTS9MiyR4GAtoaSaSEXmcKH3y0R4vQojA9DzqW8E1kejxTYs73cOwQpb70QgTrBYj+VRRADmMMUMAYgrtqkh5bi5e0PxY8FZqaAETRWxO2hjK2iqCSckKCowUhr4pNTdNnqFdE5mF0wkvw2hLEiYDr4DZAcQOLoM8tIkBwnQgdCtbA8MwKPnrCYg0tCtPzAP2K2yPWxFgzJeZ8D6fOIbwIeD/9BfcjQROD/Myi+wEgpiV9AJ8ioUelxUvaHzVPBe0lXhfBI9GLFONmOH9P8AOxfFELOWmOFroUGhKOLzZ/Z5nDPVu+uD83S84ug6+3ke4XXcg3+Bw/5M76EhvlbpbH24G4HaC8cux6eyIy9+FAfMwjjg9V/9ieNf2nu7kB8U26uQHxTbq5AfFNurkB8U26uQHxTbq5AfFNurkB8U26uQHxjbv+/+TjB3n3XwEivvP4Z3pXKVT4vfrkU8zMCAVdq1ZlUcpc5nZUBrorrnaNNuzrtYQavDU5HGJXn5WvP/9Kcy+nMUqX/OxWR9DywXTeeGoA4cX7azOpBCpzC4NU/b7ScuzcYA3z32NqalksiAEVYwR1PPtiHf+cUuM84BBUkSfJVY/mQETnSUqthmLhn5i0xBwIS0MSrwJRn1iO2ZFKZ28AIi+BCBeBWD/JrgIx87F9CqZqwFy7dwCxMkyrP51Flpo5EAcJWiirQTUukAq+sCEq5QpWtwg1h4Kzk7h8K7DG0arBq1V0YDeovkfJspLzeaR4L/RATAuDQ1IBehqHEaW57mnGEBkq9kmpUbgUo6VcMpZJN0z/wS6HefCzMHaH188wFguHi8THFCToI+OA2rGoesTQSf4U845CVtzXk5ZGNLkHYmA0qA4G7T2OSdkQXrmCtf9rDgWFNfXlcH2pHflKJIBoahUUSBX+g0SAETzMrxx4DwmqBMyRSsq+QFgWr+OH8uJ8FQ8fwox9gRp9yWyiQc1uNJZJlKg5phIFAtyVNNPPMBYLegUfp6hcjzpZ2C1mL5QsF+SNeTckkAz0pXqIBIh7qlChOhi5MB1eOb+JMRsQpY4Vh4IYF1y/5FvBv1G4DKZWQTlNlP8AHRDwOhQIOg7o6mwuyD0S4VtWfoZjiAxhxr5ADflpsBZG56Z1ZZmAqVIDAe7KQnGksFjs54WjWgfMDLOnAWT03bx7VSBKDosZEFmFSoJpZojyhVPVmAOR0eDQJ59vBYwMQj1XahVgTID/wAoZg/A6BAhVyNEUH5qqI6lSiGRvmWdFwhrh2ReoIRQHqIeMBoSyTAIzVWaL9Sjndq04UlgsUYgESnjQOhIoHfsZEOi7edcAoueJhA2mXOlg8PQi+UjmyhU4PYv4j0xNh9HnWwEjg+bBwalVgP1Q8R8G4XUoEL3ogQQwM5CtC3Y15wnX7WuGSAxxxr5ADcms4qYmzzKJwiOJUOEYivALpF8q/QxjsVCvosm39JYXBl4Ve2VqojbNO1biKFMTq9mcmO/AU4QoVKgOBhYi5CPpKlUN4j5E0a4oHIpRk5bFstATP2IqkwanVgH2g/IfePHMwuugdk5H5nwcCzMDHKSj6GkkyHkxqVE4EIUhQkoh5qPV0Mwqtlh7lgkW68x5IuOoP27wLFKo9TOMxUK9OppqyKmzxRqXEGqPLz2OwpNx2h8ZSiNSOh3HpmBie7t+X5ibe96qsWFsoVvbjU0fL/fN5PHe4l1u3wqv2Lsoh3H7KL0ViLeyGrTe9fr5oo/5lmF9o3drvt0PxB2jtD30+ybebUBsQGxA/Dmd3IDYgNiA2IDYgNiA2IDYgPhve7clrfnC7VKmkw2I5gZp8emJ4dnvTSIokDnxeg5rXIq8ZrbmTQyzjmpAPV3yat6KcSfy4m+DxZG8FfNz1tfUBGJV82NRK10a2W7Rh7zYU1zHk8havSOw1DgCPaQmkUMrhO8NLweixZtYB8K8WUo6aCBfuRPpZiCi89Pk1au+Jm0zNfyb0yRiE4jkjnpGSqy8yrNnvgsKRhuI41mB4IBhE4jLD5NzuACEMhvS2o+tCUR4DxBhFYiwBKKh+dFwcgbE/OAciNACgjN+nA6awUScMxZDHA4j9DXojEA55WCg3qFIoHeSK8UkztGkV+cI4SxqHJ4bopZcfeNBjPOMJMqdqHO6qDJGrXIBa1FUMKDNIZliChBFHyNAqn3pn9f86CRLCrd1Uq0S+gTmyBl9CJqhphujvH3Nc3M6lNqW4yWorkyvGUyIBDCYZD+tEXg3f4phZqhcGAeDApvgIVAWhiyfwW/IqW/wJug7v2nPRx03RCxZfeNB5DTPSKLcCZ/TxXRBcqVy0Ze4N1gq1EKSXCboawhWCmdEFhbHUvNDfdHMMxQ8HconMEdek+ZVkYQlSYE4jsimorWH0qsgGhIko3/2ZwQC5VjKLMkNVC6Mg2FpbigvCUnhGL8BnAeb0o03wbwQ1sFw3JCksWWtb+F35DXxjAr1xud0MV2QLniVC2OCaHw9aqYYOyNKKQBB/i0zwrDmh/BBYrBkHyKRwQwP+v81aV4V0B1oxOyMeJUUHvT9teoNnfxDTprBxAMhs1kFBASBclGkALZ8RihzwzgYwYAw3gRUMkgHw3FDxJLV90DMM5J4IHKY64KE0atceCD6AkSopiYrJWeE5GFZan6IJ2MLCHBYDIgwKu+EgehbQLje6G7NYOKAkN96X3KrBCmnHAxwHXSNgLJFrCzIVZNT5xA6ST/jhoglq288CDc1GdPEcU8WuiDJq1xkNzUVZamSJieHohDSa5upsDgamh9ualINENEq6ZU58lqIdcJI6VkeQ2QeqLTWthwvIUjGj5ygYkHX1lisc1LeAy/QY5maTsbBANchi3AONDaOjtmx5E1guZLsKhU3BJasvvEgukVGEuVOVDldii6ILqpQuQAl5ygKIMifgkwx6CsUQrRUHCWbDPvT0PwQXzTzDGuDZP0E5sjAHI9jyVBDI3a0O5+MY6DoaK/KlX54x5aXyho32pvfueY76uc3eXqfnY+8s75pTN6nIJEXn2+1twbEhyparHj6Fv/es31Wnz5k2541bR39T/i3AbEBsfm3AbEBsfm3AbEBsfm3dfT7+Hdf85/p7K22r5e738tv8CPZgPjsft3uAgfn7ekIvxV2RvZlSiA/yPt2pCs8c/j6o62zT2lB4RT3/o2rfVwOxsK2WmqXy+77NUvH4RYgdESmx9rTeMTRYvVFdSQt7eXFpxy8FkdujVJyQKQiZWAv/2YOnw3l6XyogEg3kABiUDltBIRCcm+keYmFoR6Mlm2NPLXKpVUgrISjFGQ9FhdAmKCDOtCJVEkXjLIQa+bBYK21gCjl12gJllnENx4NCI67jb1Gu5ZAeMZCnKlrIALs6TX0qvccCO1KDNdsZwGuVc4DsVZibsnzLuozogbiwMlZ8vRvxR25AwhoLjh+iPIefJ4TcBc4/BKwR15LL0BQZOmAZicgCqeDwiAneWUW2hz8+Qjdi4DvZMv2EJRBgFCti5KZ5Kpt/dQqV+U0WSnRgVHCllhrQ3gXh9qmKHt4mDKNCXNDMH6suVHk2AEE2B2e/6GfDqxlQl6U4xJxTpa+QDUtcJpSEHKCIk9rBA8gUwIGDnZSRC8DiPLae0BYUPKG4BecoF8QJXbbSYT5VQKG9H4zgEhF64J5HCNncrhoG8HJdjmf02StRJQIcpSX54PwLvrKpip7eCCmIR4Q1u3txfaSoECB6FPN/9BPvZR3/BCoWnTMTDiefWYRACHTyHAIekYQ4UxzM0yLJX45AQwHr0xhDIky7OLi0e1hsskgQATTumCBlHDNNltaKedzmqyViMKjUDmJogRS1VBlDw9Er5QZ0GSEp+FypwxCG3J6JSWFR1EkUD+P5zK8xEzIwWcW8UBE5mgcytQ0lCA+gGBuA7MWijJFbwyJGohY7clJ9blUvqRocQwYjKu2Q7ucz2myViIKj8IEVmogUCOWdtzUpAQByYwS3Tq6BCIHRB/XgMjBcREkxY3LLCJAwBhlMjkJV8OumsJsako6fVA4Xk/YlPzUpBPKq/EzNBNLUK0Ldm3orttm8NrlfE6TtRJuamIGQFECqWyqx6rcZi0bqcamJqGkDZ6j4fkfSkmKNRPFuAjdWGcW0RwhxGvI5XqfV4pyH3E8030EaoOtoQtq4OxW/JnypYwDL9C8WJ+hkyF7aIE8Q80CWV2S8DiOTPC6bFuyljTLVTlNVkocx0EsHQfmbZyiclpcDVX2CHK5wcs880YKlwOaG6dOfRyYLcnXg57/YfwWKR/s+GKL6/eUzZu1eWlNnGaf58eWW17uSZL47wbba+W8/UslWl7llVGIq324vcSlfl8c2reUfi9nAfVvtb09awrbs6YNiA2IDYgNiA2IDYgNiA2IDYgNiA2IDYiv6NdvBrXdtbUOf/5AoIXPAHED4k8GIru/7WNrn9Y3054onZieGq8BgcgfPa+kp5hxGCQeUGpeaMdcWvUrN3JtZA2A0d9K7WOW5WTazlbC55gJ86wzDQca7WZvI3ubSyDSwnaJrkGT4wIEdR6RajgLzSDNgcj+3eyUm0Dkln/G97gkytJ4Gjog/md/C69kluUET6MLmyXNO+v2DMvexXnxIVXCJNnbvAWIUAER3gxE1wZiiBrXZSgQ622fEZ598RFAxAqIcA2IRWfdnqG7DYjZjFO+z3UsKD+JhC/yYXT6GqaGEYyVoBwQ/Q5OQiz8hmnwRQ+Dw/HDGflPXO1//jlQML4C4pB8TX7srZ5IWAavxkb5O2NIiDrGQSTdwUmh3rAOBr/LrRlYKAyGPC6qShIkTq3aHcGVoEGjo62sMxROXvRuqLPNAFCtATtJRmqmYxEkdst8Dc2SAl5F0Y2wzAd94YDod6huxBJkpRWA2QtgUhD/Q16z19r//NNT6ZNGuvhN/pPWBLMjgNlWcpWA6qaqIHHGkAhyTuHFdQOin/YnVucwIPB71xflTR/AlAo6V4KpRaGddYaAWPRuqLPNQF5Fa0BKgHpcUms4HYszgGC2BCl0aAKNZGoY0ZQsknJA9HuSElHs0XR0PGchvXShuFpq44yQ4Wb9j1fuImoqByLkrko6wmeEqIJEx5BANhQN20MbRIEgiBPrbhgQmKBUw8OIZqbdgTMCJVKZZJZZZ2hUFr0b6mwzOCO0hup3yLJR61gom4EmA465jgpELmoY0ZQsUuGALIDIyhocZZAoHH9qAUFrhObogYaGLNbctjA2MvKcGBAkQ8FnBFO9FAjKZ6IWmMPR6xqhQPDvfLZYq4aHaMic/RkRXQmdmppZZ4Zl78CNtWwzoO2VGqrfkURpo9Kx6OmDJo6xU6p3ahg2NRkHRL+nVIDgephgmAXFvIhXELlKbbAl3FVTgLYF70/K7NCpSbL3JAGix9TEPMTyoziMuRf+BDM+AARKnk5kj+Zu8Aj1r8oLzacmmrvL5JX8GbHMOjN5seidjEPJNlOA6G1qKkDMdCxOYF0gX8lBuB1Hk9ESlQllJSgHRL9HYTcIz2HAkssDkokXQeoY0dXuZvcRWX4UnSzWSayD/pgT2BeSOO0E5kTIFUOClDWy5oYR8aVAbBIhkZ0YcREBor9QzVAyJobatDvoPkJLGBDLrDOUG2beO27XZZsBo7jUKIv1uxQ4btni1Ttrz5b499zeT0eUra2Xov+eW+39e7bLWCmRlheS2bWQL2c6WfU7XPCiHgGz81u3j3rEQVSs27a8uNZfK3H9Ecc1IN7o058MxOf5tj3024DYgNiA2IDYgNiA2IDYgNiA2IDYgPiPAfHWxr/br4f8uXXAv/0v+SvqbUDMN9FHJrehClyelJSnNi3WBb1NF/hJZdZSzua8kZoN4TO0VG8D8duWVrZTD+fm1rRtOapx5P9N2OLfc56VmdfxGhndyjDlqi9d8yi9axinp6/8aDC1+psvA6HhkiQshRzqVBst1kUOULi/AITfMSMEmGfVmCYOt89zraQ2FWBuLaoGx6CROAARwxKIVGdIcUDEq0CkVBMafHqRyfmcJWNMLN75t//CahsMBB5fq0+LM6LBusAbxheBKAMUZz9Dl/5lqAeJ9iw4G1eAqOsrELEojLSBCFeAiOtAhLAKBF6GnmeMiQ0roWY+iJYEcxs6AyJrlhPKEEGSIEvWhbzqPdVTBQw6Q9Z4GMpk8FwMDvlMFitfBnTxgKBTEO7E0OJkLHKdBGhwIMLWDaowEoPPwkJBJFPgkBedT2Rds8eQNxg8jNOhjBgk1hPHuo/K+QimvpGRVUBE6JFbhUYyhq72gQQlauaDaEkItwFpXsoZoCoTocW6oPevwXgoChiDTF0NHgYxGZBNwnMxOMHGWPmS4UXPYkLITkJB1xYnY57rBFoXFPmm/2NRGIl1FpYeQWKfIYX29dM62Iu1QXKhYJzoCPLBIA54HJVZkqRcyfuCWLxkjBHlkYARMB8C7xN1CmU+iJbEIGuEnBHHIouArcW6sDNCFTCOIYR1HkYKCoRyMXRer3wZNMFNJ7lamDsxtDgZ81wniKzrGWEKI7HKwsLDGZLLkMKck6yfo/jJv2ptXzKwwOskky14HjI1oYXpF0XsDbwbznIwIZaIvvrA/ZfZrDAfdKavgTCWBaamFuvCrRGqgDGEsM7DUCAcF4Nne/zW1KKuEb0wlBw3YsHJmOc6USBssc4y68OffgWILLSJIL9mAwL+90aUkMRwqg5VUQtmawS4LznoGeEzwQCIivmgeUUwtxaFGlwl9c2rJmFd2FVTrwoYuSTFWfIwPBC9TU1UxvvCV008q+eeOHvgTrQ4GXGR6wRaFwqECVvo8cOofpbMJ25qOoyAP3L/9CqZjnFWlr4HEEWfIzlSa1HfEM2Semo6jLH40ItkEWtxeOaD0gjBbQAQneg0FUmo0GJd2H3EQbKnYA1p8zCYySCLoHIxWKcj1b5k8C0G2heg3XFizYwlJ2OY5zqB1oXeR3TBAyFLqSp1jLHKkEL7slwiHOWyAxc2LMgSQkkXh8WaOCvIpaJ3FpIT5jywaggW66N4x/qIPhPM6egyqiwuzmpFu/bW4jCs3b8ueRhzC3GV8YALzty8AFy7FoflXO6scyNpyKUtv+HIbVu+7+DHP+K4lYeRV/atqXvkq/60H3F8SyDuGdDPqfdZ2/bQbwNiA2IDYgNiA2ID4oPqbUBsQGxAbEBsQPzXhn3z5pMaaz9XiuE9W7z7yGd1Pb7p2OWx+bhx0sYQNRlkRx1U1r2rnAa3pYWFYXHMH8nlSKlZvLnQZl6zKXubu4dF7eWxNLe6GJt1GLhEHD4CiEb3akmDtWD6BSAao7cYgBUgkmsp3QVEXHHrUqUrQFzZ9AeDKFhcabG9P7nG8oocxwVtibWOxLW+DjcDId58FhDxPiDytUemZO+gnqXL0K8CQSEOZTh4lQnV54DsAbQlOG9IyTKipXOGssQZgZuRHngjdHKQurAIJoQpfKALbK1wJ+hf9Qb6F8LdELUMfc0YLArNwYKIm8tbckJv6kwx5gnlsFj6IjWDcjtwzMamLg8L1rPCVcJrxkE1ObR9SWnV3M/eUWBPGQ5eZQKRt15nW9aWiC40X9QuEr/onYKk99DQYJL4roYYgykbyKvx8luquBP0b/GGs7kIdyNBRV9fvAeLgrklmlIgacaXXpgeoWSK6UuOCgn55tDwJSThqiDQK23Z2CzKF3v4wdi+MgW59lVa3u9PPlOLvvqtJ4mqTBzPxFLQSGx31ngzNDliULUL0uyAsgQDUTKUpMLZ6CRvCwQROMBZEj5Nv/ez507Qv+pNd8bURB51CiPzRoKG/ZnzkYov2haYHuw1pW6zXgTjqSx9QR/l38x/p/I2NvPydc8UCB2fNGu/c2uEz2DTBWO5jcpw8CoTTI8qjWY7I1iJQ4HIrNkBZYnXZElnsjIsaBszpBUG7UwYS2eGmjtB/6o3JZsLq30YEDl4IEBPKPw6Uf4ov2Ge4Mt5LZ5EgDnzxSfaSIO2ZWNTl+cfXLHnzwiMT5q3PxoQVQabURbWxF0fyqqpKhPqjEin9FgjarKKElKaU1OAlgU6xjNzNtEVB0TvuROYmuBNZv+Fu5FSpf+hBC/yNQVQHpxCSB+RpUUyxRzGlLwnyaYa54sma2Kbg9a1sVn4bj0LBkaZmlLdPqfrme1PqitC7AfiMnggVGUCLAVkVAnMlgD/wLKMSOlRlSU4O0lZrFnhI+gSqddFVCIJuwN94bYKdwKLtWR0YV0L4W6IWgazO/QygcRTmNURVHlD85accMprppgs/HP1pPhV+QJ7okeCb6Mfm4Xv1rOAdQIpejA+cazbJ2rlfD9nsBmZUR71Em22LRka+eawd5Nke5PuR17x5mq9C0IP88+3eVJdvl7eVtglS1/yBzR2FxDhy4G4lr/lq4C4qf13Nfal2/bQbwNiA2Lz5v+96xsQmzdb1zcgNm+2rm9AbN58smNGVPlug3XJq8/y+DeMxAbEp9jNrZ3pNiCOrFasCvK0sbz/mXgMkZ4oZmM05KV1RKzGSs0ix6DvprXYG11ovGyWKKd22E3/8bZXQtkv+vsohR750+MvlHziY1qD6tjftY3qP7z4bytALMzs9lpvt398mY7vbgAi3QEEva+ZqiefHWTHswzZSJ/iRSCg91HYIQO/yyyvCseqCoJEpmLh1DMICOke4fHPP7tpMB744IMUeuBPT3sAscf+3cNVIHaVhV0ZwgeH5K4G4ln9MCC03sPzfjcdL+1aKx8MxCHoG9cHGioncbIKRKi4GgNZOJS3vVtANIAlIJ4xjjRwV4DYh/0+uHG8AoQf+vrbHUDIp6dfAKLRinEfjFUgTAbR0KjyrZyY1SBAxAAgTG+DqADgVLDQf4YsRLGSjCeh/8orxMLm4GEWzXpVrECYBS/aRtYJEYZJUdMgIKaBeX582e+fXsLzP//Q1PMQHl8eX2jA8G8B4jk876ep5a+Xx5/4G6a/079T7cfpX5THnseXp5dpUuO9k71pwGjffkf2pkkmPGmZl3/+efz5F9d7IJvTviexQr7swl8vOCPobKB2H1/qVkQTomYbCIdDNDQ0KMkhvkyhw3JGjASEKFNk+RVnZAnhl9SzBVX70gKs67/Q+4hSCi+9xwCKis/jwuFJViKQcCTrg/QlIQKdCfvdnn57ez0jpu88CUw7dwDicU+QPYSn54fwY7f/wX+nsj92u6nsfpo4aBKhej+m//Y8p5CdHzucU08v+x3+o700wCiz3/3zz9Q21yObNIfR3h3XJMDw7+TXfqr5A775VkQTwrMNwEsAP4EzpSTwOSAe4IHAGXEQ1gPSX0zLc6HbKBDFSok4C9NC6AF4d1/qTxaiRNPrPC5QrRB2RtEHgWIHDe9OgShnRAFiWqQBxAOBRCXL0T0DgcF8mqaOHZfHME0TSRCABYgdTS70nUDhX/rTLz5PAgPBUw+Vf/y142O7sJeVq8xOz3Sczq66laIJYWyDrPlUaIIYJeuH5FshFYLFGjEgIUcOwlEo5IJCNRErGjIPxhRxQPCszyIoHohc8rhoghDexnJGhCxrxF6AsDXCgNjvDIg9fXdAPD4rENMZETwQ+7AEgmv/4PoY4BBepoM0NT3Tt+mX//yDf+M7rr3fATADYvKUvHqpW/EKFIVtoFMTZByUXKIpW0C4igZE5tpBVwbiYMmElMsnnmS6ovZh/zogEoDg66FcgCjksZSgwoEAPemBHEawPnDV9AwgdrslEDwJKRDPvK4/yF4aCJ6auDYub3T64UljNwNi+vz4s0xNUoanJj46rQXPf73saa6TScvOCLlweLapyVpRgmDNNjgpP4E1KxKu7PmMYFYDqWaQyAjuIzDAuqhOU8lIdw9MJczlEy4KkuRWCfYvgDgqJTGThSPzQFSxAiwHYnGQaoWwMzivC53D5C3uI55pAfz58KL3EVisn7D4/nziTwYEzfa0lxZeWqxpGabPO1ncecBpkedFl4aRLGCxDjss0HRfgTK8WMNGoGsy8oP21kDQuUeNU7tYrK2Vy3cVTRVDd0a4i7Z8Swj9QzK4ZPcpzy6pW9/aF9+27Xf3etCqMZ0RO70EXrsxfHhLJ9tA0K+z/YjjGo/i1jL3QfIRQDy+3NtuqwadEdeA+OBte9b0lXY3IN7lwQbE1UHZgPgSn64PygbEl/h0fVA2IL7Ep+uDsgHxJT5dH5QNiC/x6fqgbEB8iU/XB+U/BMS2bdt/eXuoIrf2WGpffaofVz0Kh+Fhbmnxab9aJki0fwoCUJAXT6Ndi7v6gc2u8ck+766yMC7X/zAboTF+N9oAELtVIB4qIHayzwLzDzcB8VBqWlgdxx6eGYhnG/hdBcTO/bN7AxCX6+/cwY+w4Xt3p41rQIQrQNx6RqwBwaHH60Dsmx3QOvvlINbtrtV3g/ghNpZA3GyDOQUvT8IpmH7/e/AZ/pI9zHN4eRBmw9PLX8KI0NLER3jeGXsBnx5f7PPzjqw8lSNPpTU6FR6ed447BP4DmA+wgbLE0PgLQZ6f9P2X1EcZiol5Fga3/XxLfWNhfIyNqnd32aCp5wcHCIWBsN8zr2D/owQKf2iQfUcBxd0O8V8JIhInYfcsJelYgBVhJ9DnZxyTGuA3oDUCYrd/KGdYEP4DGA8cQEQIUyzuKK72g+r+rd7u4E/NwthJjev1jYXxMTaq3t1lg4eU2AaPvxBip7jto/6V0PnTrwcJqIMkwsSSX3xsT5/+3uEbcxqYx0BsBg6LT5//3ukx+hf8BrQGKhaF0wvjjvkPGuvacWnlSWg8+fEXtQhvHkuI3rMwdnwxcUt9O/oxNqre3WUDQXSgWoAIL/JXOQxhCQSO7YgcMg21fONS/Evl86gGQq3tXGsyb+4FkvBDfls0NZUBZsZDePlhgf3wt/AfiEHBvIqKhUGsitvqGwvj42xo7+6zoVPTjv4qEG5qCn5qonIFiF0BYvfz514He7LDa8e0LigR6+dPPqZkLfAbpDUwUAOfGxRaB/9hr1NTwLdp8mOLTP76QXX/LnupzeeKhQE/Hp9vqW8sjI+xUffuHhuyWD8Q/fCFiU8/mVfwE4stzWa2WE/HmamABWkn3DcscHCeau55Kd9rjcDEK+E0PCi/gVvbgdcACpCcxszDYCCYFfETPIlntviARe4nLg/2zNugNp8rFgbxix6ZKHm9vrEwPsaG7919NsLHb7dQ3JcXutfD7reG5B9WSt8T0v8uNr4MiHuc3H+IX/8NG9u2bdu2bdu2bdu2bdu2bdu2bdu2bW/ZPGXldvrKvOTXk23+c1Sb/w8gqteR5koTtQ5FqbJfK29MD9v4meMdDu1n1uglxbneRrW1UzSkWUn3Pbu/OmT0ejG/WJ+g24z9/IKyvCHI2doHUhyZvg+k/0z7B8roPYpuM72LOPx7Ro5sesMvZbUbx2sYeJpBJXCA79Ch2FXPSw2IhwWWD4s3xna7+mnrw8za7LdQURkQTmX5hqB6GynPxj3Vf2zgi3K8+057WkB0rJegQCR5nTgG0UEX/XF645nKUY7ugZQSKJe7vleboEdSdNQnICYQxK7KisdVIPaXgQgat3o7ECFcHPglEGEJxDPKPbSAyE0g8gyIfBmIg2rt8wt6lpBGhrgIwYdc3hnPMyCkHIDoGYjjWe2uvs/pGQiepxEC9v+qdChImeLXy6MwFMzCQ7FDDIwpzrZXIB7lRVaOi0s53v+TmR1BFSyoBLVNQSbUI4ZIZe2Z9AVMbwMZTQ6i0jFNGYNmTsHLymdkPuFhsLL6nfZAy0N1RpBZJioQrMxOLxOfOUtA4AQLDojhNiAOckZA85+AoLZISaQbhyQKJ+SXMRBqngZClWEK5nkdCvot/r3TV73ll1tCqWBqcOS5AEG8DrwwPg1feXk8MO0A9IMfEmjkKYdDlWKV9TKctWeSbzC9Df5NJ1b+l8GDlshxpCwrOb2y8gfLVGQtm8t32tPzHlJOSMkyy+C98ijTUQyvbGUCmN4BHzKvCGjtiLe9JyCOw+u0FtCb4cgwYEBklmyhiYreLCc7vYKXhmTJGDyLoaIHBGEl/F3pUAAIiBqYJBWxPKAoUd7cNyAmG4+iW0GKGPjsgUCrKDFZ2D+J4NVTwNv9+50/I0xvA6k4EuddOXLGFc11QllWjudXydBQzghO9aHfkZWFIDwG6Iwgs0wsU0gqQEhaEJIwkjPiyG1CziixWgKlMjlgEpOsLwIEdBGgrJCiZrkg8M6vCS2T7olnIMyAYIaEOyOUh/H3DioUQa+iXoTlIUAwlcYBAT4IBEKgQcED/QwgKLgOjsPTLwCxV6t8Rpg1rBGmt4FZXlQ6WDskjH2yvCua36WsESPrjLgzQtLviM4IMssoEHiH284IHsqp5CGVvC58Lnkg8mBA5OQX6zkQVofXDPLdMRAqngYIXvsff++wRhhF7O/CUADzwk1Nu/qMkKM/9jLxqAYF7+frMKPpYLrbgVH1rDao7QqIveltlKHskYBpCJo5hTKiYGqivW5qCvZdpiZJaZVKZpkoVzdI3gEgsEaw6kiYphoDgnbIoJ4o0Um2q6YlELSzAiJNdcoPJzgGQsXToOWS+AggpaoOBSlTEPMADAXcFzwWQiGYGkyeYu0LOfoTCzRxGUSDQi4FVOcCXAy609iBiojFevfEZ49aw31ErbdBWh6Ud4WWuyFo5hTKiIKlEAMv9wJUdhzcHciB99CaYJllplmer/clp8s069MyTJcAGaJFYyjyRHSPwPmKRk2MpfcRWEXsPoI1qYZuUCDkWDaFk7WLKZEmlJunX/OjD41LzMu3ZPP9ttRfqldbuH5nnVs9uenOOt5q9Q16IpfvrJe5a+4czvdtdvF7e9ufCcS9Vj8OiD9w2x76bUB8mN8bEBsQGxAbEBsQGxCfb3UD4l1+b0BsQGxAfEMgrrnUHmovw/LnDdO3PB8+A4h4Q7vxjwBi/hgr/XvO7gGLZkzpUFZUdPGoSp4astC7r51LcvqSO5r+nEW8d2gOTE5F3Hd6Oimf6UmoAdFBaRmeKM9iUAM+B0uuPw3HYd7fXAYqZdJZhldLlkVX7GZXrZtKWh+rp6d1zowzeR1HS0HSLdrQFhyvAGnQOcQ9uPGR46yuPhivoRxzsXnUVvZDKp4jtEIsiY4ZDsFZkW3Q45RNgp7idxxz80Dok34DIq387nPZJ59mQKSSk4Uiypxrnb1KaT6iPouL2YmcB6b00TE+jMSQpM+s9B+sz9ZG3cK86YEPDOhYVwMxzIAICyCG6PKexAUQB8660jwjBj1+ECC0bBOIXPMs1oAIF4BQqxxbELspXAOiK6Ph+uh/EkMXLEtMFK8PHohWC5rZBLwCZC1BuowDS6VTOJApUxRlHRUiqsxK9ZIbJQsj4qAlNDv9ECQrCqK+mG7QZeYvIK3TeABTYrCpKcrUFGZATK32sKU8C22B2qBQC3pzkAwxneSMMU+Q++XIxyCxHQcPhPIqiBFykPFQngcHUQe0VWYNGqXEuWgCRoBKHJlBgj6TdcBZai5aCJrZBLwCRICR1QSnXh6Iq4DjMuUMeoJljoGRheOQElILoDZNYby6ZM2lgrPLA5FKthWul2ogED+GJ36N6JkLYdmGeAqQBCExIHUIeYtWyarkjBlQLkiemBT6FEV0PjGhBl4Jy0Kyy4D3EcEWYUtR8mUwr6P0EaWVR5Ix1fFJ17HX8gMNpeaihVAynDA7B3NZlIQaWEgOzJEAEP6M4HVb86QwSABxfkbkAkR9RuDkpdOYuAwptM+IONRnROAQfFki5H/2kIEAX4MA6bg/UXLG4OyZykmeGKQF6eSqhn9OSfl8mmgkBU0eIvlkEup0zNGoz4gsQGiJpMf5jOglBUmpuWhBE3lICJyYDLpGyDXBibgLYcx5vkZQ2D4giO6AaKwRBYgYlkBwthVxO7qrJgUlhTYQufAsZL7nX3TivC4B2Y1GBUIX2aSZXEYFIowKRL4IRJYlmcgx+USW52tEX4BACb1gkTVCc8FIzTYQyGwyRKFg6VUTDBFToUxNCVdN3QAgJEMKnewGxOKqqV8AIdcnJY8KHGoDke3ckC4Frj0UnoWwUpMAUU9NoZqauLcyNQklLRYg1Cvl8mGiLVMT94QyfGE8Yn1lyPyRqGTMjLppAYTVnLcQNLMJeAWR855ETHs84JothbKZTNe/7Genaz+W8ckCJhfmJAgnyK6xsUTy3ciZWBJHuWIn+0dJvsZO830EHaeLgqN+PvNFreRwYU8GD0THv+5O2pCcjpO3aDVhUabFeZByJ0nUxiw9skZA8CI6GBDoV2SWB7K94KKADoPQeYTP5T6C+CNgbUyjcqKRTO4+Ar7BLmeFGeYtuJuz5bZ2P5pndxi3b/VdZly0vnZnbTlcrm83syLEh1vvfP89X+pxvrHPl0f2jdv9GVL+9GdN6z3OH2L/y7btod8GxAbEBkT4ftsGxAbEBsQGRPh+2wbEBsQGxAZE+D3bWpqZzx3O3z883+6XvwHxIdv0hDXXse2MzxkPsuWLr5J8zRTAWkBw9Cjf8AxVHWZWQ8XFqLge5d/caq3RcmiWo2/E+gDDJM/sKneDPFPuRqudwuHI+t2zN5zVs++3llyxNd9azxKnB+RGdkkuyD5cAwI1lbVgMTl+lj+LPJeIReFiwGLN7shi/hoQqVmOBypp+CjP7Cp3IyTjbuSG8UIdGNSzir1RrM76HS6xNRZPX1v0ohTeDEQwIHIFRJ5FnjE8NRfj64EAdyNk425cBqLJ3pgBkWsg1mzNNxpbpwbBIRylB4CdIWwHCX53EtwAn0FrURjE1dShT969MKwAIfEdDd9EDt3Iv2PmfqoHynyAdgaHZ07wE+EltYHSqKFAVPbZrnI3wmDcDQST2hyONnujZqgYEOtsDemJBrBYMYTHDfyEkDR8eByFHpDA70gcauyTxZqz8Bm0FoUeXc0BrAX80oiLwd2Y8fWEPuO4GLDIIcRcXhsfQjAPlPmAQCQHLHuNFisvQ31LpcZxQGzO2We7yt0AEOBu9El61OBwtNkbNUOlk+G+xNbAeMcSkH1VCRZRgziD2UDf5Iw4g1bAahVnDaFHO+WT1lJlCn9GTMANdkb0ab5G9CKrYFwMcC0qIKalPgTzQAPuxNRIQV8wR2TaPFV/io6MTk1mn+0qdyMGH6mWHjU4HOvsDT8J9wWWNbYGxhv8EWKekB4HzgilDI5BREPwG1ERnCIXNcyA6LUWOBy5XiOSzKSDBPznfD1x2LgY4FqwhstQtJGGECrBKnSKtTO6wSR7DAiy0Ys/cyDUPuwqZaAGQnrU4nCssDc8McLBssrWyI69QsyTV50ehY/Ta1d0OGkPT01C3mIgZGqKATQv1OKpydUUhyogwJbwXAzYKlwMgT36AZt++wRK8UCZDwNkUPTHINyTYoPWrxRKDQDh7TMjpXA3aiD6VQ5Hk73RrwCxztZQ7pefmmSxZk7BSdkQSfgbzN44ge3A3IRsi3U3EJ9Ba9Ffq4n7iE6uz4/yDZzuiosxeiDQiipZFGU8poIVD5RbIaoaulgTF0J5GayUIcdRA0DU9umsU+5GDYT0qMXhaLI3aoaK9WudrYHxPpaLAlIMcfRi2jz7QS+z2owIPS/aJdbvrG/jYvi7m3xTqbC4bL22XfLiPhbIWr/rcWztfWs7s22dz/DeRxy3cUPewqe4Nmzv27ZnTd9k2DYgNiA2ID7X4gbEN7G4AfFNLG5AfBOLGxDfxOIGxDexuAHxOyx+9bB9R0rMZ4/BTfU3IL4NEHPOBeUKuax/YeyE/AanKDJG3ApjT3TXq0lDNYeCXgzmZ05neBnyOoPCtVM9LyQLqO2fF0+fZzFFfcfOeCfersazZUutPsuL7WuMkSXnIvD7lfUrtTP9ixK0vw+IFDRWXN6WTuiAPaWM7XqDHvUciiDB2qgv/o5zBkVtTWkA1V5OVsB9DNUYhHkoSxREKiBqUkD5KyITSh+Q+HiCHkeLMRJCg3MR8lz/AnvCQv/ibUBwiDN79kS8Xq/JoSCPHBA55haD4goQWYDIFRA5h3ZMsQQVV4EILq2LAgFZgENYY4wYEL1FmShXyAyIIaRa/wJcBOVGzNkVyoawo8plKGfE4NkTkbUCOKizyDRCqhrHFQ5FJqGJVAZvwG/WGBTemnI4DqOyVNAeWUBtpTPIGMyB6HM1BivsjJEnqpNwSyipSCpnRK9hWWOMgMXB9YR7cDLOxRwIVkTKnnNB5pPjTczZFb3Gu5nbEZKFC3W+nGAYjT3BocOcm5lG6NX1FFYUMHIcTe+CgpkzBoWzpoHSPilLRdobKesQ1e6TH4PDfI04aUjUXl1fsjOklV6VCJJT+6Ax9owRjMqrjKBjWeivIS6keVTqynMuPPdiya6gjCJghYDr0YnehgbtozLsxHVV0agzjWSX3aHJoZgi8XHQMyIGAOEYFM4aXtIHHwUsFeZrsAXUrsegT+11Usegzc5AK2CdeCCCpPHwYdkjCYwxi4MnKmNZuA4tgBA5CMe5KNILocWu8Pw6ZkaMorexCgRUNOpMIwbECociY+XTwcN1jzEoKmujJmJSlkoY1YIHYm0Mwox30mZnSCu86ibVK1kBgi2BxeGAyJ6piqsm41yo2pfpXwgQa+wKR8UBy0OcLsoXKeQlEM1MI5jwVhQwsnI+hGGbawaFt+YznHAp8DUykt8s2bpz3glV9WPQVuuwqQmktMRsFwEieyAaU5PpW+AaOok0j3EukvzWDAhoUyg3Ys6ugHoHymkyMrZblC+SMELg1LGoaCwzjfDcPK4oYJz47kHuIxQIY1B4a9RH0crg+whjoByltr+XSmHBOxniTAOkpdYhTJETjkG9pDBGhqPrsy7WQyqLdX0NZZ/u0b9wV4MX9Tku26t5DfewHG5hUMQba79/DOrxuF6fe/rRt/fXOBj/b484IFb3m534+CH784C4uf4GxAbEBsRXOvGxQ7YBsQGxAbEB8SkDtwHxm5z42CHbgNiA+HZ9/oLtezv+xw7rBsSd21x1wvbPWRv0ZppsSXOq+Ip1fhH/DSVaDAunAlK9jYYwhsuHciVOPte7yPP2mtWj1DUr1VblYPHvCFaMjrT4EJD75FbfV1QnUnEy1claVPacC2lOFd9MnV8kp1S6KoXTihdj40VZqFVbg64z8VIvgok0VMyPwRMLYkVfWNXKmAHhQmk+q0tq+FUB0SJbpFnpsFSdSDNn4YpGqQoQoQZimV8kVx2f/WLc3sl2bgCRRaj8QmfWgFCfki8xtLOm1AN4AxDZvHsfEGUaaalOnKdwCoXfjRFR8pQMZFr1LiiXiqp24A1nPl7nF5HsJVxqQOMH5i9o6IfrUNQyuxwkvWOOsMWaOXKQjCjr+U86bjPzQCGvCvWIot94uXYgm/xZ+RfGJoliUzVJSvnpm3g3mHfgakAYnr0bdGRpXMiHc816OalaSFfGKbRVJ6YAXrbUFdByB6MCMTCiG3CgtNecKlSiL697+/wirIWPzCZyKk3xMw4SSsCxR/yZB21QTsbUhcIcCcHUNUxxA2kCUlpnVHCbPFDI/5LEHmJkA4KZFf9C2SRRbEqKjx7loQkC7+Jo3jE7LRDdBAwWmdxlXKjfr5XvynRJQmTjltqqEwqEMSI0T4muDB3rb1hOFYGq/Gv5RYKoM7HKxqATK4DoYIOB6PW4cO2MOaJR7lpxI1mkeIVRwXyKoJy7LhgQNPm+Snjf8y+UTRKLBAZ6h/JgZ8zzurD9wXzsZJVVNZPMQFS+M9OFuSXnkv6jrTqhQBgjImieEgFCiQKaU0UA6EOY5xcxILpB1wgFguxLnZyT8GGDkR51PUIn+0pxYx0IZVSwB0MhP45usR4NCM+/UDaJOyOKQsdrAjtjntelBqIvxE8nXFHOCPiuTJcU7Acb2qoTDEQyRgRmfp2aMKFIfhLJqUIlNBHTLL+IpIBC06yuUYDgX9BJMrek4BZrpUAKZ4I9pXnWFDccECuMCtW6UYYR7x4wTYVwOqVgU1bNJoHvovgkU9NJ8sd4EqoqicjUxD4yjWIIRc0EU5P3XZkuiaczGae26sRAwzM6RkQoeUoG6F90J8lPIjlVuoCMI3Lc5xfh7CJHKGqETs4I4i8I8RF1Tl19HzEYc4TuI5LcvZjiRgrGnWgzKpAvjIlu4GXQWpePyBvD4OKz8S+UTSKaHUH7ivLQBIF3piUCn6gH8LHT8RI1E1qyh+R9V6YLJQcq43TbtnbtPi+hCh3tS8w8u70RnsYiPeDavWte+fxZW7uNy3fWeaVmno3DJ296F50vHl27lP7vAHF5//2ZaH7Ttj3027r6x3u3AbEBsXm3AbEBsXm3dXUDYvNu6+qf4913de57CbF8gS9/IhC3ZND94Cy7N41VbsSSxyqzicQiuqZxfo5qIgkNvY4urGlYXO52bu3Eu4wh3dhpyeJSq2kM9NQz83Nhfg2Zn4L+e+anoRTgPR/xavslP8JNSiKlp/ameayUOfw2LIPnSfOSm6ZB0JdUfcmkAXVHgFnqdcQQVjOMaIm0GG19o7UBxJAXnswtzbK4zNQ0VMNcXlIWwgL28n/ZethO86F56te3pUiE5nLxyhyrQFgjoQHEsjFJJ79kNnwIEOEmIFoDMEseMlPT4Mhx4Mi2vIPd10Ck24AIdwIBSQCvzIHTpuhUaCaVwKwHNNIxO4EZHXhN9xSFEwFeR8cBGDAb5Be3otcx1TtdYFxIadAIuiJ+fhS2hrAiRmNHRPJx0Awvxunw7AwHRG/SLxLPoXgaQ3AoQByaQDDXwvmpjJCOuS7G/fAMjZWeDnJG9Dl4ZQ5+MK6C/iFpJpXAvxLEq2KAGoa8Ii7SHj2rbCABQE5J1Soc72Kp1xGDphloalhIVFdoBAlRa9RXkX/N4wJ2BHkhr31XnA7PznBrxAlBTFPTYF9ODMHpOBxHDmCeInvOa8RQzogSMYeffQmvUpTPuB/BMTRWejrIGnE6jF6ZQ5gaAKI7ayYVTb1ijaDZ47nT3Cq8vE2nWEaoXkQSsmc2zPQ6dDibjIss7aqeALE7urPWR/6RnGRRPVsuCHhcczo8O8NdNZVsJya4xNFoKAXIGUE/s0NYnBGWoUVaXwIBz0TLwCmUzHo6yHQ5z+XC/iBiOlgKihqIbijNlhwkqsEBtQsFAlHmFb2OFSCc6oYDgtkdyYCQzC1uUfUe15wOz87wQMyzuNSLtQChuYQqIApjxQlc1ECA+8GR66H42ehp1HVrnssllKGfutWbRkV1Ruj0wawETE1KFIHaRRJmQ8iO2bDQ67CpqcW4kJwuqo8BnohIDVn+kaLtQRQcEfahz57T4dkZTjsgqwZZWSLWrpry/KoJ1DHLuZJNxijjyhLcD+5fmOV/WeRSYSDyPKlOWayJuwGNCqhjKPvhKAwJWaxP3ek4HgsbAWoXcQSzoQZirtdxHHWxbjEu0C7agZYHeCRUX/OPiCVhR4R8QN1Tzenw7AynpjGoKp8B0fEdRH0fwfwrVvjj+4gAbY04lpwuTKoBIwQ+GvfD+reWl4XoRKzMMXSjV+a4KYp9C+fgd99Z51W/3q+mcacnV0brHb7cgtb3BeKr/bs+Wtuzpm/m059nfAPimxjfgPgmxjcgvonxDYhvYnwD4psY34D4JsY3IL6J8Q2I32z4Uzz8Lt5+wk9kA+JDgFjjIOgzk8hPIvHWWilvqRGmOBY9WKNIBKIRxV5ef43p4lai6Z3ULS3Wm9OhEE/Ormm1lSV5CHsHdWP7HCSK5Z8PdTexMupuWXuVp6lVqQuhaKQH8S1QWGlJGFj8WxLdMzBKAtAC9IgZJBsJRJrUwv1AREdOUCWNOOuUClV4IFJVppAKJAKhHkKV2D4rENlVj7dymYZbgJiTI+BZrIDoKOyVrgKRmT/kgQgLIA4UGwj6cu57gAg3ABHWgMitMwIPweEhgLDPQZU+wmcBsTwjlkAcQsx8oBuXihYVEMMUdu+1TJSMIVDfABAR6WIGNKKaF8KtkEwrIQRR4NCJAGyHoahsQD0Deh14+78TBkRXQkr8n+ZFYRaFCK2PykGhFkT5Az5yWAjeqQI+f+6d0kdoM0uUX2FqGsriULUOPS46JTIuwkI58UQ3miA7eaVsGT81sQYJhyEXihYGxIFzjVBcqpeAZ7RgZQGClSkECJMF6qHXMcir5sS6EJl+TPL0fVBVilDCqYNNU71SvkqQFd/Li/f5OCIHBBEJ8F71Kx9lJggHVCX9wGhrBH8+OaWP0GaW9PILNzUNjFFfXokvx1kMQMeFR1CYJj5FQVRGygyIDCCUL+GD3QYEdznpOkHLnGYMqc6IpCQV5WWIlAmrYWAPfe/OyCCCZRHfc9LjXWFmAIjuDFkItMbMDilTaAA8c0IXRIf0VcASHwdZEJM7I1wikXUtDww0OCKqpmER6SjsFzmelL0hIeazsmGUf6Kek/UlEFGzjKwDkSsgQIvKKYd1ILKtEZAJKYpNkrhCJ8yxl0QWhcM6Rpm7nTCJ5kFh3Q7KzVI0PphFEoRlUpSkChBZaEKXgLiUHUUpANUZ4TgkYL8UmkAWYho0N4wtNuKMUM91wfZA8I8IJ/5c0UIZHEIlYe2JPJuaZOgZCMt1EovmBUgumpxG1Dx6GqZuACy09kDhQo+XuRuW+rWpSYBIieEAyyIVqAtxkieHrnioQDDYjuiwyixJyhFRNQ3LFhMGO94rEDo1ge1RpqbDWE9NuIDQaHoMks2ElpW5ogUygPB/ko1EWQzMRwCfg4/TfcRRFDvAXFPNC5BciuoEpiZS+RCOOetWnKBwoWofR/ElwNJJpKWqxVrzogS5F+iEZaF9GFi2i5kgJ4hmqYdYI+TzYECsMUugsMGLuahpoCSkrGSx1sX5BM0OaZP5LkEkunJQzw9iXXglfCFxHCvGwTJ/SbxyKafH1+5Va4t59vnf832SDuv5Va7xJvK776zzTR5eq1+P3SfwS95u6u1d89t9vInvAcT2rGl76LcBsQGxAbEBsQGxAbEBsQGxAfE9urYBsQHxOUB8t4G9zZ+v9fo3ZB3agPgiIIR7MZbvA70HDGYBBw7H97t9zUamPy2eBv09K89BeBd1rVIOpfQ9ucLESG1vFkyN5Fqsfch1mVS3FqrWbtlyagNBARKO7Glb9PR+ALOAowxXnm3eIuqbVoz4JOEteoAqhZs3QWIelSTDUJgao745WigMTrCi0CKcmkhyo6CWqr4NKddlHC+EHrWHqrUl6KHdK/qxJ5+YJnCAZA7E8QxmAcI9HwBEeB8Q5k3luCunpbKDK82sen6Kxqtn4iBXgAgChLV2uAJEaAIRm0AMiNEeVf2CzwgwCwAERaa8ugR0Jw4uTKPfiOugGh3YA+ZDqvKWHIpWBYIwmptF7audGDSbhHjTYxqA0oZxQmJhaiQHRKf6Iew/6yQIwwO6MAh86T6UQ4sU+jlI3zTUZWVC1ZqGh9GasVSQJYL1QU42Xt5bvEBs3BnI+GfEa5G0AGsEMQvwHnWflDKHgCJ0J1J5A9990xA7vwiur4WHRNE7/azcjiAJPoJkITGdDrUjw1J4Dsq76OU81Yh3LEwN6rICIeHOPnrBIg6jisJI0twqr6ztEZESauAArKiBIOZYlwlVayVOn+BRxVLhJA9TyDQj7h0qb/WMUO5MUNYCMw9kamLTWSPRyksAjyKJ7gTKc1aQoN9oAEWFghIiiHudWMdnUAqgXMEvgJ9zYVxk4TvAjj8jmOdQhjgGi09rOZSyM0KZGNFNSUfmjCTps99XYu4DX74M7FFQIHyZULVWM1dqlgqOM+XnbMfUWz0jlLjATskZgZwjAgRV8UDwdDaSc9CdQPks2UnkW35VFQqqNGoQPpW8JaqbkW0/FD6c0kWxM+gaITyHJhC5LH9R0tB4LomWxU8nh1fJ9RILuw/7pNwAZkccehaBSJKjqyoTfGs1c8VYKsdzAYLOiIa35YxQIJICoVOJAMHUDQ+ETk2iO5EK98F981NTKNIhfbK8JaKbAf4DEScsJ4txJMSOAwIVHBCpdI3LCZsv21WT6oe4M4LLQ7WjTE2yT8oNwuHI4F1A1mdWJvjW3NSUjKXizwjQI9wx8dZPTUj7FHAfEZELcTwyEGAWIPUZBloXaxbTGoWpMHaSnUS+MUcDvIokpZFppLAsRDcD/IdItU9dYVyojoXYKfcR4s2gOlBH5olQSS0HpobdRyiXhJRDSj49ZnjkojBCi7Xuk3LcCpT+0Dc6cWZlgm/NlnPxSEYgBGiGHClfDPKxJOtV5+4juqJ1sthuYWXUnIpbv9Hnf8+5Uaq955I/c47Irfe62V3jx8U+S064Xu+e1vS69k3efvbDgnxn+Y99xJFv2ndtz71AvMnb7VnT57e2AfGlPm1AfBOfNiC+iU8bEN/Epw2Ib+LTBsQ38WkD4pv4tAHxTXz6pPprlb9iCO5t46N9+lY/zg2IT9+6xvfl80A8g8fD6CXrg8IcLc5Hd63Ny8Fex+SY7I/rr5LlqgbKz9RBqtZSPkLvmJ/3Hgckvx/4PUF6MEl/4zBQdDGrPXlnb2HN9zoW5ses5+djmDNCFlucHcOT+nmSCqhY0EuqTdYHv4i7pGu00n5U+9OylDMyOCZH0vdWLeVHWgIxmPrGTB1kBoQogEsEBOGpQVXMEfXIxB0AEF0IM2KCJSZJznwUcYpUt1v2ixO3AjGXlQAQrLeR26wPCiu3OB+3ARGuAqEt1GXaQGj5mTrIChCiC4A4Yca75BDHyGnsEWqS9ufAprm/twOxeFEU+hPyau14Vp2JgFddu3AuLA2oWGAaWrI+SLHDcz6cdsfYzjFiuVGQl4XCJqhTeBMVk0NT2RiPw/u85HTQu7tzjgWCNIgqKxAHBwTe2zYgTiekIpD2a4YIh7kQhCXPz8L70NGMlX9G1cFLzBSWAp8F/4n+BCLIIb2KzgTyihCqCP1RKahY5NxmfcSROAuO81G0O6D1scwxEkpuFBAVhpIMQD2pmRxRpFCMx+F9XnI6ppl7qDkWmhxBVjdaGUi/g1+5p1XiqDFrfjM6MnsDAi+w14212ojqkSB7zGsKSVM/ucQiwhxhkpwsYiloXhrlwmSVPQg4B86vIlygGUkICMFcxBNybrM+MCMb56M7q2QEtD4gg+WlFiw3Cmy+iiaHedKF6owQKRSjD3ifl5yOsOBYaK4Tnczov0OQqPnsjAgJPuVyoRJmDJFYGJrIlfGaunMBQvIzmX/ujJBRKhIZPJJlUMKo/B0FAsbgCnEy4Ax1tcX6qIHImpMnFK2PcQmEyikgL8uranKYJ2NFIDCGhhJbnM9LTkdocCxy4VgoEHnIzTUC2h1DE4gcDAjhhzAQwoQaFIh6TIvC06hAiEQGj6QblMQ8A6wRmiwJUxO+QcWiUDRnrI+cas5H7qFdoVofyxwjlhsFeVloPk6VJzWTI9dApNrnJacjpzTnWFRTk2ZNWb9qUiDUniOvJT81FSB6VSHJJfmUjSlPsoN6TWMMPkuZmjSVVxxVceLIyhrM6eAltBvoG1Qs9D5izvrgb47z0Z1yuQQAp2OueWG5UZCXhZhzKXhPaiZHJ3O08Ti8z0tORzdTBwE/xO6AOPfiGflR6vsIvWcoQIS6fdg7FvEwZI+ZWj/xnU4OMnbeP7qPCKI4EmWxzoULU7z6182cLVUOnJ7Na9/V/fkiu8O+r5XKV9pY9zm/4064xSXJ77yzzo1PRnK7WunWAV8H4n3bNSAutfS9HnHkO/e/0ZntWdMXbxsQGxAbEBsQX9fHDYjfYG8D4ova34DYgPg9bX9U+a+2twHxRe1/aKO/c+D/LMg+aUQ2ID67zcsbPzitc30QZ6Ot1cFsjvEW1kaLxZHvcqyb+9mub89pi9ftLVc1UH7Bz7iJ70GfQS24ZiNcYqskpwHCgZ2hVjam9PT0Am+LtUFH0oy1MSMFmOWqzeiGYt23WH2OTSBmtUvkgnwL6Xa+B/fybXwPSpiQ12zE2lha7alEXbSjAkT0QOSYW0Dw+bBgbdwGRLgJiHULNwCRo/A3fCvrfA/uS3oT34PjOjfZaPE2qpFXMXYkthjAgChADPTGcYu1QUcAxDwXCzKprLE4jpwPBW8ee7aGV/uwrClBcqd05ejBZSxBOIsDV+PB8T2YtjLczvfgvqQ38T0yom5io5/ZKCyPNm9jNvJZMj+wmQEMCAWCOBt9arI2WPZEWRtzdsY6iwPchjzkGVtDcizMsqYEyZ0S5aiyPlQpIwj3QzKsDML3oBwrwx18jzHnmp9xK99j+jtKIJdsnGobFkpt8zZmI9/JSSLUlaFeI2gWbLE2QhKInHSDkQLWWBwdrxFTiBK5SBxbo+QvqbKmhJBLshmod2QHF3gcxNyAGEM5I5hpcjvfI6Sc5woeN/I9hO1gI1WrgCiUbd7GbOTBtkFKmeViPTmTWqyNkND5NSDaLA4s8Myc6EPN1tCMPXXWFAdEDsb6MCDC+MrTRp/8Yo0EPLfyPbQvb+B7JAMCI2U2JJsQLLR5G9XIu6kJQICxgGUMmVRarI1ypMnOWGdxJNFrYX6fZ2uEvJiaZkAkY330RYII3A/SVzIgeAkNt/M9tC9v4Htk5QFyYpFKBYQmbjc1NXgb9cibgNQJANMsq7k+urFcHc9YG3oEAzxnZ6yzOKLmQ0kztgZzLBZZU1jE6ij5UYz1cRBfINHFrbv7CDA5bud7WC/v5XscB/ZKbVQqINmxPNq8jXrkw/w6PV/N9XHtPlJ5EJdYHDnVbA18W+N51N9sb4vx8fV8j7aN3LgMn/M2riQf/KhHHPnCMZ8FJYf3b/lG7/Kbe3PLiF3vfX6L2e1Z01fa2IDYgNiA2IDYgNiA2ID4oPIbEBsQX9TsBsQGxFe32dr+By2Ma2d4lz6QAAAAAElFTkSuQmCC","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"iVBORw0KGgoAAAANSUhEUgAABiAAAAYaAgMAAAAgkEnVAAAACVBMVEX///8AAACAgIBJX6m2AAEdZklEQVR42uxdS46kOtN1WqRkMaKlqrmVI5SryCv1AmpQtR9Uo1wGw9S/yp+IE2GHweSjuro77/3g6lYn4EfYB9tAHE445z4+nNnKvXK7du47tj9Z9+9uyxcs2YB4HiCa6T/eOjm8Oy0z7N/pXNM13evbfn6ySamKPT22v2EMlaomfbynclJernuy8vXNVcq/t5Zq85up0dJ2sqPeqqLGld9dakvRF6aNO+rXjvp2NyV/fX99n9qN/heTOvwkPD4+qCAUtks10d/XjozfnXanrtlJ4TtjUMP7u8K8RlKldOaXLZtKdVL3rkiHvFx3Q3ghy67SX2u17GY9upsDccp4aCc25p9m0flNcVKv4iUQuwKrptvTuTc6P0H01lG3T+Bw/zvn6ccJpZCR6IyuWXYWgCAcu1lHZCDcTSBcFQgq9SYQk+Vd4yoj4not9Uv7LiC6KhDWiO4RIH5MWXfchh9u+jcDQRa0w8fHlOG0f+umKcedPj6oS7pm/zb95/Zv0zT09uONpoRyRNDRH1OK6f+fE8I/kXLv6AwfkXzT+bedQ/qp3I7KPDXY4xycT0cE1c0lvFEj9pJX6j65k9j1M9V7Ry2Ulmv6+QO1caqPjx9vKGvncHxK9e74nLTljXoFbaf27d/euTZ3ynZTildJTXVRXpmW2CJNh7J3U782DMSOYOEc1OOUjkdEw0k4Racjgv7jjBOULzwY911eI3Z89GVK8TLlm9K/IOUOJb2gBMrXcDlcEhXf0a9Tg72GUzVsHtYIjAjUzZdeAyCobvr1+kbHupdU7x21UNoG7aOEnOqlmepqcll0/AWXeINU2n7KO9X6Qg3/h2vbGbt3PFE13C2Uv9HJM+WX9jUYIWRF11D/dd0L5yAgGgwyamKjQMiIeH2npW9Cajqzf+dOngaTDueOzrynqrhDpoVnOsad9c6IihEwl45SGdRFr+//NNhruJ4mLWo6IhIQUiLqprJ+UI53U+8dteBymqzqyEK56AgIUxYd72SuQSq1DG2nFFSi1qZnkZb7iPM35ibj9Z37gm3TyRNA8K9O5jQeEQLENOQFCLNGvDm67t8U/TkQeczsT4x/6gyYNO8i3A80J/r1TyN7NK9ONbg3BWI2IiQv6qY5tYOlud47agEQE6gndEkGArnd6UWuW56UpeM6Xk7d24sAQS0EEJ2xu3NauwKhbaFz3BdSnl5iuEwJIgCBNWI6RmvE1D4Zuo1bTE0NTzvNAoiXNCKcTE08zO3URMdSFzWpi5qfP2VP0uhwXk5NKBF1n+ieoqNjdFTqvaMWaQU6cOpXAwSX5U4/3uh4p1OTw940uU17P954snuhFv4jR7PdJ06LPupk0ppGw6mYmpo8Ihgv3JB0fBEACBoDLd/HnniJ3b3pcwQWRV5Gp+NTkW8lEK98lGY7SonFjFLKwiXLKOVrnC5n6BT6NS2m2ONFbpcWOHmGkSWOy+ASDRANL9U/U7131PLKKfZ8jtrI5xgIlEWzBLe9wSKLVJSeemW66NC+n1j+qfOz3SdOSy2nMzTm2CpZrF9TOgAxrQ3yHDG1qePbAYfniOlXfOjJGg9b5dY17lu2j/fHnqwfrbdMb8vrruV6oIbdF/J84ysOvZ6/AYqH6n603jL97wDiF1u9vWv6u9sGxAbEBsQGxB9rywbEN9S1AbEB8Xc6YwNiA2IDYgPiqiXfa9bzNPJfZ+EGxFNsXc2wsJa6Hd30qlC3KEdmzQz83+JMdH44DsdLdMfLccglXNv8mNPF6znitfOxaiHaih//d7luS1z89fZI6rF29m8+3rp5mojflBfcjQUGq0BQhWqwR/eEMnluZgzzpoQ4bY7/izUg/PKKCNeBMFUMt4HwCyCmI8Gb1lQuBannXiD8oi1B9v2inVEscMrduAaEN0V70yGpewogmBeCvl40bDoYpv9wOrgr3bbs6FjthEeBSJeKWIgirgPh7gKi/zUglLsRYzvSRNKOh/FI/55p/zLSfju0A/2ajk2/6Fo+0B6dcfRr6tp4HDU9+/wG/m+qhs4P4TDqlEZpMxBSyohf/B9b4NJfslFrc9Mvri/CKsdnjiPZQ2d9cZ6aeRCb8JvLcXQmW8g1nukX8msfwGqxgu04iqVlHTgSw/FyCNoWL63xTluIvJ7PoB2aBscnC5S7MXVP4Ekn9DwXxH6aRcJnoH0qgH7FiKJi7GkvhOkMfk2Z+VqnVDQivKDfjtN1EobAV0sAENMaMYE5/c+1Ufn6K3LuyKljynMcQ0A6uur494C6adqafk0XRuCzvjgvVynbhN/TMbbYGQu5xl6B6FMfwGqU42XM42xZB45wrrO2xXNrMM5geWBLeT7hXNSnLmjr2QLlbkwITwsooR8BRGgvxwt3PzfjM7Q6q9M1TXuTaVTwtPQCCEdAUBkfH2kCDlQOuqK9EBBTajMiaNlGDVQ7LWbSCD16cVwSncNRAvw47QcZ8vQLlwE6oDyP2skm5G0v6NRsIWpUIGLQPoDVKMfz0hrkbFlH69TW2MeAFNwGrksn48C5CQjkot8t37LoxeeUuzH1zsj9PMqI4LncAuF4MlAgKHWQKi0QkWfgNAEz4p/Stcs1IjoFgmsf3VgCgSuRZ+cxAzFdjS6keTsD4Yb5eRmlToGQRlsLuca0RgTtA1iNcjxZxl0Z3cKGkWvp0aVIob1SAwK5+iBpnAWCuRt5ajqMAfj2YkyemrBYhzQ1OTM1BZ2aaI2QCZhLi/35jCtmtkbEPCGVU5MBohcgJFWPToh8dQ/onBAyEPPz1EWwSfL2VH5poeej3jkzGtVqzA4yNVVs0KlpmuNNW1AOlapHQ0hAcC5aIzCCok5Nyt3A4kyzeuQ792n/TAsWLcW0RNOvlpdtWrSxfAZZwOiI5385lcvNpNLaM48gWpB4KNJzBP+HpZLKN4v1kY9wTslDQLRydLpIHHIEmS6mpW6qXYCI8/PTrzNskrznI50tLDxyfoKKQRqt1VQOrVK8BIultg5drHGrrm05Sp8cR22h5zxHbh0bSmnO2nr6nW/nitu8/7uU99FymxZ85T5/eZs4L+33bPGB8/GKhfNbza9YHcM32X5f182fk3+ttL+53XrF8ajV9/bMQ0D8/mY+PxD/EcM2IDYgNiD+Fc3cgNiA2IDYgNiA2IDYgHjyZv6vAfH7rPkbdn6lzi/buQHxNECAK8FvLOXfCn0gFufTJu/++Y8fcAgcjZjPLN7HKItDt8kCvDu7VN9V8bnpLXFZqZyi977HiyvSlpYH5Sq4YM7GStoo/6s9ps57tlmeWLbCzVsWy2ZOHitlNKg7e0Yf4G0ozqf+8LlnBvXCoN3RAhVCkRMpipJGeAXIkUn/BtsErhtn4PvztpeDVsxnBz+/UAYLRKYdREkbTFoFYjJ3Xudiq7yLnuWpA7E4M2Qg3DUg3AII9kgXQPDfQS4w+Tcac+OswCXVxkcFYm4u6vYFEMXlHsb+24FY1nnPNstzNxBgFRAQ4EEkXgE7K0puhbfnJwzj0DI/QfAc4FrBeTA9hOsg09RReR9njIjMqsAQmi5qSjnQwKaU5FhRHsbUsIE8a54dLOy44pzwIscw+dMcuCdHTgvX0kGcTdQthtExKP/jIGmPws+Q9hAvI5L/sawzl8C1kv3sQGqv2FlyPigfainPeCesgo+P4wgehFNeQWDXZsGt8OY8jw32EYeAphC4YCnQ0eNguA4RawSlZedqxBqRWRVoVmQgWr6eqDZ4d5UbQtcanREX5eSIVOoZ+f0+eVWCAxOdS97b7NLNJQGIng1nNsUAl2s+xmkjALV1Wk4I1cWe5x7+Rz23zFPySmDH/Ix3wiqgEQFva0vOzIvnxs25Fd6cx9LeggthRgSzKih9NFwHWcCD0g2wfsTMqkCzYhoRcOgrieETNQ6YezMPIpipCRSBqfsuCsSUZ2COiQvOm5KcwNRe4EfOQOgxJ3SIZZ2ZEwLQ0RqMSpxb5il5Jc4pN8Se8TpzZyCikASEiVdyK4Z8nos8uzFIM/MagZnXch1kTk5AgA1iWRVoFgDW68kCEc0iGJ0FQqcmLNYtuBWylvmhZ6ZEkK5XuHy6LmPQNUL5SRaIZZ1aAqamqCPiqp2W8yHWLs8MDgPXANGvTU0CRG+mpgh+gr1rkqZEoZxFUEnAqEBa5vwMGBHKqnCSYQZESA3kulsZK2CZlCNiSJcP7nY4HU13IIkNpiS+1SCeRpByPTM0XD7GUxOAKOoUTogdEUqNSOdmeUrOh1q7ODPwzE0ZAcRRGBrMQzgvuBXRnB+wNPoxFM8RTGQZ6Tkicx0i318hLbMomFXBKYRVgbLycwQdneboUZ8RcK7ltawdwTI5jmH2HMEMkxH3//SbIZcjpiS+MIin4UfwcNrBjyCEyjG+oQCtytapnBDUOkEw2egCW57PzfKUnA+1dnFGby/rT4Ill+PmHfSVLc5uXOOVs/a5tLQg3nhi9QtGdlxPuWhLJuXbWm/ViavwMTuv9O2ffXVwi/Pw9Vccy+8x7rs0rqW+B4hH7fxC0//Gtr3024DYgNiA+GsN3IB4kgZuQDxJAzcgnqSBGxBP0sANiCdp4AbEXzDsV8v7s51xX06JdvFLvfFLuTcg/gwQC1JDZm4E9wVWw3KLZWWz8r7y7ibmd0xH/aVckFl5dR5KLikUtcXVGgO9YXVhadnsjWaNF0P7QWuKa3UoOyKqq8cSBpashnuc6rO0Qz4zL88/CISyS/JbV/2V3igndsqSHmHtsjyVO4Bgd6j8LVs6lEDU6BgAQmoS2YrZldEuO2IGhP8GIFpj5oIl8ZURMUR3HQj88FeAcH8BCG+BiIPVq4iDsiOcMCwsc4O+iLUMhctILvaldgV/vc+/lUWRGRLs7ct1lswM6GJIflsmNTcfWbJLsuYHu52IJUJcEMpxVq4InFmmNcLdgF1gl6jHmS9KoxOSaz7gY2bxrNPfJV/Epq/xYg7CJUHb0UtTp1huwuRBVXYEOwJL5gYBYRkKn3AoLrQrZKqc9pVFoaoZUKQwdZbMDHz6LfltmdTcfGTJLsmaH+TbozLIS8ZKF+La7UMaEdqaRBnI7BILhNUJyTUH/ZSdIBgBxJwvYtPXeDG9cEm88FXYPdsW3IQ2syOEo1AwN4aSoaBAzLUrMPzQVWBRqGoG6AKmzqK81vjYyjL5unNG12PGLsmaHwfhn1AXJVmGoFC7gocCIHATAj6GBcLqhOSaA5dfjog5X8Smn/NicqnadvXynwu9ivMcCMvcyIs1OleBmGtXMFdBr1qnQESIOfTTrJjqLMsDWwT5yzK5a0bDTJqxS7LmB7Ei6Mrz0NvI+i8JiMxDEb2C0bBLzBphdUJyzaw50pdrxJwvYtPPeTG51LRYZ7pF1quY9pgd4UsgEnOjZCgwEBXtCh3Y+l8IqpoRWBsp1zlnSdipyZaJNEbXY8YuMalJuOEsQPA0IuuSCAvZ1gSZeDK7JHMwfKETkmsOTBOwQNT4Ijn9Gi/GSV+qfghzEbJeBSlwHJnXQVTA44y5wcQYw1CYcp2ZUTHTrtCljioUFoWoZqAluc4ZM8OoUpRlTgvvUOh6zNgl+RyrYQQ8RzBH4hwdWuJ5sbY8FC9LcWaXKAdDNEfGuTYH2oGhpM8RS76ITV/jxUTlkvBzBNgc9kmkuOlae86oMxTijRvYzFlYUo/Xbk1jcfvqzZEaAyIu7pe13FjcSrrZXsHYCGqR1Rz5v8uiZlM+LFuqfdyz3cP2cKLNdOc9/X0V40q/p7wlELdquQeIe21bWjQrowDCfRmI7V3Tt+TcXvptQHxD5g2I7+vLDYgNiA2Ib8u8AfF9fbkBsQGxAfFtmTcgvqsvKeszi578fdv+kAXfCYR/4OiTdcP3WxAfSRzwzjGHwRDOxHr0FVOH/auu+mV0FhcsueCa2XGtG2ysFFfT7qgd0bgtV5uz6K+2yNCKBbasmqJJ/DUgEHmE3qAvIqaEWr1BHEnrATBq0VmusQjMWBnYHTM/WgHCz4ozBgd7PoRaPWF2JNTjvwS7p7omtXphe6zY/eCISEBoxJSrQLjbQLjKiFgFws2AWBkRNlbKNSDi2vDwbs2GO4Fw9wChOVodJ7XoI3NeRo48Qu/fTcQUSLezwwMlgRlBgg7TLweuxFokEnYczqKzwDHUjjZGC+yhFBr3BeFEWpQp6h4tc0SybbDdyye0aiMzSZh7ovFUJu/3qHFbOCKMaIfYaC1aR26Lxkxhi8444rlVqmui8vXz1ngT2wWKJsSGcfXoI0tehkYeIe5CjkfCPjx1AUbEDuHpJoI1gRGxFonE1aKz9KjNxmjhmYNTaHAFdjwOlKPP6h5sbbYtSDQYRC5RG4N+zJ7iqTjxpAVpv4b1sNFatA5tS8iu2oiyQ8ixKaBrwo5YU2+OOJNju0DRhFzOrh59ZMnL0MgjGBE6DRNnolV2QiSegoQLEHESddHXI5G4anQWA0SK0dJexMEfib2BGCfHxOGAukfLnI1sm/ItwKLIwg4i75DiqbjhKHFb0P5WuIg2WovWoW3RGC+wSH3Z3mk8GeV7tLZep6oiNrYL2cBA1KOPzHkZLkUewRqh0zBzJuBgnwZQBFdiXALBaReRSFw1OsscCM1LKagOh2llzCNCiA983WfbKL9ELumjUdjQERGlVV5SB61tzOHOLBAaOETLdkxKQKsViOgsEDEHJbGtGWYqJGcAUY0+UnIocASRRwBEikfCAxPDbzIpQCRIVTryFFGPROJq0Vny1BTy1BT7BETQIDbEQjqA5weweqIKZNsCt4KsJrJCmiICazEJ7wXkoRRAp9cIMvNoLVqHtkVjvIhFfQIieAtE6puiNUOO7QJFEwBRjT5ScihwBJFH8ByRA8NwUDRekLAsCjOC+FEcQeRaJBJeLOfRWc56PMdoofI45JMEDXPM/yYljMhyXMK1IPpKsEBQK5gXkhZr1tpw4J4M0ipdrInRgrgxy2gtWofPDIxEsUTZiJiC4G8KRO6bojXRxHahGwaq9Vy5iY0rR9Lt4RO94oizqIh817RsS3ArrVkvt3LsLjW5pa7JLyqarJn7nEAkxZixBkTJHIlfrOsr8WR+47a99NuAeKZu2IB4km7YgHiSbtiAeJJu2IB4km7YgHiSbtiAeJJu2IB4km54LgseoZ38B5p7s1a9SP+4bRsQ3wxEjm2SNY2X/IrFu5bLjK/AGyKqTO8tB/rPFfFRoqu+6bkeVmKmbhGyWvLgUnnH4sValRsy5bnSAznlXUDElbNHZbnk/jD2r9iVeyHHNskq30t+BV7t2ipKvoLkjeg7jTuvbyNDAYRfAWItCEfeC0k/3MZsieWFVOWG+Crusg32TFi9TmyIEf3l7dlB/nemR5P965yVkDvddqZ3NX5FFQi3vHzlIk4fsdaAcN8BRKgBoR635ZV3Gwh/NxB+7WwBhJsD4dZGhFDETGyTQc1a8is4UoJhVjh2gSjLg3yv9EsJNgoEpbrI563gNuQ4KiXPARyQejSUfF46jdy1JmZLdIlDMpAAe5UbMizZIfYChA6C5W4s06smgTBAUCPHX8FZL/8fpO52hJfyICLwNbtQB5QCcmyTQWOjVPgVM2YFvpDO6huIjSJARAWCUn1ypBJmhhgGw5znAA5IPRpKPi8dSnWbmC3HIef3K9yQ1lXYIbqNepFa7sYyfbrWo6pqiIs3ytkR/5Nnt08SGk64H3W7hMwg+ifKKkgjosavKJkVTpyCmQ0XExB+sCPiE5FKkgSJMhhChQNSj4aSz9sRkVkh0Znza9yQYckOKadkxFbJ3I1l+gTEoCIXquKBszoiiAMDMoFP0VzW7LJAhOz6TmZV+BUls0Ld5DUg1LmOVJ+4yhIQymAwPIfMAalHQ8nnXV4jMivEnncr3BDvluwQu0YkoRRXByK6PDXBRgtELNaIXllPaYRnIOZ2KRDBxjaxd00LfsWMWVEHAoJQ0dm7ps+ASCWJy9H7xJ+Yc0DWoqGk8xkIE7MlOj0vKhoVbkgCIrUB+k2+uGuy3I1l+iIwW5pkRAFkyEAQByYRZhIQdbtQB7Ogc2wT+xyx4FfMmBV0Lx9cVt9g1t90JdNySVNLfo6AZkcrsUoyg8HyHJQDUo+GYs67/ByRWSF0jQqHBCoaNW7IZc6nYHbKwAodqd2Wu7FMb58joKrhWeeDoqq05jkiSrwVFuiKYLuscVZQh/uVrR75xC/52dWtVLtYnol31j3Pe//3FfHh9B8f65yMaJ6so/uj2+99xfE1IP5mi/+jQPyNUr8LiKcy6/d22QbEBsQGxO8vdQPiN9W9AfHbu2wDYgNiA+L3l7oB8Zvq/tcC8exkmb8NxB+zeAPir1usET6S0oZhbdCXaPjS7MjvIWlv4BfSxKSopcObx5jS5BDFeWtnShbO7MVVM/Us6RzjkHd4R5slelP0EimZrVwUFeXvWl2x8mu2H2/ltBEjtF9vAqGi4ZohpGAXwToxwsAOj4HdoPBpVdLF9PUzpynjR6tcehZQMG9AQyiaYqKdyG5IH9EqoPiouHWZI+ILIGDBLSD8zMKYfteASL5Ck8evABGqQPh1IAqlDaNaUXiT0MkMxEGAqKQbWB1+0DRXgSjbGZdAuKtAHLhWHw+uIOs8DIT7ZSDcLSDilVfKlpXRWqUNFhFX3kQdCO/W0h246w9O03insUnAfIByBaKNkHo+8R3I9UJ7R+FmUMQWjXYCJ43Vw8hA0JfUkS8kVfkgXQJ21ZxLIOCUifFi9DQm31o0vIqz1tqKMyzrb+CI/s8fLnMt+UjrlKlCdeSPldVi9GsZDUb7xDIaIMU/qkQ53HmBWR3T3E/O/JG/bI4070/O9NGnqWmermcgeqdpvNPYJGA+BAkcIK5GnsgGcTyyv5XDN4H9wcf4Iuzl0/JepB7SGnGmpnvhiUjJvX48n4EQ1lv8FKdtBAtkiKLaAb2NzDnRj9ttXJVeIlLIDBCj6pWI9xpMFaojf76vFmciQY4Gw1NACCUrQ3kIqmWRwl8MB6dXOnmHqZM/zYiopZuPCNHjEId7kO6SKCsELetgBNHEODKJEuwPpsgMqrWhehgWCO6eqKEyyG1qdT0MEFDQiJ+iDcIRX1iNBKodUYBQzgl0P1R/A/okzMIwQBwv0CuhPN74pqFjkqkUsBj6HzYaDPUJ5S1YGc4obdgO9qy5cQjQ3lgHwqbTNSIDEd0SCGVgoMSoHArmZnCgkJCBiFZ+wZVAIHiMxizxha6HBSKSgkZUfY80IpJqB/mvM+ckMt9J9TfciJGDf3HhtdAiAeVgBkSfLFCLZYkoo8E4YZ9YVobyEFRCQSOdcNwVF8+iscHddj7PgJil0+vLABEy8yEBwSQNLjGQ8gfvEbMjYmqKPaKdUE/oZIMJhoDQgGygoeQ68tQk5zMQy6kpQnooa3pkzkk0kkAqugJuidDqBg3gQ8yUoPFdZGrilD2reojFKptRRINhJovEJhlMhA9V2nApDonTYTqCjeD5aYE6mVaEtXT6HIE0PvMYOC2UK47MZxhoipIFmvdGjrgyasSWIPwQxGFRPQx6jmjBGKF/eX1CjVIyL9aSYgC1VBQ0zkMwi7VEfcmaHplzIiJdOa6KxGfhXqCbk6jxUQLrexwTU4XqiOCouGyx9msRDYZTVaObrD4B3qVF8UC6X9rISn+Tg+HvZmnEfO98R7riXvt6iYXFDzXvf/MVR3S4mb8vnW7X0sdfsXh71/QkFm9AbEBsQGxAbEBsQGxAbEBsQGxAbEBsQDyvxc9p1ppVt629leJpL5wNiPpm2Aa1LZo0LkW9aB8twfyqGeZvfSYU9C1bescTrT38sny1bdePKoulbtXVblzNWQKRrWvrSZKrveImXwKBP/NYJv6+ElymEVjDjYtnWLFP6xCBiJnLR7+GjdVAG2GlrELIIbFYqlYtu9kG5sg5/SrWBESoKZHUyv06EO5uINzDQBR1DLMPHZF+FYhFf6wcDZnF8ktAuK8DAcUIyzaAVIFqRmhkEnKKmJDxg35ye4ALhvkM84gjypew5bXC52DHzqisDvnIVSOlJMZHwXYg54t0PTldLhLDBJFe0JrcAo3BkgPcz8viv5IWHBTyUNatouNqF3RJjmqh2ETfVh8Mj6NMA6DUOgi7t9mNREfFwWfYBvYz7xA0Mgk79ITfQJFVcB3ymYiQBpbxUPIlzIfeQfkcRLfKjlM4DPVjdetWzWwHhE1Al1MTPoOqJYg9RQtSDBb9qHxRVtZKUA+0X7WKdA7ULmZtxPTJe69A9FJfqKQBEGqduJFLV2xgLoVlG0BFJSiDTiKTSCSJC/Z4RFy8+IDbxNKYRRwRvkTr5hoUPCKYywDmhpPoJx5p8UH5jO0gygiDzkKITqKRXvJqjhYo/wNtq5WFuCxObHDSnXWrpuPJLiYqDMrHUJsgSaE0inkaBQLWsVWxvcSsSCKu/4JtoEC4UYGIkGoI4CpEDUAh7u/JPZ4c77OII8KXoF+lBgUMJy5DFB5i1Ot91AbP2Q7ocO8yEJHTZ+mKogXC/5C21cqiuXrEOSV2rllFfK8MBCmFBLGQRiNy9gUQszQJiCg3LC3IeL32lXaNYRtYcZzEaCW3eQ+uggxHUdQAbSVNTWXEkeXUJFQzHuy9jjOwSaCppFOAX7AdvDRaw9ggOolGeml5TJgWSAyW5dQUa1NTYrHUrcpTE0oIiQFD6xNykriFAaJIAxWPEEyoKokAo33FcUpKtoHyCqAZIZFJpoR8DMoUzDogRQ3lOoA1sYg4InyJigYFLYvnVo55TsP3PlGXMVbUKNgOHBslanwXjU6ikV5wb162QNu2ZE4chbypizU0PojFUreKF3Gxi2oAawWLLGwi1hLqAxBlGqh4qHVHUTbh3NJXldvLldvP8PiZ9W0ed4QVLsqQlbMcWQMj3rg9/voWV63KxxcKIkFzxoqtmiYubm9Vcu9hI9f5CvdGGim3yn33H6HhfM2qK8/NYvVV/Y2wLEFjLLm/vW3vmp68yRsQGxAbEBsQGxAbEBsQGxAbEBsQGxD/q0Cg2q8361HTnwnW623fgPi3AxHpTxH3w6r2xlrq6R0Xvvn8EieCi7mHF2FKZI9wNBbii76lZdca2S5Tp7JWtzIiCvu+U9tF0aS9VfGtxs2B0D85isqaOAIZ8xgnQtOoav0aL2KWzmkHlkDAzxmLHPFaKUOuyc+ACMG29xYQ3radgchOq1rmm0C4a0C4+4DwXwDCLYC4mg7CDksgXKnnsQaEqwDh5kDM+/ouIHxS3ohFuX4diBrTQvkNVf7GlP3A+h3XVCroe+37OBE1NYsaL6KueuHT1KTWUjp0XlbEMJaulGK4FabluSyNtOKc5ZdAu+OQ3DyISKNth/JGRLnUj9S/g6Y/jCXnxFl3ZmZuSAiCGn9jyh7YUXlNpYLMuI8TUVOzqPEi6qoXxzEDITb00AyxihjG0mopSjeLEJIIy7KElyFXvjpe+/QBvoNPPygQCOghkTCYU9Kzu1gVQRzXa1gzIStTFMwN8Buq/A0HAYfPcE2lgq+H+zgRFTWLKi+iqnphRwRsUImJrIhh9TTWSmkvBgi3LCuTbtqLMDOMJAXiwjAXZMht1wnSy3moKdg4MmoVleaSMkXB3PCqQrHgb8yB4FluoVKhC9ZtTkRVzaLCi6irXpRA6H8WCLIqW1ovxQ3q0m8HBaIsKwOhnBALhPyW5T+1XaJseT3PSg+avg8F58SVU5PVXQl1/kYBxKpKBRlzJyeipmZR40VUVS+wAIIjgUsGl01wVhHDWFovZVBOR8zMDlOW8ka4Db2FSegwHBdGA1Np21V5w2tpkVc/SU9rhO0/Z5kWmbkBfkONvwEgiENxTaWCY63cxYmoqllUeBF11QsHW3VqOiO2iS7WqohRsDtqpUTldOCWZF4W4nWhZZYTonQYxIXRerXtWdGkhboI9dCg6WkkWKvuuYG906lvbxC5Y+7iRKyVPudFXFe9WL/VjQ+1pX4+uke2Wtu1HP/FMnm7l2kwB+I+TsR66SUv4roV9wJxqy3fwapYf8URV+36bdv2rml76bcBsQGxAbEBsQGxAfHltBsQs7P3lfFIfRsQGxAbEI+n/Z8C4vEO+rrhvwb372//X7HDz35tQNxtRzT/tilwQlxLfusr/iH/wpvI9m6T26s2tjUFi5j4EmVj3FrdsUwmLmnT9iKFpss8jllvrfZm/o7oKJ/2lz3aFhevHyb3UC46xSEZqooWbqlgkc6FtdEBJ0m4iqKa7FdTxIWi8YztEeaXgV8pcR0Ib4KEzNMpjyMUQHjTpjA7n4Eg70tyoJme9AYUUacviq4C8cCI+CIQ/gYQKwHAB+FLVIColdgWHezxM94BhPtlIFYubdooIgzFeMg8DA6Jwd/FG3bBNQWLUu2CUjijpaHxKMB+uEzHwYLQ8+ztlj3EchFNDFeeIy9iK0CUFnnVzziDdWGUPIZaiXFgizkNGBVgWiC+S47WQkdyOrhyLOtiCJJjzO4krkMYIs6pRRqPA/08ueAkaot3rWpwqHJC5mH49Bm4ZResK1jM1S56+WBV+RkKBFydnyGqNkXMbs7sMs0p8wKQmB6DTk2lRV70M+DG7YNR8hiqJQ7EC+GYEWy5jjN8gp+jtdCRnM6xjn4wrIshSI7kTtYRASaLc2oRItQQkL2EyeolmIN3vfn83Y8mmIRPwgiZnbGuYDFXu9DOzfwMBQJKFwqEOV/s5ZRuce6Y1ojSIsv2wH7KM6yUyCmP6XiiGZhuRbQVmy64rMGASC/IkVVH0tQUtDdgkY4I0Io+Q46q4VPEFtBtKkDwGqHsjFUFi7naRbrKEz9D1wgoXSgQ+TyH0xktEDHNxuW5PCJmFg3KFUFMoKzk4aslxrN00KDHk7qGvb5d6khJZ4GILgMRdV0YMxDaG7BI1wgLBE2JGvPFjXMgNPhSz0CEpVBPRcFioXZRTii6YGJYMxBhbWrCjB6DnUkM0yPWpqbM9mCWSeD4XSlPtURmyiENuBk5xolMTUGlgHI6A4ScpdgvCTphrmQtk4ipiS0yd02BYsb4JIviEx2HaJpMmFAeBuKQkF2WXXBFwWKmdiFTk/AzeImbjDiK0gXilNjzNjXHcpGUaSIx6hfH8TgelxZF5Yow38QoeUh0mHmJ58MYXOJxuAxEK23Xltt0WKyZZCRnWfRI+B7MbZlqyVomiP4Ci8xzBJ2Jx6TGcUwRW46jH36zAoG/++nWsh6yksX8Vm9+pswZ7y6xlv5Xt7jaqroKxx/e/uQrjvhtJX4fODUg/or+xvau6Unew25AbEBsQGxAbEB8U4kbEBsQv9OQ7065AfGNnfQrKTcgvrGTfiXlBsSDXXLNgN9l3J9q9N9o2wbEnwECzjoXEYcajAXPwecpqrp9RWVT1bYjfVLYTP/x1olBu9My5f6dzjVd0+HsfpFi96alpDyVVNhe3z7eTw3K+/igv69v6+3t7E6zemY1L5WfSGTv1npYyG2begHnHqgLWvL8wSo+Pg1wskcH31AGIqeyrw+tosVkQocKCQ/qFDKcOzZZQn9fO2rI7rQ75bP6V6xurMENn92tdU+zcwQElffxQX93pvW74l8tUfaanemlbtFzuwoQVL6TtpV2w0JuW5NLTmVU6mrmQBycj1kxn3kFSUE/f0SbU5VAOAvECXVQI2Bs15SXRAbi9f31HWeXXdy4x4DoOpT38UF/d4tymhsjog5EbURQ+TeB6Fy3W2vSKhCgC8SDhH3HV9aH8TAqEKKZMUiq3oqbH1jR4sB8BwJiqv60f+tocjhRp1A37d+m/9z+7XX698cbTRt5RHTNj+no1JHN6/veTb9/Ovr71vBfSu8kJ5pJx7qOS+QzdJ6AaKZa7Yj4Ialeqeaf+FdL55Idl/xzz2WobShfUiCvHFVrUAsBQW3bcXq1ywBxcmzDXmqiemt1vUq57zSVkua9Zycivi+GukUvGhbOOPQk1VmDX0FtQAIRkLNvAoJGwjSJ0oXe6Yig/+goTeMvPOfsO10juumI47N0Db00dGU304nXt4bzvVBBnBPNnNJNaVAm7dH5V+6CrslrBEqiNGxP5/hflD5ZwDVyyS87lDEd0rro35cm5ZWjL2INaskjAm2DXXKpdHQ50n9UT9dovbW6Gm6ra/5pZG6hLnZBnd5CtkpAtHDlSyqhYaWv+MVrPS3jGBGNAiEjgq70PV0705n9O3eq23V6Z0GT0/6dgTgBQhqwr+8N/93zxIacAgSV0e35HLqNju24/HzHgpIoDYDYv+sFguZLjTyWqA463+GIXDiTXU7yNLCS+n9Kme6MMCISEMlCalvDebm9pt55XXsnrXw3QEzduAqEE94mUq0BEWWN6AQIs0a8ORq+bwzRDIg0Xl5wVe9PBETHJk6Z33gN4ZwKBDfrjc6VQLi3Eojm1HHOqfknvbqp9K5BaQqEe+PrfAZE5ywQnXsRINybAjEbEclCalvHNnF7Tb3zusg6/AYQETemsQACEUH8bGpi3ZQoQjeqapFoXbhrOgGIpnGLqanpXvjEHIgJhP1PaTYvYTxoXZ6AmgZAcfoXOzVlILDsF0BQo6nmCWb+l0unyRHThE4X03ry0pgS9T/Jw0enNI3WWgUiWUhtm5YIAPHSpXprdTV8UXQvDATRQI4cmQQLsCpxtMQtGJQ3Qekk1QDmgupaePARzu2I54gTLVw/d2/6HIHFmpfb6fhk2NsciP3PaQGfJoPXN0pLQOz5WuGFmYYw53zlZZ7KpsVXF2szNZkRQSXJQso1kz1a+o+pbCqNS/5JZ6dpZTqvde25y3NeHO3kzD6NCACOtsEuWGiBwG0CctXq2r1xK3+eGnd9WxdBq2+PPFmXDzx6pXW3THrIgnmZ9dJvP8jN03y8P/ZkbSxwf2T7+isOvdL2b99pwbzMeumPA/HoK45c79MD8Xss+FMt/Ts2bEBsQPxaB21AbEBsQGxAbEBsQGxAbED8URs2IP5E2542Es7/2rYB8Ze3bgnE4rVIk48sGAnqMFnzK6/6m2tnml01r3X7d0Xtu+vlrdZvWiQMjfJ0tb3ZDV7lZeQylqyTJROF3kXvbwBxmtdmgZgzEkrOQrOwft4RWm5n+B0pZVMyL64BsbsBRFNhiBhuR5ePWCAa809zBYgaBySXuav0wqIfmpltjwLhfhsQzgDx6yOiqdbfLFpUBaKrAtGlf6sckAeByLadGuIUEOeBGAUfH/ufP5hlABbCq3AOfryRj5bS1dkPO+E/aK6So0AMB5ynenDu9U1zlGwGYl2ow2VP/ua3XWEjnSMrhFXhfkjpP3J5P7H3wzAxdiv8DNqQDi3MrZrsPCnrBK6od7T1hDTTv4te4OOYhFKbhbvyTvYrEHBsTefeyTGEdFMj2RV54r/N5G9losILeA7ismyYdcD8iq7OftgJ/4E5C+yXthwFcRhSDcwrQLlOcpRsBvivXWJggD6WbYQz/iU5M1+kdOVZIA9b0bjExNit8DNoQzuwp7m6VGpD/r4XsuefbG/ys5e90DTZwZXajDKZy6FAMBcFtIQ3pw7ZfzjxKzMn9o6BYNbCThgH4FR0MkLRkUtGws74pil9yVEAi4PL6fi643KJ44CSLJthh2PKouiILmZt5Jo74lOoe59SZJ4F8sCKxN6A57rCz0C37FMLO4dcYIDsnV5U+/fX938ExL3QG5a90Bj6pVglHniGJgMx1UEpnPBT+DcaOV1tdI3S1HRqmLXgTri2tVJky0CUjIQ8Iqg7uE2Wo8AsDuZmnDodLU47rmQz7FyXStlx+l1hI5dEVrylDuUl+yUBsQeTRLpgDkTJz8CIwHXLdDjJxQwSbqN04pTvH8nPrBLXVXqhccryoNxcp5THF4oBAvbjEkffuJ8/uaAOxhAVpGG2gTuBcdCImcWIWDAS0LAfbzoFlRyFPEyJW+GclPuS+R2ZzSBD/gUTD6XfFTYWU5PLU5PyLHKXN5m90exW+BlEdNF2yNTktFUnLpXb8kL2/JOOkrWnRS+AQIMyOzsdFyNCzr6gfmWUEMGu2zNvaM+MgmlE8L/EgwHjYPeG8wCCeBg1RgKWN2HxvPHdseEo6ILUCNtCyv25F7aFZTO8vvFi/RMsCj5b2Mgl/cSiS/O3LtbKs9glvkXjwN6wRxf8DEwUxDppsIw20qr924lL3WGxnuyhdnTMUiFrT4teoLlkL88F+7Tko/94emOeh5z9CR5KI6UsvOnTiGj0Hm3N0a38i+6OI9fyP3ru+mZuaJvHcnV3lXo7xe6uPHfSB2hE3GNgvcg/xFG4Yc2jTJDvAuJbt+1d05NsGxAbENu2AbEBsW0bEBsQ27YBsQGxbX8GsueH77kt/CXrNiD+inX2jcqeXSMsdCDnqCCS8advTvUTryhRQ1SJOGgsi/oW1s/n6CNW1Rh1zNVebYoYjoP/P3zp9/FBn2b6obRR67a/4vxYUVvt99zKa1tc/goaZYO33UJ5opkBkc/ic24WOnCquoFI5tpIxMeRQBcqITFAZEX2HFT5U6iNoOeXPZCjkIRZF2YgvOrVhxxeg3XpI759nZo6UOSSx4DwItGOA9HYExd2+sV+uAJEmAHRrnX8Egi3BOKEdLsMxNTwBRCSi/RuFsMg/w16fh2I5bno5lFPbOwW1k5g9RBSrT9eDoWN9wDhqkC4u4BwDwFxkMnm595BtSKxH95fE+eCP2/tiC9huAikeuGy6gZpenA0Bw4kcEzxTXI0ErriEUMEsVkoFsuRI6NA5h0jgvaQ6iJC7ohcAiH3wNFcsnw8lSQaIqIPEiRSCQlZHBIQBx4R3knECbbxMGoZLEzv9NdBjnEt9OGzlJxjrbQs9q6S8aJD4rxLbZMWqmIJxV9hkXpJq7FZxIKzmZqoc+GqfxHnHk85yrkgV94J6hWJi3AioYOsukGaHj6F2yBxgl5k/VM0Eo5zgE/kSf2DzvPUhAABPdYIjklBHRg+k/KBl+gLFHLHpzALTmKX9CEHH2Hlec7PY/JMQNAKEc9YIxCaQWwM+sE+h2pgi+kXxWKgX1qq6ixoNBX+wJ/DH+CT/4CP/UUkoA8uRZ9RxZKB9wekHXJsFrST6qwAAV8viALKuWBOglPRBDsisuoGz/IupFm/veSIIzm8TOQJSLUNAATFZUEUEix5ATFKLp+hlcFMaRBFBfFbWp7ophpMABHvEIYgyC/upp51EvgKIAu1TrXxeIkScINrvijE+NW6ORCIpsKSFxJXouWIK6xDYuJBlBZFuaQ++fIoY7RwK+xivT8BiP1JnO4iS9J1sja8YURkLgLWiKy6ASA0lokGmomuBKIPUWbXDAR3RZbnGjm4CJvvRgWC9EAoigrit2g8lwSExEVyowKBCCfcIQAiZCCMjc4CEWfBSxDrpAQiCtQU0UKiwxCwQ3TliEijaZwDUcZoiZKWgWBWAYvaKP8BVIFGeBPCOuh2DtpIBoguq25QS7yExeBh3/cmWIz2Ms3nWe0Dd0EIiyTTA0dAwVT1GRAEBrFOAGxweWrCMQIXUaygDxJkSkixW9JdE1SpjI2qJTKVzvX0KTJMH/PUFFR5RKOpaDAfqSEwEGlqOowGCLGU4q8gCktIYwnTJ9fTS6A2sApeE7MBHAXiKUNLAudJQQmMJHAR8BxhVTdIWIsayTemI5YzXaxz4EREO8HKMc3nHMfEy5IlSzPHMmnPtLi1Dss3oqQcR4nfMmq0FVZfGx3WBqntyPn9iOeIdtDnCEQuyTYGWfCnf89czzkypPQX2iIoVZVHNJoKbOA4WdIaGwYtOo0toxZRHBUGJFJajdFCpaGd038cdwY0gStKGF31FvZrT9bRPbbdl/6eCCXXLXzYrvBdJeHGN1l3nxLGnwfivu2eCCXf+4pjvcavtXB71/RfsG4DYgPiTzd1A2IDYgNiA2IDYgNiA2IDYgPiGa37G8bWZVTuF1e5v8wNiA2I+yz2Th3q9DJ2zsWgOCmVnHEZtBgb3lMuM5TH1FUeuM7jJVuQDIv2KLsxFo7/IdiapUCOY73M7U274BuPhSWu7mK+usXlr7A8Z/kr4oU05ziG+Mg2tgaItuRiIFtytdvYKdaVHlLFHKlceBtluyy7wvIymL8QKkCM9ujkzWbHUGlZHQguZYyuzB1czh1NV/lsTaihofbeTwQQJ6nJlfkr+BVNH8CLwTYenDIbiEtQcDHEi1y5ZK4DUbnAwqxhGYgDu5TUAgXCR3t0ur6jj3PL1oGgyC9l7ry/BMLdBMI9BEQ+gt3MX8EvazEDwTZiamJmA/uJDM9BsoVWGA/gXDAjI3EaWJwdvIeRcpCPCy4XYjmAEzGEkl2ReRmkr0/1ZQsUiMk6c9ST5z3OLSt5EZcRziJMRsQhsbmjy7mj4VuwJZK35JDktj3GyBgo3kbB46BxgJwYEZLaoScBhB+cT/9xp0TDc5AR4cVNCEelMhTAaYDrj49OHrcQyZSps4O6RynVULAryDenvAwAQfO3WqBAxGiPwuc5t+yz4EV8Bjg4PefuQ5n7MObc06qXvMu49D6DukkzhyS37TFGxhDQOzkXFY6csJtSU61IM/UHLjzvlNnAnTJknoOOCC+Mh+wx9onTIMYy98Eh0Ac4HDIsKf1nmLErEi9DRkTIFmQgyqMAorSs5EVwZwZapGu5e8PfiC4xQNgSAgI8EsshcczOABPjEUbGZ1AANJczOfE7cK3i6XYx3zWB2UCdEpzhOeQRgXBoCQheI/hWAHwGZSOE6BLfoQaEsCsML0OBUAsUCILUHuW4eDPLSl4EgED6ZW7L3zAMECExfMp+wSGhtgkT4xFGRgkEmChLIGKoAgFmA3VKdIbnkO6agjOxUwCEkk+YzxBlaPNtJqXu6Sg4GMRimLMrMi+D6wzWApmioj3qZKEtLSt5EXZqWubO+1xUr1wQzwxFMzUlDklu2yOMDDqnqfVfdD5yYroOXGsCgqkNPHuD2UBcArrulOegzxHgRbTgXCAoWlROA463IxgQiM/CvAg66hA8LcaSXZF5GYHrbI0FLfEuKDJLtEctENmykhfBrA9eXmu58z6T04QBAkuOI/KWHJLctkcYGTyhjb7gceD5CjlxAxM41pnwIB09R7jF892jDzXhngee9W3+FFwu2l+z67uerGNwf2j7BiDuYVA83mnPAcSvtu2PAvG9Jmwv/TYgNiA2IDYgNiA2IDYgNiA2IDYgNiA2IL4ExGbkf93KDYi7NsN4WE9TenrjIkFb+bWuaxEXv+OVJtLL9SW7BNv0JnVcKhrkSv/vEmv1hut11nRBrmuJ1Kyc1xxXewG/DF9i3pQla6PstpzDp9TeSD9YIKyQwxoQNk1ITSQWRlyxMaaPbueIJ5bEChDhChClbgjKnR8r+qdiZVgBwi+sERstX6J2TV0DwhVAlODMgchnwx0jwgLBLIy6isZvAmKuGxJcTUskmLqWVoZHR4TlS6i6xDprA+eUv9Dyq+LLSF68o3yM6x1ysRoG3C9nKGnQJ7/kOCrLsr/BbADb4yiWCAsjWBtRPzyEjoE4jsocoXNgiKCLkoLGCBeVam4kHorR00Be5V20yT52XolWyLJ/pt+DWFnokcxrtm2kI6pMAn4LqYYMmeGAr+f7VdYG+ArKX4C7b3I0RpFnGKF2QbmSPkxyi8KF72Zl0e/jaJkNxAgJ4ljtA1gY7VjYGNWJeRymNW7Qz9d5FMon7gqE1sVH6ZhobiQeitHTQF7lXYCbwsQOskU8+Mv+iUG5IqUeybxm20aqQJVJvOroZB6HDHVmQ6ywNtgo5S94XpozEHDHe3bWJxEGztmKgoWsEaas/FuZDcQIAZsiiD83RquiMZUecY7rGzA1UR0AooVehwCh5dPUcnRZc0OtsHoayKs3HdQKsgRqIWYqnPVP65xwRUo9knnNto0tt6lNlIwpJd0zzRgODH6dtSGdN6iiBrEUlkBEl4CAqIIjxkY7ZCCiEyd6sOXCyB7CCmyBaAbMWBiB6+eyZS7mv+CIRCJcMkHTKYFLVUDouowhA8Fzs9HTkLzYpl+90ALyfF/tn9EJV2ShRzKrObfRwYc9plK4DwxzQyVB1lgb0nkxK2okIMArlakJUxd0MtLU5KCuoWUdxiBDu2Q2TDM2jWLphAoLIwQm8XDZ/J+WGUKemrzwWrUuPhqz5kYwIiiqp6FTk/YEWyJqIW61f2LwNaZJmNds22impjR9JiB0sebhtsLaYAbCqPwFUAqHQJwEL3TK45i1KqCToUoanM9lVYsoolqpXGE2RNJ+YgZEBsKyMECUBMmWh/0FehnCETknjY0BQKCuVtqimhuWh6J6GsgrUxMra7Tn4KAWYo7O+geLNVtZ6JHMa7ZtbFmtBHXrsv5bN8uEiOGWQGHt+Poz6y2Whb+jrhvW35nOX7PSPcVmkZ6j/qtAPHIVfa077s/19ED8+ra99Nua+J+zcgNiA2KzcgNiA2KzcgNiA2Kzcmviv8PKrRu3Jv4brcxKGXMHueU7WD2NtpJq9ksiIwRiiQxr7IZlabH6O96wVI/HLwIBr0yNnuDXeivMLI9lj7XVFtU2qweSZR7ijP1i3OxWT8OXBoZS0cICQR40G8kkVKkHOUsNiEwBKC1dtiC6cguzjpyramD/48NqkiBfTlGWl+qacVPEDZGAqLE1rD0plsxChgKZwhUgTOprMUnyb7AboltnN8wbG26MiNLSpQFLINyNIwrEwVmeyFUg3NeAWNZeAAHh8YPoT+CT08w+sHwHq6fh5XdmRbTiWjlknsMZ7IbDaDkYNfZEjkZyFOZEjeewtFRT6nHdrzFR2jHHSIEyRlbZ+PiARETmiRSaHGNmjhzHJO9+VhcVSlK+CH86HDWezLwVxh7sSeQZYnFw1wdRCFBvroj1F3wHG63EJwefsiLEBdiHzHPowW7oQxHJpMKeyLEXcLbOc1haipQuHc/xG5ZMFBPVQTQ1Mi3i4yNrd8DhW2hyaAgE4aYcR/3An33UqSTDWYkI2sApi1ZYZgzcznC4pjgkwS2ASOoSNpaI6mn4xI9QVoQXtYqQeQ5B7kdCrLAb6tFIcLbOc1haijztxamGhlM1hCUTpXUWiE8TM4WltoLlspSaHIjgQpaK0gA7aFGzTzwUE9kigHWCVpatsMwYKHhIlJiLxCGpAZEUNkwsEdXT8IZ7oAVDrQKEgF7ULTIQc3ZDPRpJSDoZFZ7DWAcCNoLHYYGYMVHGORBZZUOBUBtLTQ5EcCFLFYjoNJqLTzwUN8yBkFaaVvQLIGIKDJKbAfGSmCKIJHUJG0tE9TR8vgL6WE5NFOWKeXgUqQSB0UKF3VCPRqIhmGo8h6WlaoEeL4PTlEyUHOdElTGyXsa0WAdfcFkKTY6gzBGVBdKpCZwMZbrExdQ0b8VhzPZ4UfoQaHpZ6oKDwBXdnfsUCQXqEjaWiOppEFdDWBDCihBpqTOzPMBdOLcas7HCbqhHI8HZOs9haanyMvR4yllhorRjjpECZYyssvHxQctzwRMpNDl0WQY3xSzWZ+VxOKb8JCZIiidTtmIab8keZkXK8RxR5pu3mKjFjzyzRve3tj/1ZP3HWxhTlRsQz9HC7V3T1sT/npUbEBsQm5UbEBsQm5UbEBsQm5VbE/8dVj6Pgf8maZO6tMsfab7/4rnvt+RfDgR8p8svcMjjvKYvEUW+nLehVmQszsXieLzTMmE4BLKk0AyJpNrM36gFPxBPZNofWKt+UI3jI7tK+ZXzdOz/LjnAublMxlzP+iUTbx8PrlQ2j14YLGz1jS2WV624d5J4ApzqNX0JlVpfGp/c4WEl+M2DQPgERHSFZsiQhNYDKcLzB+oRn7+q98Gnj2mhXBArsi9BKAC+CoTSNR4FYtobJZZLiG5dWkOFLipAzEdEnYFxCwj3e4CwmiEGCP5qX9wzg/FbQ3K+L2BZs/ebgeD4LxgZdSDUf69AeOhPYPogH1nLgedVZ4PYDa7CwMjsA53QwK3IEUjI0XHQj4OdpjiwjLqqecyVPSSH7CG3xF45syVGM8S7AojBAnEogDB7Uzr65NdqeZCTZ7Jb/Is2akqOp5KZJQfhZFCvHbJWxxmOHQPEEKKMiIH6STU+srIHi9HL9MlHBwgzCBDsyIsa6wPshiLGijAVAnM31BmIIQ4/sEZOIEcif7fPFw2RUEjXoufAAvSL1DygwKH8CErXB9XZQHrpvikX8yyMHgfpfhy5DFElYMbEceQABGf8BhDYE2f+sNDyGImp4V1ui0ZNIUEHdSQrsySI4xOhQdCSnkuj9logoqwRbuB+iuquVWUP8Ap5RHA5HnFDEhAOQCgrg53qw5KBwTFTEk8Bw+14kTgjl+S/pvgmQxrGAXyPln8dnVHjiOBEIAd0NjS9l3953jVckOlKTw78I2tlHEQFAXGOpnFBVlxwSShYaKfV8qCWZCBs1JTpKhYNjswsCcLJEI0CbVM0jBUncTgkdJWEKRGND/7EPyl7QHFEo7swO0HXCPYhDwoE2A1FjJXE59Ch7yRmCM2jMiJcBiI6C4RMH2AFMXtDmD2DmIJILKLzocwo5FIgEs9ioBJ02onQB9FIQzg65MU6rxGYkbOWB6gIXsawjZqisWIsswRAcCoBgi+twTBWBAiE2MlAQOMjOqvsIYojIQMRMq3Ka+AZmZoQLGbOwJj+7WdTkzRAY4mIGErIDFXEO4Eux/Q3ZjUO8CM0/gp0NqDIQbI74JKwJUaPgwb/1ICzAYIOOGVnRNYHMcoXAgRqyVoeBAeAYGUPEzUlJg2OzCwJeZKR2wNuj1MyXQov5TSYm8ig5KlJSgX0rDgiSiUOuhZYxMAo0Fgfwm64LBkYvuApAIiW45JoBBI0yy7WxPWg/xF9xapxgB+h8VdEZ4MVOSiSF7gkZInV46D7JH2OwMQx/R2yrEnLq0RS9+ByufNdqeVBcAy00kA1ZMyxT6hetCYzS4JTy/GvtkTppRoPxgLBtZ11sVZlD04JxRH0yFDeJs5uYr/2PPt/l/z3zgcZVxday7asWPILYS3nt+vzm/f746lEeRaZx4O51nt+wVJfbPb5+ne/WIg3zmVbfocl5ZuE+XuF+5U+NOUTvGv6/dv20m8DYgPiOS3ZgNiA2IDYgHhCSzYgNiA2IDYgntCS/xAQfz+Eznd3+b/M/N9b6n8WiOhEo2L0V1KY7UJvP/OGPaTJkReOUuptboNV/qDS8U+7SGOVMfRFnagyz+psx1+k9oQlEHg7zf4q+PvITbxIb1sT768tGCDgGKrbgyKztkSMdh97sag8MLlGnTwrrXTy/Wrx9tPP/s1prDJG+vZVvCKZ7EN1xqt13jrq5LV5CQT8NQpEUWtRUEi6EbHaXlfjYWRmAQetX2UhRNNN6mw04yXUgUCp9fgtNixOmJW+BGKpjNEHPRaDSSt11oHw7r6jGQi/BEK04q8A4W4C0Q5WFaNkFrAb3K2xEHKEENojn1mOGKI8B+FLOFWyIB8ZlUqds1YqFDQgBn+Qo26Avobq6Wsa+7HtdDzocXExOWGCoCXhektEP8SwLGyslYM5Z4Egm9hfNagDSsXulbGiH0tnrZG52ge72HyhilEyC8h80EFqLIQcIYRdjdOqkCOGON7T2CvHISlZjCgVQNRLhcsW3AvV8WiZN9IXESY8z8lGGWNynerxHNslBq2zvdoSYZfEfLSMtcKKI3LOrhG90RnRWp1wT2LIGiL6sXyfnKaq9nEUb2GhilEyC8jn1K+yEGyEEMdeZLsfY469Ep0qWYA/KFPZSqlIF1g9I8UcG8AByT5oEaQwyhj46l/0EbQzhTLAzIqrLWG/+SVzN+axVnAe5+yIUG4J3LsJCOa5sAs3MUF00gT3xap9BNYTKWOKFMyCbP4aCyFFCFFtibRvY68YJYvBAlEvNXXyCNe8li4jop8DocoY9rgBotc611vSBw0pkrkbs35JQg7RLYHQcCIWiERqSEwQBYJLL9Q+AoJ4FDFFDLMAbvB1FkKOEKIdq/vMlYg59goDASULca4Ht8ptEAWNIHSDVHqPNaI9O5MmK2MkFkgogCB5iuFWS8AmId5InFuSYq3wWT7HKlTinc7ckqEEgrkn0fI/AETIU2NW+wisxTGUMUUMs4CJIesshBwhhJfT6Tki7WONSLFX6F4bShZHh1K5vmqpvOCNUP1wUNTg0idQpoaCbZHT5MU6sUD4eEy3HS5onestAZskio6GN4t17hc+i3Mu8zUytwRAoFYnOiYSb0UWa77jSkokVu2Dery4TSuZF9eUNMrbu/nNXnTr2/2lZn6EEsMweVor8++1OsFRul5nvslOj4SLfolFC0q+xm/f/vYrjpJJEWfMjkfKuF5nTH/XS4+VFmzvmv5ynRsQGxDPX+oGxJOUugHxJKVuQDxJqRsQT1LqBsSTlLoB8SSl/qeB+HsNrzf136Ii80R2bkA8vMUZMyLKn4jdsDhnXoe15ujclBDzF3Z4iUY8Cnw5l/Iap3PJATFfaI7K+ThyqbQPjzdrJ8c6W4SPwRU85iOIfO0SJ6TYqjEwjipqMbpKfyzzsx0Xa3W5la2M1ro4c8hzUUNyjiSHe6gAUQtVozFKIE4SEmRtEn2PCZqCJTDYVmYggtHfoFJ5f5R3/uwXzDbEoresHyBfSCHRCkJhtQvzX7AKX3WrZkeNEGDPKLkA4uV+kTLGsj9NH90Cwj0IBAgtHEnFAHGQD9QtEO4uIKLR30B8FihdJCCqbBEvKhzQoy+AcDUg7CW3BGLWDfXBkTgxXqyuAjHrT9NHqqkxgAHBXAOf2Qbn1jAecrQT8CCUU0FMByhhkNvkOCKSigUCfjC6trUE5licjTz5AI4DWBMFEEl/AxGLoHSRvqWuskXYccUtii7zJaK4Z6iuWdQUsoWPQkfEXGqDk5gZeo4mnVp+nvoGXDBkJfM7hHFCVlK7yWlm+5N1C8D0UGbEIKyGXqIsaHwQiQCSvLrBRDNJnAp2QB5JxZ4dlT6tERaIHiwCUY+B+1P5FzoilDWRgWAxFHD0RiqV9qMCMQFUZ4tw6czpoK5PfAlW73Ap5EiOmoKP4yUiRG+ucxaMAV+kPLfMr4OuFat7SCvB0z396VPECduf4lKGJ5cQ/mSOQnQ54AvUMryDegbOQScDDTGcitiKEgaxOIJMLfMRwboeIpeAcGOqcWGAENZEBoKbE3REOGGVoNOO6ilfsEW4T3q2NmS+hIbi0Ks88yiU8tDOiHIYEV7YLvZcPb+OiGx1EC5ISKE/bH8KEE6AoLlTlTQQlCKkeCmkydGnczLjjgkIcCqIG9GjjGtAKHMBK1OW+imBiOUakZrkJCwIlC4wItywwhaRNSSYWjMQy6gpqSPHGWNRF2tzDlNTLT/s8oXVWKUsEEV/ZiB0gjmfwVEQIJRtwFMTMRhUkSI4Z/QtaGbE1ES/qAy6w9BIKrps+tztKb5JnpqEnkVABNF/KXgSQeVMND4LJLRwAQx1toghiBm+hAWi5FGUU1M7JK+1AWIxNc3y27umbDVi0CQg4rw/yXDuE9XUmKYX5igwEDGxDRAzxOk5LHaiWRHAqQAZ0UFa58wxSiSSCi9wzHcQhb0LPUsYzb0zVCw4BTWeuRVzngT4F3iOAP8iPUcEeo6osUUyH8TyJah2vrYvcx4F2yJLOBghzB2R54ijtPpoIp8s8zvzHJGtRgwaijTDY2aY96cwQ0Z3zxbXz4Tlr5I14euE3uIefMkCqfMk7n9ivVVnuYELUobwe6SEue7ILTuj++K2njEjWWK6veL4LUB8vyn/20D8i0zZgNiA2IDYgNiA2IDYgNiA2IDYgNiA+N8D4rZ5/7ZOvd/ef1mgnA2Ilc28KyKF/LzHX9ZnaWmrfeEucjiAk3Esi2THN4veK6FgVG2PY8HPuMXHKPYqDIzjPCBGPjPYvVj5Ikg/5XWVQBHBlXwSUd6QXmhL62IWiCCuyVRTqa6w4MO0ZsdUHC2dZAjGye3ZS5z2jfaFL7keYRbiJoh3K3r5vhpuGvUtZH7GTT6GKy6L5TZz8d/FmjBd4l3VfrFageAX2SGzOryzlAq0VSBjx5orRVgWfJgsUZHKKJU1GAi3CoQp9hEgZtyGwfIzbvEx7gXiIdZE0e57gIBDM0O+DsSRBQP8yohYAkFUI3dJn7iq8gRp1EM7AjFScL0Mwr9In9OS0YPGFPEQNXdgLbTmKLy4+NaZu9tSToYcWuMaHyO6Q2KQUHkXqQN6F+pZBofiXtaEbXfr1P5ZycMMiB72tgJEy5/ywjpt6/ECrgl9IY/+IFuWfBjkFy7M1I/iZ4ZnK0h0EC/aEZ/BCSMCLI8+ZO0LGKRxUhBegHzZyaUqRxUIL9yGQ2Jk0F4C4iofg84EYZDQzPqpH5KrgogE5+F5+k7WhLZ7WkXUsTrZOyt5KPkk/BW6U1aHF8crrMuBQsA1ac+YFhGFZcmHkfA84MIM0/5nkMUmqPJElO/+gzA7AMQnRw8ptS9oOlGNDOTD+ZjkIFqnQCgjo0+MDJUTuYOPAVpBYmCQzcQdbC/gfHgNsjbcz5rQdkfWD1H7ZyUPJZ/E5TUCcCD2i8R50cg04Jq4EDVAVqxZokDocboGZOpS5YkYwOIIwuzw0vgkZpC0L3yKkwL5EXQEzgfsyxUkwdRyXJMBe0kw4TofwwCBiyLSKBgRzUTXCNzR3Mua0HZb+RQ3lCXP1ogFENFlIHTSpakJ6jTJnlizJI+IKecZQMyUJ2gmTECwtg91MDE0lE2qU5NPcVJkahrSeda6gEYSgOA1omBkQIDlLj7GEgiNZyL0Hyu4dR9rIrXbgUcC+8uSYXXmk6j4VgICdDsFIuhizUDkkHGxxodRIECYIzjCXHmiPVM5tDedw6wYwdDQgGFpsb5onBREPjm6dJ75GC62opcRmP9WMjKYg+fu4WMcIV6UGBhDyAsqR3Axt7n3sia03ZGZKrB/mqyLksXqrLsxiPUjuB1Uk3OwTtvqhGtCAc2ExcH9NufDID+eoCg+TbqNe3R7NEf84pP1ddvui3sSv1C23kw/rrtRS6k8j3jlyB/bfscrjvv4QPEXSv7eVxzxW2x7QiD+nNXbS78NiA2IDYgNiA2IDYgNiA2IDYgNiA2I/2Eg/jQo9Tr+bjc9HUgbENUtfvFcuU1vOomVQa+v+ZUW3nxOrtbh/y75fWPk18YFJ+KCvWXsFvLGgUOyLItdnOyqpDqPo9h54XexUVkjWsfRAMFWjt7N47OUG79zvsBpoX2AvRDn+9iZFRD0bGtSk/eO6hdfRby7s8NCaWNeo2VMcCdHCW4hvgB6/e/Tt6T4stQLJyL7r4OLRewW1IOzRF2olOVUdzsyPSGmfKpMYHkXzsR8cEG5JL7Sf5lQQV4OC0S4Awgt0RsgUniSCIaHSw6mEGcCEzUg2ioQZde3JRDp810BQvx2yX9XABELIPKerccAsSzLAIGjvgDCl7wLC0RULkkVCHcNCH/HiAhpRNjOViBIWyc5jaLJkZkT+H2RT02jxDY5yL7EGxENCnlbP2ROwiNAgExD+veZWkM+MTiJmLHhEH/lMPq1shZAgFciIZ2ENaK8iyLu+pC5JJjcDhKTBR+yK7dCdDiEEYKUXEfRT05sJudOK3arNgdSk0/6IJ//8ogYlDThcEbE5BNzAi7vz6CdDMZBL/ucLvqsQcFpYuIkMAVgOA4lEK3ENkE4C2aACBBkGogErTAkDiL+07ODFAyOPviVsgAErxFDHhHshqcOEtYIBS1Q1kgGwo+HUV29KZhATDOscCu8Sl3EHiokOuK4T7SfnNgce0R/6SUdfQCvbCuOExNR+9QBU/2ZiSUlwxd8VD5CIGUJJSEK4yDoJHW8EBCt5EGaVnzUeUQgioh2nuOIIsVVLEBkToTuARaNvKJ//bWyIFMhR3kb5AZgUN6Fd/MR4Z3WpTwMtE17tXUatQKaHSL5cikm4cQCcWJzIlEoECFPct7FBATWD59Aov7liCqZOaEVwKJ4FsYBrjBeLVrCERoUEtPHjXZELKcmF6KbTSe6RhhOBPbgfI8yFjIQCKNULUuAUD0R4R4OLrFGUpOHEoiYmIohtU2mJtIVwXkZSwAiujkQ0azvOR7SGhCYpmpAoA9dZk44VayQiCiZcUD7epcDDQqQBzgfMxFkrwQiquxJcaeTgUicCLtHpQkQGiZnWC2rAEInGwAB1ghixgAI9T0HueIzDyNYlnWMSZHEp6lJ4sGoVFiamtC94H0cRgEiLKcmlA+NEnuHmBRDepeZEzw1nSF+pTFEoMmBu6NWlhXPUaKQBmoX0AWk54jIUU3k3l0YFJjf9d6f7qGDs5wIaFfoXsuRfY6Iv8JNo4t8WRYUAhWI41QueCVTDbBW6kCTj4aNgZaAjQEeBo4oDlwu6r0gNsphhK5H7niwQPTeknvwHHUVG1WbA6kRAwypAQT4MjgniiFGrGsRncQOPR2S9lbV3novb3L/73LtyLUn62VZ7krp+ddSz6Os4zobQ9sWr1Y/f+SM1d9qk5+VH7+qfznf1pQ3ont0+1Ugvl7HWldoi3AVf2W7bvc9kV3+wra9a9qA2IDYgNiA2IDYgLhm0wbEBsQGhHuubQNiA2ID4pkMWbsQ7r9Afq0lf70fNiBu5JZ3eJfsf83+WF/6Zosck0d2WB7T/ai/41G+cwOHoh3rn0x5UZtP+2P+i83qVzg11S3jpti0x3HW1pkdR/Eke/bquWb6j7dOuuvjnf7uJfeef+3fkRLnbm2UY/dm9wwQXQUI72pAtOZ3jryhx3DUXwcifdoODkUMftE1AsSQ31lqHJMQrOyCnB0MeM4t4qbY7+Niwa9Iwg7ZjoH3RnLzT786AEF4fHw0UzftONdOStvxr9cOQOSjeWvM35ynaZxJa4A4FanBNSiBUJ7BYQUIYTQYdYsaEFxuBBBeORR3AZFqMntWv6IAws2B0OgmfqbH4dzCDgIiet6bfp1wjTZTh90AonPdrnL914Cw+ytAHEQiHByIxNYYM2PBBHeC1LpEPclOkumowzFNdxAuBFIJt4fdMsyhAGkgqXZocHpIUXg9I77xkLgOiREh+hXMvYAVs7gpl7EVByXE2gv+SbajT/5soex8fEzddNq/dd3rmzt9fLy+U8ft3/Zv1H34NwFxcqed+/FGR/f070/He1NOR39x5v2NS5i6mva6hkrYv3187H/S+R9vO83Tp8+vMav2+Nw7ZMaCES8PEPrBJ9zZbciuzThjNQRVnGBny+hGMCuIwyBrRFLtUDetVy+1OOKPwg/0orCRGBGsXyHciwHQ27gp9Ncn6gFzRyz/JNtxTgwP1v6g4zQSuqbrpmu00xEx7fMk1PFkxJB0BJlMOXz2Zcrj6C/PQfS3aSj9Pw1G0esblYKSuubjY6ph+v3COXneiiFmYapB/K+GoWCnEfANCIjWLI3RAYgFq8F4vZQTYV33LateyOfiooUB3zPO+DQiWC0nrRGRxRVa5V5gtSjiptDfPCkd5vyTZEcmL+BCwohoFIg0IhIQ0yINIHYEEk9f05X+3gl4BMDre8MLMp19fd+/KxC011EqAuWdgaAjDMv+vVHuRZ5VhRjiakCAbxBwhafzACIMJg/GVQKC5UJksc5AMD9kFAEFkWIA9QtnvLBFAIQbMxCR0p0TryQBkeOmCBDgmgwl/yTbYcKqyYVAE04nQOQ1IgPRNRmIac8JbJxmf3L8m1eDNwKC0mcgKH33wmmnkvcnOjKNhpOMomimpgREb6cevnplPQiszrGYmgIDYVgNh9GW62SxhuteORSs4gEFkADighNFC5zhcXBWIJQdISwOVe5gvkYZ34QgBRBYdWIs+SfZjkzHBA0Rd00nANE0SyCa7iUDMS0R6OAXXNcdUlBGmpoahujFADGd2/9MUxMf+TGtRD/eupcpMS9luvResPjaxTrHqXKicuFn5+noUYKoJVaDa0VxglPJc0TrLIcCehfMaODFmq/P2GqklZEVLyBMBA2P0TAiIqltqF5GGd+E2CcD8TqmEtNibfgnxo4hRXx0+TniRMvqz92bPkdgscYSvP/5yr8sEPufdJaWYQJgWow55e6Nl+ef04XvKA8Wa9dQWl6s+QjNZtOE9zM/ZVRUKQoOQuU2rfZQFhfpy1LWniiZ91DhA8dguR9LZkgOMFjbYiJylZFf4m95su4ad3PTNNOI4F+dmz/RffMW7zbfbBUgbmltXD9fY53U2BTfA8T+7Xa/aBoaEX8EiPvN/zvb9tJvA2IDYgPimQzYgHgSAzYgnsSADYgnMWAD4kkM2IB4EgM2ILZt2/5tW9Mtj9j3JxX/rDjPc8auSFc7vnur+Xltnl3Vulrtc25FmYbejhqvcnOzvNlx2x/dsn8aVyu5Kf/tVnp4X22NSVaWszt1Ten0nrnEZ0DsVoCwxxvJoy723Q0gcgr2A8i+EC1mLv0KEKcSiGYFiOYqEE0BRDMvbxWIRnqgWZS4W14b14Ag5946EF8bEY0rgbg1IuYpmqIXrgFBHrHfCERXBaJzxdkHgHhlLsGPN/LOgnvQdXCD8IhwmY2wm3ET9uTQIOdGh2M/wGWY0uHI8jjtERDgLdCZ6a+j+uicpt6xqwUcB07n5Cjlc9in3MSaQJngVpDv6wfbtuc2wKeMFOBYWPu1xh9v8+P41UmnwQbtH7V8mrJPsN+xtaj1hxzds98tl0qp+CimpdQX0gfv1OfssGuaF7gpwD3odEJq0BxhI+wW3ARyHJJb8aWBi5C5DJQOR5bHp7+vbzsug+olDyGlxjmkfmlw5AVOR6Tgo1SKTE3ihlQ3JrgVVAqV/A/XDe/yTlK8cGprv9b40syPN2BbCBDgWKT+aaRuyd8QO+PFSa0v6CNldXC+TngdXD4GS+oL9L0DA4S4BI14jsA96DozITWZjbBbcBM6PkaN3jNBbscshp0eWRwn1kIjjIYm7XWdniO/L4CkPUpD/Ab1GUtX0zUuuQUI4Vbg7Os7gJiuYOYdkYUou+RWyGXxvjwufItOu22f+qdzjdTNPCWn1Jn9u9TaNFwWLm/0K/UBA2FuLKS9GASApjPZnHIPFkCAjbBbcBPYpf5GHT79lQ7v5BqvHncKRCfl8N4J514SEHswKcTQJRB8LbkMhJx9QcnUJVQmLxInthDjprBfRoRbHgffYnJdy4jAlUv9oxZ1XKZ7e9FubFArEwjehK8h9lMfAAind6Bv0hdSGl8mXZomdOgxEDxgdq5kI+wW3ITZ1OTy1NQ11eNNAqJp0rQ1dRqf+/GWCFhoHNX10jQzIGj/hXOnqQncClqxGodJgsrky/wkKXRqKlqDGufH0X0EBBFkwLFI/aN1NyfOz4yNF+Jx/JOOUv6T8Dm63EruY6HcaF80ZkTseAGVqQncg5/CRDiVbITdgpuAxRrkQSoFixN1CbEZKsdlsW64JCIg7t5e+Tgv+kJK3CVyYsP8hlc5SkyJKfUb1yuLNRZO4VY0KPkktEbMs6m0n8vWUI3cPcXxJlMseTpiRof0Dyyi8yfOv8Ni/RO1dswAYRuEz8H9QG09Neyv3stiDbvR9zy5/XTr285d2+4jpD+ymZvd5r4cpQ23ci0t7m605JZr/363/+7hHH9xy0bew4dYbo/n6n4xzb+iW7dt27Zt27Zt27Zt27Zt27Zt27btX7hZssh3EEf+7eSTv2b/BsQX7N8vGAW7tyrLYHXrZqU1ULSQc2SERhC5c5t/OXRBHBJRzhiQpvxWaO3LoePskzAJo8Fb1Lxa5mzD517Hykdlqu4xrzV/bZh/AQi1XnKEec5pf7d4y9o05ZvXXXL4pxQzIHYm7Y4/i1XneYOrQT+kXfR1qPX+QjsDcUjkW9SoH8svO02zm0/0Z+WFuATCa5kVIMKiBFubVQDR49788nlE5E+axfpFzgoQzl3t+CUQbgnECel2AEIiiNSASIFfsuHBFWe9qMfMmlI2pFJ2CQR+QKN/AUQ1rEo/A8ItgXDXgTBTk1rv1Powt1+BABticlm8N06dOHz8pzAnfiZexbtyPZBv+uWUi8Clnej7/axn4Ry+sob8OZQ6NI7JkcPd51/z2CR0jD9ITwoepHpAafABscZ7iRqJBSW5FOXFleXR19wa2D6K5gdiOOA3YsuIYgGrjMxLQDcjrVUA0eMQkZdfIijPEhO9T3aS9bOcGYiONSU69rw5ceXx8U78xN2LOPd4ymGeBPJ1J9aTYC6CAtEYPQtWXBrkw/MgMVOck1giPkUVwcfsZWwSxDWhNUIVPI4Dp+/RXI33EjmnhBeQcniuHsry+GN+VC+BD1zwKRiCBHDQMs9U1rwErABIaxVA9DiUFfBLQyxw/K+zxAIIsL7MadYI4kIQv4D5Bu/gPpRAwIuMFA0zLbA8v7LnGJ7XPCKynoWTwBV0LdPS1zqNVqFRSvKvZWwSp0E4zBqBDpTgNpfcqbQnyiAGIFseypdaWbmgvQCIFvvxeNF4MbHn325pEat7kBCGUQDR46QkopEuNOgI1KGwtsGCeU4XDBDgXIAEAu4Dd/RJKCwncedzCnA9eHvDiAAXIa8RWc8iA+EQLW7sVY8mzbnp1yI2SQpLk5Y7C0SvEmlJQ6QGRC5PRScyEFFGhOPoFIFjy4iGB1tVs4jUPaZzg1UA0eMeoy4D0ZdA6DgscwoQwi146WTiEXYGZD6cMCcSJYruq8D1kHwdsePyiMBdU9azcC4LCWFqOowhZBUPP/tlY5PkmA+xHBGi48EhNoYk5RK0JI0Dw8025cnU5NI5FifSUcZA5HgxiHUyK0EjyUjapACix0lJhNYAnZqoLMR2MZIz/SJnYJ0I4Rb8xAJNTAVhRzjQEZVJAYYCPWmA64HzxK2AIAjYFHiOsHoWiCAChQ0odZCuhsQSEbUML4t1GZuEF+sLytA1QpZcLK5O4r+MukRCGYTLkanOlofFGvodiMnSno8j4rWghjbFixGVkVkJbVqsfaEAosc5lkpAeoQ109guPtmJSDQ2pzxXrDnB0b1OF/PqeVcp4daTNU0nOepI1vDzi5tRrwp8K1t063ITsfJr/Uh5GxofKGt53N4OU1nXn6yv25K2TJ29lupxIB4x7ToQt7c7m5q25XN6/HJ++r29a3qSbQNiA2IDYgNiA2IDYgNiA2IDYgNiA2ID4o/a/92deV9t99f07wHnFy3dgPjDQBzH/7vANUP/+iGKRj3HVJjeGB6d/HYP8DDKdzM51vmIGnUvcx0MEHEt3kqxMbPjzrSzrb2dJHwhj1uJ/cIedz9kf/s8wouI26u7BRHSKQ6AuvcQYwTa8o7jMdRsrfIwVoAQF2XQvexiN0CM8zAfNWVeYXaYtOHunq2XHmaHwqy+m3lS++Y5Jm+EBDQRXkioXrRoSi9ARPF7sZ8VzANWzPbx4B7gYXDwAb8EoohwElwWh89A+FgHonxdDmaHTfsUQLg1ILSXbwJxKIA4MBCIMSLa8lG8zjd5GBeJkgI3D1wvyq8gzxi5gXRPWQ/tkIFArbYmCKynss+Z2TGlZW6ExlvRejSiCzuhYo7ZcuA97zL7Q3Mi2oqyKshxc0z135mHW1TL8fFxGLWX4V4qrYyIRnPkWTaej4MCMSU9M4643kbPv727j4fxKbFUkuOTORABrv0REU507ziqxzgDgUbkmhDVAJcElx0zs2NKe/bJDvo3iLtU48SAl6E+8j6IMzOxPzRnirbCPuTJzxwwKB7IExBXYJnj46NXNy94IWNp5ZGjuTgZCT3+ZXaCOLxjVLX5DMRtHsbn9KuV4dcy2RLueuu0170DdzCVaIGQEcHHo8RwwYI2+XeJgkAbjyHiUXhTO3gaLbganIdiqdCexmyhPW/ZH5IT0VaUVTGN1AvV+0ie9VoMz094IaWVEdFo8pSUpiahgCDGCBZrKvg+HgYF0nBC0WJmhIyfGhARYTuKxRq1ak1IK3y7qezppmFwaVWZzE+3GjIiXBQ+RhQ3KvEy5LIRloa37A+1e7SsCr10HsqzWoslXIIXUloJG1eA4Fk74q4Jv/2dPIzPxALKU9NhDCkolLdAyBBuObiMhiUDEGlqchzDJZftMrNDqZyonWbvUPA5HDpFzirzwruS/RFCGZGF5weqs38oT5qw5jmIwxGMndr+9B+i0SgA4GuDohWZeaDPEfjNMVPu4GEMQZgKZrHmCSYv1mlPWA8j8xwo/aDPEbkmDkYjizXKzsyOKa2sbIi3oiVz/ZIfFEjEbGEiGTgewv7QnJZVwYs1x415KE9arOc5iMPhjZ1zK5mUxrcBiVGRI5ZY5kGNhRDddR5G+TtWj9rSzIhY2byJeVRndvzfJS6O1H/P2R/lubz3eJ5ajt/6iiM+VFa8u7bbRitD4ivskK/a/9U88c42/QIQ371t75o2IDYgNiA2IDYgNiA2IDYgNiA2IDYgNiD+jZZ+8bJ4HLxnacEGxL8BiKjf+6aN/E3KSyBvGn1zxl+Ujewckq/W6Oj/Te4f6Fks09H3aZ6jvxOXgT4gv55W30uWJatHBJwSKFtYHwazOURVAKVH2zhpG/nQTAvF8WwYGhEW8avFAfZSabCLLIKLVMs5Io0wX9Q1Vm76+bHRExE74zxpljUIhboEszqS6gS82fIlpjPfceIDWfGcVdJNb+IDnwSXId5Ia9/T55LJAc88kiE7441XD553bWAsgfDSRP1Weg6EeSM8oFb2NdBX1OQVifoVaYx6TsuhI3RYPuqVcsr3y9EpEH6Y2TlPDXtUeWIGhCuAmP7rk+9LvvVnj0bUj8SX6fCxreQQIK6lrZZMnc08EuaUrACBwrT0xbUmHeiWQLgKEOSqye4l/m8ogHDOXEQ+8WAWM00CAp4eY2eZUPUlVHkiMxM8e7xE6yJ12sEAcQiHVSAODwBxWAAxL5mYEPBYkPPoMLoCCGFJDPzf1H+HpKrBUyi7mWwHyhl8InwmKgL4Ft4VQAwJCIbk4K4BQfY6nhSpNOcuRiWE6qMRMdnZZzuFc8KpJ9eS6kuAYWA1Igjl46jsNJotJ6iGSIIeg3wufib2B5lxHFbTJSCO4qG+nrYdayUTE4KnEF4bev58PAPRMy3Hi/PoOARcv5NbUngive3A4wjmFh/lD97BQnHMMKH6jqN4zjk/vsOe8vBaAR0OLec4KBBkL4DwPLt8CufjyAJHfaA1YrLznO3smaGCXh7yqgCGgdWIUNEE4ErXAwsY9HlEkG93NiKW6ZingRzMbbiRVkQSZiWTA97nr66DHywQcM6rAhBzU8LRKdeCO/Qyu5ID3MLtRb3rlMK7fNUfmTtykLWA/j2wxfMRAS4Gg+0UiBjAOJERwVNS1q1KdtKFf9HUGQhwE4xGRAkEzZAuWH5EVr+wQCzSrU9N1bSqVhFDDQghgDINYQ6ETL2quTEo10LrP5gO7EOeWITmgDVioInS3EgMMpYGWSOGspw8NdnFWrpWpiakykCUdlogDDfBMhMUCNGtiLfumlbSxQoQ19NWSiYmBIDA7wj2q/i50Z0y9cqIYMEfvRAwNZ3T3U6geZn1PHpMTSKiMrBFZwNElLumKHdNsSgnSpoEBLVLu1alsqAbAj0RmlLVTib/9Eh9PjurL8GaSImZQHNgUOYCBvLKc4TnZ4N6OsyvIIth9r2etnyO0JKJCUE8EnBKwHzIzA8oY2gDpzXCgSEBVgWpY4DRkYmUvGSfmcB5xrzPS2tErcLbu4CHh9UNzxEx2HLoOYJS0PMFJjPW+OB1lRknAyt+nCmv6IkM2U6+FTojdYxucau3cgNWbiVb4Z50S37DPWW6dPO3/H2b+VF9YLrd4nB/Odf7wN7Obq847uiwP92CDYgnacEGxAbEBsQGxAbEBsQGxAbEBsQGxAbEBsR/EIitU29ufvHL30jpb5a0AbG6LZgOeRuECwH3B780Xilkfj5UzqctOpMqkK48f282YzrERSXRtelMdKJ9sSAGFBfA6FyOeEKtpDfM9MHXEBbWQMmY3xwTl2MIJRCePYLJx5M800tbb6t4xHyF5hgkc6aDLyKdQK6h2rnzA2H+u6anUQMiQLeiAEI/uSzN9wUQ/iYQIdgv8ei/QVw8Q9C25k6M8nF9ZC7HHIgATolYGa4AcVtMJtZ6bcF0qIaccauknFrpa0DUR0SN6RCqI+JhICz8DMTYWyqQWwIR4M0GqcYCIZwSsfI6EHeOCHL8HEdEKSmZDuzaMKI2k4PDJVmeAXkPo3Ilyggheh7xQyrnedDSFAjGxWGEmyYBkZgONDo5r1PnjpN8Hs6piDJ47zxnaOAcjsHJhU9p0c1n+NooAAE5crgeLUGBOIiKvksxW4iRIaIvYuVRVDdwXvkjSIu4KlnFI7NLuJaoeRzc2Yx8LEYEDceAqUkCuhAVJPjiuuqD+n/LCCH5uuvlw/F57BF1KfIVF3sEIuiVGpOZDiC+9cnJydMm50PskjjN4BrdQWoyDA0pn48dR4ljEbBGkF+evH4YWwO3leOwSIuEAXKGhoh84M5lZXKPF/YX7BQtkAH1hUKTxDltK85KLVHiwCCiiIS+KIAgHgfO5cHqOXUe5upQnOtS6HnRm6icB1htmou1JBUQyUwHZSAiNVQ+2otaE5mECathSVYJaS/KxwCfA5wJqZV1QD7h9ufO/ORaUbr4tcXfTVwO6JQE4UIquUetDIknYvkjiN/CRNGL3FgEaIOwC/eSGVMRMlkKBHlYLUPhU7rEAqH6SDGPCCzt4yy2CLKMKsM1Py+jZrRAgOsQZ4wMuqqDxDxxovKB/4UONuTLxwKhNfcSZUVLkVpZB2SoApHYfUOKPQTdAJeBSPFS2Mqg9IiCPxKYw4h0wuiA3IO5xCwQ04QTMuUlMxT6aQblcwkIYhz0Pi8BojYRF7oU+Ty0OJbnAUSemmKamqBbkZkONM+HzDZFbT2mJs6Tx3GaBDNDA6sMjoEz4VXsJJZAnM9eNNUSHS0mVokoacyBUCtDUNAtfyRw/+jUlJgyQ7YQQOjUdD5yDBLwGAzT4UxqSMfRm8X6yPFHckdDbQLMBqtLkc8jfsjyPNYJXaynuoIu1tCtyEwHunx8erqRxfoMqij4ES5repyh3pEZGkp5lKnpLFFOIPwyAghRISG1EbTljBYxP2PURRslgwupnBK10kvvgaKq/BH+LU8sRsUjWah1HcRyM/W42wyG6q3XOv/C3XXe3hguanjQopWbw2/d7n+yjl+xYy3e559s5rKJX7Hqd1pYt/JP1v5Htu1d09bE/5yVGxAbEJuVGxAbEJuVGxAbEJuVWxP/RVb+mwz9l1FyNiBc3eX5eBCR61v1ZV+cp6gdTxyFWJRFLAerOlEWi7T4Vps/vx25USP7zWpKGCO9sD6Oyr1YamBYG/I3clGVRMKCS5GcV8VnYENps9g5/t+F3xgP9q2XtHewfVDrv7jWo7eB8MmpUUTgSD9icTX42dHMxbCqE2YbkqM9qtMfHo+QPrhdKGEI38Ilp+NcA8PaIL4EbnhIFAJfAcIXQLQOkSsWzAz6XjVFggnz9rq1cREXvRgXqecMh9mIuA8IdxWIkotRBSIkIFLGdSWMsU9AuLoGRhUIdz8QB3EvLZgZ0O5fAcI/DIRbA0L5EyVv4zhq3BA+dlbRdOUrtML7ACfiOKZYJomL4UouRrwwa8GnEjL7Ah/hLoAYLBDn8xoQpIHBn9FqxBO+FMCiCOCZnMnDpgyS3B75eD5NTQLEjJnB7uIBQJDziNpyhPw69dnAvvfxkD4hFqbG+VD0YuZ4QHsDvXUcNdoLa3HAregK3gbzGXhkD+zs1DACylcQxk+EXkUQ/oIzXAwEOkhcjPjJrAWfSjgy+4LqQxwSWQNqShjCt+DvrMeaBoZlUsDV3bPjEqobZD07SNlfntuzBMIqeiQ7R0NKi3w0wvtOl5KMCGp91FgzcLL2wfaiC6X2BpyrTPWQXrZMDMPbCBJVBBFSECOkdZmv0ArvA/FQgsYyuWQuBjk8DRcjfsosnxgPEZFLfNKlcGtKGMK3cOKur2hgGMv8wEHcgsZXUZ+zMkhye+ojYsHMcCUQsQAC0xP1FJeY6gMsuRfbS6m9gZgqiBaDXk5iByVvI0dA+QwI7BGZiaB8BShc0Hf0MheGoOocwsUIhuWQR4RAn+fyYIFYUcJwcPOXQFgNDAUi5hVOg4Bk6qQwSHJ7vKsBsWBmuLURgdgumDApMgZSo752UCCUVVJqbyCmShBCggIhU5PlbfDQYQUhmp1ZKQZTU5q3VX1CjwbVlBEuRnSWAkBA0NzohlQC7of6Aoi6EkYJREUDg7oklesssSbKROQTgyS3xxd6Hl7umubMjBkQxFvhm7ooFrFVfTBaTlnrLPcieCdZeyOo9onMXazFkRZrw9tgXsKZo5REWYIhLZXYHuB9HNNRcDQyF4O4EUmmxBF7A7ceWgL0mag+u1jXlDCiA9+CnyhcTQOjddmyaW/IVDOwSJh/IgyS3J7AIlyq50HKg22FmZGBYJEtsmD6S3ZyP/Cy3p5DIoNG3AwwHyT3IngnWXsD6T3n9GPS4ri91bkUawyL+XNqXLl9W32qeXCr3VLf0x7/uJ5HQVO2dZV98avck9WtzqVYY1j8EhB/ZJvHZfkKEGXrr+39tW176fdkzduA2IDYgNiA2IDYgNiA2IDYgNiA2ID43wXi2c38Ffv+VNtyPb8A/gbEnwIi3CqEPjCcsxx0a4tSost/17do84QbaeS773oqeBX41d1Mw8Ob7//K8tqibdkrlzd+s3vxHE/i/1j3+f8u5Dn0w5C+0ta3rfhv5e1S5F4btZ4yEgwcuKKZ4G80EqSTqbgZy6FMVQfCz9D2XwLCGyBqih78cSF9yT3T8BAvZEoVU4Ve/AhoG+VtjW3JM8It1sDr8HxMvqrsjQlGIT1a/RKzjUG+MUU9ZQCSVv3kzvAc1jYUArdlnZnh7xwRwaT/6ohYA+LgDgsNjzBTZrFA2LZR3sMcCIQYCQmIFORj7JPH3qlHrudvq6sUtKhlZBsrzBGI7YCBQEiXWhHTLwkZTyPCiz/Nxi1JahPiDgGLgc5QKRpbRBkexxGqFPxhfaHAkTU2VJ9CzkLLwx3Sp7tW0UM1No4jhv1MwyPiY1zEK2mFUUEMDGgeXISvkqcmqW0EmWENiPMZQBwMEAeOLcFfUkfmwVzgonIYU31kFxSEhcqpCYFEHNnthccxhFIrIonksA+awzQV8UCIp6BqE8582N5DWWOA26RP7AXxYssVUCpwZI0N1aeQs+xOFIWYhaIH/QuuRAbC8kYwk4eQdTbgdcbU9Bl0atJccm2rhMSQmSL8nbhwTD6F2UFRYBCchOK/9CIXEzF9p4mK9olaoy6oMhIM7VOf8hjViA2fodSKoF+s3MKBAkQnoIhboiwJ1sQQJoj4ksORF0SffMekmgE6gYQDKxQ4ssaGfo3f6l+Rp6gpfkRJb4GwvBGkhWvei5f4KhBQCDBTkzJFHF/JBz5CNR/AHrkkskB/0Ik5IjZYooFGvVzyDUVJ84TMTJgDYbQipl8Awo0WiKSSEcGS6EV8IToLRBzcmEUegsRr8Wl5KRU4XNLYUCCmffxVwZaK4kciNBggjIZHDCmK2CNAuLG2RnDLB9AYiG2SFusUkUuBcKOobzil17gQExBlJBi2QMJ9OOlGmfuMVkRgIHj6ARBkm41bgmmLhl3s23OeYiT6SFAgyGlONYjiB9NQSgWOrLFhhEJ0anLF1GQUPZCb7o6kkYWGhwDBbVGdjSi3MAmI4GdTk0oK4a7JrBFMvExAxKz8ke+a8tQUss+aEaU2y8UyY45EnqwcqIXgcQjhK2tF8KyIKCsEBLMcjEpGK1G6IrMYXFqsqTS9r5ZoIayC0bLih6pSlAocWWND9SnoKJbloAHZFooeKIGeF4iDcZxpeBA/Iig7RHQ2wMPAcwDxy6e2Dch7VBmugLt+5olA4+OiawQmyCFpOaX2jmgvXWDUHuW0p+eIgdos9RSRYDjlIEDcs117+tSp5laK9f1f377+dHwff6PUElmLEnONPRKLJ+tHmSMPAHGNtRDd7962d01Psm1AbEBsQGxAbEBsQGxAbEBsQGxA/AVjNyCexNgNiH99d95X4t9q+rLep760/9eBYE8KfRQbeTfWUqxspRJHNNn13RO9bSRdDXyNlvIYTzV9JQf3HL0MQ3zr8vWZcjFitjeKAMUgNSUF/swosSV69p4dhY2S64i3WrpI0LI6yJWumV7EZu5InLWsFTvC4kgCghywcyCUw1GRUDApZkAMMwMDsz+S4IMwKCxlgIEAKaaF03XmyA8ivWCAGNDhrBSRyiq+aQu2ROGPDGCjcLaQgZiHKjH7QwWpwabxi7NUnQpDxHnLQBRgL115BP0XnemouLxEVkfEHUDEQxZ2yHkqQIDn4GIFCLcE4iCXnwKh4usWiEWJA9go7FGOXwGivQ8IZ0dEYYcBYn7ECScjzOOBXJg9kaTPCz5G+thXmA84CkkEKFgkY9jLR1Lryg3hCCNnI0uOxg7wYblh8v+OcMWoTkVwqlrh5BPi/OUzOWiYCzLQJ+atZZSYEqPwmMBGoSNourJL1mK/eP6wWKPGkItUgYAmB/Q82lGE1kcE/AhgxgiDpLBDx0o+0uNIOygnYxYP5Dh+MnsiiEsQChKSIqhciTIf+oDYJk7cmuaKYc9Wz1ozvVCzJNxAXwIB1+Fk9rnHtMElI54LrnxEO4Hx3oyIEHAEEVBEXqi3JQJa8r3zVDEQTIlLovEmKrFfPLtGe3GfWiA4jeh5RP5uPaYpkOCAJx9aC9kOYfWNCgQuKsc6DsJ6WMYDgRlB4pR4+y0/60goS0MIhxLbxCeHfp4ieEYeMD3x4p1YGRYI9Vw5mb8RjUUjk+iX/e0Fw9nLrUDI05M7cOktSWpdVPIEJfZBR4SuUXr5KWujHvvFiywGeqScmkR9Y+A2Ddwi4TIGITKAmWLtKEdEbqtPS4Tt5iEzH1TNYs7HiITq6AwXSGObQNxnCUSSSFA3vSuByAxWNQ7RWFQ7qpRYwPWcRoSUBbZdFNBtiToT14AAa2Ml9ssAIFT5KZ5LIBL5bEhxW4YMBJgpNTv8vK2DLhFuGQ9EgAg6NSU+xmJqOoxCFIsWCGVGeGGQZg0PnZq8Rq6SWVLoJcqHSPSyoHaA59Fj2kGEElEBQaSUiJhAmJqWJSoQ4KaVdazEfhmgFAXRE+GNDVD+CFIfItVJ3JZggdA+WtrhlUoT5UacysxAlPFABkSdksXa8DHMYg1WRhTSJtSSUmQWZirwAkZ/Lxp55SiRULDAtsNRrjDwHKjBbVqsuf4hOHBJwPDgOCnxKBFKPDQ10mJ9HJUDsizx6MBGoSMKhChiDCuxX2LL/CquwwlHY4DyB5gpKJ2FWbgkaJ8dJQKMqG4s7Aj5yIAjt+M3Xn++uXLU34xJWKaIK8+9tgzLmoiLspZ23ftk7as1rLUyVvLGq712+8k6ut8GxOPbv/kVR3ywZdu7pidp2QbEBsRXzN2AeBJzNyCexNwNiCcxdwPiSczdgHgSczcgnsTc/ykg7KHfTfR4vuvg0Wv1+1qw6OsNiKcBghUihvmHp/ymMFa5Di6xJaJGRWlrfAb6y3+ofHxcHiJKBosiUxa+tIVkhLtLAyTLDQsnIURtqR+N3IWx0G7UH3pWfG3z/iBf+HCdWSJv4Wd9PbmYoAhBL+PnKhEtXHiFi91no2gbNBhH5e3n4FPPxIDvasGiwPfTAMIGBtGwFt42/SYQ4X4ghhIIz0DAnpBCcqS2h4qSyKBnYygJDl5/DreYJUrVKPtadCw0inmpEnFwvs51eASIAXVH+dYYmh6Hq0A8NiJ+FQhrjzIu6Ihyp+ZASPpVIK4zS9AjfZjHcuF35b4ChPrWSq6D6GKcNYqIxBIZ1dNG+hnKesARxJfIQEDTg7ge4nWWuCUHE1+kdVDyyLXNNEJCm6K3lCVAwUOZJlDzyJodfq4AMuSWKuEG3jSoeqh7Svkk6dvoICkM0wR8FqICXGeWAJZDmAdMmM5gjloCwV8Wx5LrILoYEU5SZSqIM3+AisbAyhs9BErU1ysRU/Ia0QeNTtQHG/igh2IHwmCk2kqNkBDVsWljvCiTxDBNeMbNmh1+rgAy5JbKhSFOTF4BMKLTR/AhfaSe14jMNIFnHkEJrjFLhKF1nq8Rk2/v2ogAEJnrIHIMEVe6MhXAzAAQ5Gxn5Y2gIwJAEMfBJU0Pn2ZgjVsCpgZ4G16ik+TaSo0QIgeQrBXP5DnyiTBJJLYLWRaDjMf2kj3sRgHEjIg+TRt6BJNtyyUdOe5JZUQkpolH/RSo5wazhHu0X4yIcBUIYYgmrgO6BgGOIgbUCB6dKtYACGb0yBqRpqZVIKKzTA2qIwyxqK3UCGGWBuofTeQTYZIwBYJZJlFWoJgYRqUCiLeXXMiLtQWC62BFkJBoMDl9Zpr4xC+5xSxROt8KEHrX5AvcXZzzKXyKJGIjYyU1i0isB2JVTHNycdeEuCcZCF3slJ2RmRpcz6BMkNzJRiOknJqkBGWSGKZJ1BA3bM/UBaUCSA2IvEYIvaCX1kvkqzJ97hlRNOkZiCvMEr1rUm0OnwYC8wnyc0RWieDlOZZcB/ATlL8RwXIIYGYwJ+GM5rfnOHuOYCCERYFlWfjcUl5manjRxVA2BCKsWI2QcrHWEpRJYpgmHOdENDu4C0oFENFmM/Yo46Ll9vDkI7FkAp40Ypk+M01ATSV+yXVmiT5HtLO+bsfZk/U1lYhEOQ51roO93avdRsYvPZc+GpskPnC0xqb4+MKT9RpX3l+xctHXHw+/4rgnWkh8oBnXt0djk/waENu7pr+0bUBsQGxAbEBsQGxAbED8pmZsQHxzszcgNiA2IDYg/kRf/ys6+7/d3g2IX9uWbAWzxZrWxFpi1Z2AF24qd6zxIKxShVO/c1y+DWoLG5nn4LIuR8pflj8rOythUH7RNF4xnv4MyW1OXwbOy6z3jpbejqsyGXHWppWY1IaZEUw7sjRBYa3EB6+kTboTYGq4EGoRVQarVJGBCAuKQvkuV/HPQVJCBYh52VkJg10v6WxYYmCBiBpxHoWsIjfY0uOC9ZHaMei+Ui587SqYK18U2yoQNbaF6k6AqeFirAPhaiPibiDcVSDcfERkXoZ+SrsGBDyMaLUzLI21t/AAIpf+y0DAbc7cC6eKF46l1JWJMdeaqKfl19VgQQzgJWgIjJIHYZUqsiKHcjGOiYeheYeQgYBku8ZRUY5GVsuolJ14GfQds3jhRH0jtwofG0s8lFHIC4PEROG2XcbWMEJSX5jSM68jczvA6/Ai3Z65L4CmtEDc5oP5tNzJp9nKxJhrTdTTxqQ7IUyNUZgRMx6EVarIihyJzcFMkD6oK9KxOj1FMuHvscNx1Ho1h1XLqJSdeRmjApFdrtoqdWPCnx1A54l6iQTW5cjBnbhe9IUpvV201jn9rF9FBJT7ImyvwgIZlJ9w2osOBzpXmRhzrYl62lYG9UcK+gGI5zwIq1SRFTkym0NZFlhYNXaKjIhLjpKS2RtZLaNS9pDd8xpry7BBpFVqR+BgBXlE6GRIVhAzJAXuYEt8UXpctNYJ08UCcUz8lrkFMyCis52La2iuNbGSdlQWRGL5hBoPwipVZEUOw+aIJo7JaGKnOFGOkXozeyOrZSzLzgSBDERmg2ir1I7A/JBpmpEZH5dGQB3EDElAcMcOtvRlazl1jwksjQhnlEQKC2RZIu6FF5qXL4BYak3YtBmIrDuRySU1HoRVqqCh2w56pymcjGjyTuc50ot2eh9SvUmdI6lllCoYqiGis7hOrN5lopq2SqemACWRqFewjgjS5YBNSYmEWDuDLb3WWohKOKbSJK5HMEoixoIULYqWQeEfDDRHsvpGrGlN2LRY4I6Zi5GYGqpXN+dBWKUKXiTlbh0l+FHjsehiR3cyrOTBZVEpqNfn2IhR9UPmZVteRjuqmhJipthW6WIN4qRDhBkDBOlyIHBbm8qmvrClL1vL/nYmJSkLRrkvouxRWFB9BInRzW4jo1t9qonlU8mjT5rR/d7tmkXRzdt5fbtH7yNvzHVJnPA6y+VG6zFd3Ndd87T/plcc8beWLgPKXWOifIcFXzPsqYD4b7Z3A2IDYgNiA2IDYgNiA+LJ27sBsQGxAbEB8axAPO31MbPsmp3f04a/3BMbEF8vJczZBeqVjilJW8kzOzpjTNzDaXDVUkOMd9m83Iszq9Npscw103+8ddJNH+/0dy/p9vxr/46Ur3xOcxS/uATk2725lHsViG5uUTM7stNmlE5tC4RV4JgHvPD2+IwxcY8rfYlBBqLW0/7LQIjDqkNnEh4fH9QZO9MN9C/9eu0ARIfjzS713s4g0kn6pik7s0l/DRAnrXUOhKb+ZSDcDIjHOA2PAvH1ESEfDZ5wbVLDbwDRua7oq+JXBsKe/mUgrLKG+mlV3QJOFnWYwFXDehhn4lpwbJTEgnAlp6G/zWlYMim0dq1XP+WV1O4gFlAJEuMk5r11NQ5YNjX3tH/rutc3d/r4oKln5/Zv+zfqBvybgDi5UzdNNj/e9j/xd0ox/dq/Tb/fXt/oX849dSLl7BrKvZ+O0Tmk+PjY//zBKXcOuZDvxxvV/COlm/Y5hdOgBJld4JK6hfIL1NXo1M3YE9dC1FdS1JWC03C+zWlYMily7UHERUJ06pJlLQ0JVdCnGCeirBGuqnHAMrryuqbrpuuw0xEx7fOUMx1sAMS+I8h27vW0cy9N98J/p7T0q2teGpqN+F8eQa9vVAJKoYIbLpH+fnxMNTWUkkqgXHS04ZxTugbpph9NgykOHW3ZBc5l6gj4Bd6pGkcKBKOxE5jPsGRMuDs4DXUmha2XzreXmCbIHG5EY5wo5wNWralxwLIduon6K42IBMS0SAOIHYFEKdPZDkA0NAKma/+dznUCRONe32mPUlOJtM/jxDEQdJZLoFy013BOk46PUzl6xRt2gQGiD6LHpGocGQgZEUXUlTkQ1zkNdSaFrZd1N0INiBTjRDgfGieorsYhi/WpEyDyGpGB6JoMREf7Boj9CUC4txdc7wYITvvCqbmDp79v00mamk60N42AE3I1fLfQNQAM6XAcQPRzdoEC0U9O9GAipwj3wWFqOoyqQBEqjAkRj7jCaVjyGJTRketl3Y1eUxsg+hzjBMoawV1T4xB2SXcCEE2zBIInIQXixOv6To52vLbi14+37gXTSgJi+r3/maYmTDkNT018dpr9T8jVyKSFESHpeNZiIIi/ULILoH1Bi58L3kROAS+B9TDOpG5xlCVWo64UnIbhJqfBVZgUslhrvYhyoqmPEpMlxTLhGpAiuGtqHMIuaU60tP7cvelzBBbrafHm46/8KwNB8z8dpaWYFmtaWqcpbMpPyzMtzK9vWKxdg4WXnit42cdizWdpVkMuOpqBkHQ/p/HwtsP907qGxjUdjFg9dj+nIVbKiav1xjstuNeyx56su3Tn2VVzdE2tFBztVnM9sF3TwYh3NmJ5x1+WGh+s9/Z2j2WPAbF/uw5EPm9LwdFvAeLxbXvX9CQ9sQGxAbEB8UTVb0A8SfUbEE9S/QbEk1S/AfEk1W9APEn1GxDbdh8kGzz/JiB21b1O9rqVdHljd3tT7N2oY3m0KxgO/Ia/fFFTZ1U0q41qlilyiftF8v3iGPE09g90ezcrrQF/RM5VgAjV7mhuALGrdKa6xRWIZhWIZnasmbnvSyCaAojG/NMsgGjWsNGkJkWuYbfItVu2rilbvKu0YnE5mtJ24I84ZY9kUZlg/gmPAuG+GQj3KBBdFYgud/POVdN3XwZi0eKmkqI+IhIQJ219BYgov5mF0L2/gYfw+gZ+wfu0d2qIY9B1ykTgM+7HG9wpOOOc8hOQav9zb/ZQFrgLcMEgBdgNnNJJPknxQ14o4ywYD5k7MdV3Au+BWRVsJ+c5ZfbE/lRNb1LItCEtTlyKd2qRArEXy8nLrOn4+M+9Y6t/pja8v6a+oHzTL1eUdiJveWaPkA8deh4izj4Iy4U5CN0/4p1t2Nnnmn8aAoKdeSf6S0yEhpkOL3Cn0xn2w4Of4ISvMHlvm7zXCMPhpXkR1sKUout4j6tyYEbgL1Kim8B7wJ6mVQ/yZAl5z16oln+0psSe2FXT2xQ6iDptCXIwW0OBoD6AQ3TqPknHxztMTV33Ii5RnhG0Lygdsz9MaSciI2T2CGtThaDsF1EOmUYEsQkIiNd38HSYX/BOQPzDRr4y9wAgEdMBbAbmI3R8vTIPYf/eSSfwNSR7iePQ0PFOmkDMBqRqkteYjpFvOHfTPjEeOmE7KD+CoeYUr+//CMD5bD19mQILKbUYVmmJBoiphL3wMvbvaHEJBOpFCu0LLtXBe901dkRk9ggCmwRhv7TsxmWfO7EJeERQpjdwDbomAzGNCKdAoKspHR1rTh2uV1xtcrYze8JxaKSTuZw9uBTS/MSUcC8FEMKWYMaDpmVLmUkh3Tal/0fKzuyJevoyBW9vYF5oar4UDBCTJVMKEGDQYu7o005bIXyO13cA0WmpPCJyaVgjMnsEam0SdYVjsriRR8RsasKQfiEgfv7kqjp0GDfnZefygJ6AIOTBT3iRDn3pzF6jIMh/DXe5/OacmbLyQ6ha1FhaI8CWkKnJKXfixCkbrpdq+UeOZvYEddEyvU2BGszU1JQjQs6+dDLxCJMDpBomg2krGmFuoJckX8e8PVcA0WX2iExNTtgvYaBwaiRFxIvSz5OQBnfgJUz7NPzAOqC/VHjjwGnA0sXXBS+azE/4uceZn3uz18iix81g1sIuURQbzsnMCC6xkzNOphdmWzRYABvhTuzfTpxyh8V6qoXspu7O7Il9Nb1NgRrS4i7sCp7smMkhZ39igW6Ev7GX49MNh1gNlgY9aTS4dcBi3by+daY0PEeU7BEwUZi5Mg6sH3hFOfC+DcT2R7auufUgdMvdft0V333pyD1nO2N9rRXdSgm3n6yj+wtbjfGwNP7rQPx6+nusX2vFvwiI/71te+m3AbFtGxAbENu2AbEBsW0bEBsQ2/YXgFjP9tzIPrcwyxes24D4s0CEtd9XgaDPFYMzCgOq6JFT8ZdzroxKQp/dzjUM3OqnWu3SslBNGUrrQmSZ92RfrLbCHvXj7A1oqCWP13q7ahm+eP34YB1otoecQ9k2G5K66HzTM4EaVA9lMwExzoFQIQkngukojDW3i1R+qaoRihgtDuFFglO9j7AChM/hPII3mFIMC5Z5F/ty0I9QVFwEMZFP6JPoxZJdMdjgIeFuIPBxcXAZCLbKRHipFRANEPEaED6y0ky8CoRbRjBZA2JmhrMxSNaAMF0cXAEEwg+ofetA2JJmg/Obgcj2yGxiIrzo8OePcC+ISoKg8YjxQWHgL2Ad5P+geYFwHRqlBBFNvH7TPGo4elJH0RD2B46O0vLX2RpR5MCfBdNnuBeJ0YIPGXPMFuh9qFIH4qTop45TaRxXBSVo/iNb5wfRFhkgzK7RV44SEQXpYmS7znDVhKQ4Qtbg6FF0PS4j6zNoOWfpoaRa0ibLNLqLRl6JEboH0ak9ExBDts1TO0Y2MvakYfEZoMcRnGpjIDoKwjokuX6EFpgmrRh9urqD5NWP4zkR1DQiOgJhDgaN4jDgCuqhvxEoLABqdhLISC2RsAmi1IE4KZFjp1Aqz3FVVOVD4pK4yFMqlBF0atPoK4jHgnRTvQPHgqG5PxxZ8x+fzpM1sWctEAhRcRQLn8uJTqOgkADFiDSwrE/SAFAZiQOAgECA6OQM2TYCIqo0STtd95+qEZC4BcGlEXEhmQboW+jn49TFEpxDlDTSiEgchTwiEIHlc6rJjIgLgKV/AQR/wn6hjogBObyUB6UOXApHWd4yeKg9iHURNxnB2odcrTjtEaBjajW0E6ITvZCj6H58SltaJ/1xUSAkiktE+5zLf6kVHHNFlUEQ1cUdRQmkT/aQOkO2zXNrBQi67AGERPegL/nHmEYEa1yIvgVfSyNW/igzIOXhEcEaNW5UjkIJRJQO1jUimrg96FSOWRIyEBFpR1XqkFHp5kBEF1N+C4SxD7mSZXKMw1KxlAQCdrgSCDfKUR4RvEYgxM5gSpG/3JaRR4Qqj6DXJO5KTPYACLXNAtHToPoUTkEIqo0xZCD6PDV5berUxYPOMcxE8E60NljIJ0jYJAMExT7pAQQ3vseEQ/VzpwKWnmK2RNmXqUnUPmKammg7n1MeTE1m4kSkmGRfgjzk0EpUUvRl0ByZVj7looLAUeSpk4HQcoSD4cx0iF6E2lSSaGE9EExH2R7cQaltnluri/UZCyl1vh9VG2NIjx6qqQGVDVrUMhCyGI+kzYFLQfSaEJ3lfBwR6YTTRNo/FjoaqH8IR4krQkdI6gT70PtQtQ+JwSKLdYyUBvocVC7ye16sHUeKUfty9BWxatTYLcd0Rhdrz6vPUZZwz6nJPsdRJTU12oeW4y96kUASvRIR/YJ8y8dHticDIaF24k2lhdoDnV85g+vK3blF9zu3x56s77f6nhy1lvk/+YqD587RPcX2WFMft/rXNEq2d01PYtEGxAbEBsQGxAbEBsQGxAbEBsQGxAbEBsQfAGLrwJp1v8PKJ2v518zxdx77VwIRzV/8jIs0bT3tzKdsUy7exIS5Oe14jXsR68YOa8f+7xKX9q6+sKt/x9ZWgPCuGjHmwZeHtsemsv19QAzR1OeLq64GhFIGypTqfA+rQMRQ417cAOLKWInFnq1zzqBIBJaijc5QHTIQrasGKrkDCEtemAFx54gogHA3gXC/AsSCe/E7gLjWSfa3r4yIw00g/Axkv4hKY4Hg1+SIU5KjkChT4wCWg/AWvLATLuBnuBxn5ZDZCsLL4H13ECYIleXZTZLZFMcRzAXLH6F9AWLBvUAaZUgckpWthKin0mCbcj20+ZllQSW3koLro9qd5VuI0PsoHA1x2HiHX3A36dTEjs+oVkjEGNuaYfpvtO60dlD9DXYaxaP0MEvLDzw19ZM/ykQhkZAFvD8o58ELO+GTXY3exFnpg0YyEeZC37OLs4dLcnDszXbiWOzl30Hdook/EoOuEUvuhQufYhXxwPpkpQTUImfydJSdvTEFAHNqtcZaQWQLYWdwR2qMFpRIndmjDLQxopVeolTM1wgcdaql4Uo2jC+4L157UH3vMUhdPbt5GQgbHkOZGhK3hNkNLQY5sxM++a9P4T3wHyKZgE2hFbOLkBdNvhYv6tsF6+FTmpD5I22ampbcC2JRMHsiEHuCjrbC+MBSSuwQ6oKW7ylCWmC9si74vJd/0ToAAa6GRrkABwMcDTqPaDL41brKiHCwT/Pm1vAyX3BfbKQYjWyByDRRF2sZAcI6AFNDgBmicCOcRDb55L8WCLk+HY+IdETYTXxvAcYTOeOVBZGByPwRN5ZcB8u94OssNYWPipVsm5QwOfhHIiWIxWqhUz6UN7yRwAQG5Wr0BogoHI3pghiizO78qzI1sZKGammUrXFz7kuy3l0FwkQhCQaYqIOJgQjowBDs1HQYNZKJsiwOowAh0eiYA9hzqI9EUQNzwfJHiNZiuQ6We0EsChNsJ1mpjA5ih0g5Tik2Pl0qzKzoKfwUuBZpRKTZ4DAaIAI4Gtz9IQHBU5MSxBQItY+1NIrW8Djqy6lJ2TECBC7hiEgyExCIU2KikMjyFoSHAJ4CxU2i30yaHI8mzkpMkUw0AkpkqmIMXhZA4ndMMITMpvAjFkLLH6EF3nIdLPeCWBTMkKCgN+ByJOojX2uRGBlDyCwT5zQYjjArpvpB3FSCJWihYrXTxVrKPlMbvUMr8YsWa16CB0SN8bI4u6SlUbSGUp7LxTqGVpkqXANuDDgiTL59ffTG8NotZf2m8zZXIn7DM2u+rZzHialEaQl1i9cszSOi1mJLr1+/fXWLlLOWx98OxD1ciV8Hol5LrJ7JR+IdZVx7xRF/1ez/xrumP23d9tJvA2IDYgNiA2IDYgNiA2IDYgPiGc3ZgNiA+Nd17p+A7t/fgg2IR0qnD9fv4STEcvdyHOwu9pDmmF5qHZ143YZ7OA8zNYzLMEth9vmdaird3cuoKNpwiSaWcK45mlDPuY6vsDZsbcdxdi4sgPDDfZwEFJvfbYJfkwgEMaeJuZQBzp4Q7+E8zNUwhlDKR5h9uF6k9Nbdx6goqQve5a9CrVpIcTTVcT9rI9tsa4thdi4VkIA4Xu7jJMRiz89eFttg4RYIf1cz1EetahioYezLZGZfgEDpB3cfoyIDgfL7UOGV6NHwlRYUZ4va4oy4ouV7OyLIl3SbkyDsCYe9yWMXdR8jIrM+osO/7FAh31O4j/OQ1TBQw/msXBDUkffJnXJIpXt3j/VZV0PLPwR2DXEe8nPzV+JOjgak1DpcuK8OcvKgJrIw1yYfJqdzWj6XbtaI821OQs9RQLzD3uQ2H3Qfa0RmfRwHKqsXnzV9kV06FuvlWzUM1PCZuCCYsvI+OSGnekdVernHeipXfdMoP57hQ6Y0x1EnWxwFDSK3gGq5VUeQz9tVwcPWxi0z57T8ws3knbuDk8CsjEsaZAO5v/M+nOFgfURmPaAa0U66o/xSDYNq+BQKArQtVJghJK6Ili4j4ob1bBmzRbT82Iu1ShECEL1NmeoYbtfBlkUvCh7ih5faDqvnKkDc4iRALUaXnXLfsj4isx7Y2AGCB3dxHgo1DF2sVdNDF2u9hnlEDCqncB+jIoheRl6so7NAkJd7kVLqCHfUQWURM0QUPHpbG/VM/VwJRLzNSeBJofdOZU14DPRJVCQa1oewHqaf7GqPPP3d5DwYNQzUMCQuSOBOyvuUk7SlUHo5Na2VHhOFTssvjvHtCLekSKl1xDvqANHHi4IHrWO5Ns5bOUelJ5kYWjJucxIOoqyBPXqOSPtYIxLrg3RrmOI4HllQaNq/o3yrhoEahpC5IK2DQgb2mTXitPTjHYwKMCiwRGr5yqqAXgbfBGDCMSlTHZfbdaRbDVbwcMHWlhbn+Tmn3BBXfxC5xkmYP0BFd+2h5lc5D2t1xC+XXjuik9KvtsAwQ8I956IzI8LdbPSvbL+D85BL+c7Sb7E37qkjl3GNNWLPbe+a/odasAHxJC3YgHiSFmxAPEkLNiCepAUbEE/Sgg2IJ2nBBsSTtGAD4pHSfydJ5NFyf3+3XqvhTwmXrNSzAfE0QODdIqKQsL9rvFFSrB3MftqC33HR8mtaF55T8tvZAWk5EoowJjQeC230xpNF6mW/5JBwirkzv7BSwnwYa5ZNonNgaxzN76WmQlLniEsr5q1sU5yZqV+HIK4g5bUcudeFh6IeWfJDOXY/hlDv5PTvUDufJQ4svwPxQ+Rz3cLZzqnRxgAFcrVBVeRTPBYnb0aHOGONlO+ErUc/zKxspUxtbdkuBcIpSWDIv8MCiFTroEoxoYjMYm1Kb1aHlr0yKtguvJYB/stWgJA4Hzweoq+wRNaA8HJ9lKobsTCFfdaLuCrSfBlGUdQNYEM0oK4CsXjJfB2IQwbCRIGxQGTvugARl0D4UkplUCuCsaVsZXrBPRySS82wQjhUivBQKKwESCPT1TgEBkKilCTdC+IsKA+CijgIe4E9WgOOS8MHZXQIv4OahOgm7PY5jHqF0q/k/xZ6DGyIAjCAQO1wvaMWRHCwnJJWPrfNMVPIWXRIGh5OY1YM2lptD5UDLRLmUwwxJMAG8j9CESRFTOG6VIMEsScOSZ0EtgSpF3FZQFPA5MOCGPD4hVwPPPoChB+xRtBgI8aBRClhnQ39FBwuyyA6G1DH4Ot/wHHuopEuH8vv4C+IB1bjcNDA0CsFdWBWlYAeYgNg88yO88lRj+gnep3nOtTpmQINCFMtW5mBaF0GQsMq0IfnUEegc7IujPz7rIogGjEFdanigpcP10W5Q7gciK/yKcEP1CmbgfC8RsiIGIs1wqtQjehT5OAYiF8SJXATeBaqafEpkU1aYSbYEWH4HYOW7ySaiQIRQ44slUcEIJI1AnBcRCNDm6KzmeWQJBqDRnNB12Qr84jIkiuI+BKSJaooIFdqYTUipuhqQwt+VCCcKHckCkFwcyBal4GwzEH0ehoRFgi6oyiBUF5FUJ5Fj6nkU1baeAYzwa4Rmd+xbNI6EOliKICIukYMOnFAVqTgkOTfEjMluNJK73IH+xQFZg2I5eUTU0fT7UzkCDJcQwwqKHEFCDe6QceFm60RBggN7MSLTh4RRuQmCA0RDAQ2oE9TU4wyFeS7JuV3OAlbEVeAwH1ENHdNzoU5EBI6iaJX9ZmhGi2npJiaWK8jhGwlxCAUCKt9E4IC0TOfIhgg0BdpCmMVDzs10XoVI+wIQW3wVSBoLHhpLdaIfNdUANGOSyDAblC2AZZaMBBYl4PVMaiJxGTwYyieI5TfIc8GwjYPEmvEyXrBBGgsf+k5QurhFMFB74OfJyh9POT8g+WUCENCYqCQXseUJ1uZSoM1IUWBQVrIprQuW3rE70EVQVLEFK5LNUjoOYLyQ7kjczko9RDAOjlKTBhH0V9wbvYcgRsMf++TdeYsxMXTwPVtWa7lP5T8ha88z15XudCL43YN8Y4na1tX/JItV+p59BXHo/FH/quvOOK32LC9a/plIL7Hhg2IDYjvSrsB8Y1N/5W0GxDf2PRfSbsB8Y1N/5W0GxDf2PRfSbsB8QvVfmfa77dmA+KPNP/pgfjuwn83sL/W2b+S9m/ZuAHx92z08r55vrXXoi5EZSbQm8sj/1JWCL11XLJCqiyRSq3YMlPCfM8fuRT+ad996RtWWLDOG5msMrwRegur3veizWra7X6tqfvG6/1F9SqfpLBT3OKDAlFjSCzxCA5v1pOzg9+2Kyskzlgh8DGEWhyPYVnrnBNiQmEMWYodPg5v2sC2XOeNjJY3EhNvpB7AY25ZWPbEsLB7QE3LtNpf3Dsh81XmI2KojYhbQPg5EMIKmY7MWCHBlVFF5kBok9sMRCi7xgsQU1fL4Aizi8kAsXLp+oI3oqmvA+G+EQitV/kkch5xSpRrkKKrJC4HfzF/Vt7Dkt+hzATQTfjXkK61xAqBi4bSK5sjq2NcmHOReRlESbhw1JGCEzI4AYG/W5arOyLyCkvFq18aSh3rvJEQLW8kA5H5KuBowC1r+yP3QbadnUkDnFfaK5knMk+r/cUjYsh8FYd4I+AkAIisuiEO0wg3ZG+k/S2/Q5kJjO/I/qdR14jMChm4THjd1P0JVghxLUqtDwLik92YBSdkUJ6IZ3iirBGBRX56dqY68YBd543op+utWKm8kcxX6UNWlLH9kfsg2y7heNjlm/U3NC3bYNJqf/EKOmS+ijPxRsTDZFU3olMxDw1EseR3KDMBqhh5jVA2EpyRFPXEKw2BywH7g/gRqrKReRkkiQIgDF+jGBG9zqFBORsAIq9S63SFGC1vBN2eGSNQ+8hAFP0RkMba3gpHhKij2ivox1pa7a/kq86ktF6D0biEbGJFXANC+R2JRujKxRqmZCBiKIEQXQq42ocZL+OcgEjH7RpBXSlKaQkI8TG720BAViXTWZSukPkqvZAR5owRKHXMbJcre+q/oddeGVbTSn9VgKB4I6kLBmdUN8qpSfkcc36HMhNiAiImIDIZgaKe4P4gJNJYEH4EAWG1Puia/QR5q+Br5Lsm0CegnRGUzmIX66u8EdEzWQKR+CoHYQ7K1JQsyH2QbcdcDzaKxGYJAkQtrfSXxnUxQEQW6UCUEJqBs+qGk4BhYEhApaLK70hPD8f0RAF+gqHnRCpH+SA8ZJn9gXgpYGgoL4O4IdPifqbUma9hnyM4lkpsUylH0QHR54jrvBH7HIH2YEXJfBW6BcgXarYs90G2nSwhzgf3iqiJkJkraaW/sLplvor7DkmG+MCTtbIb1lgOj8QZjZVb0+/hjURXe0rzbt12/0BfrsSOuS/z9e0ZXnHoM/Z3vOKI7nG+yiN9ub1r+lMdsgGxAbEB8Zts3IB4Ehs3IJ7Exg2IJ7FxA+JJbNyAeBIbNyCexMbnKnylnr/Rkc8dgGcD4o53ZFdTZLUL+RHnvt62XuiYfsb0fVpciT0Sq0WsRyQJNktr6rGMESfvejVeSzS5c2vYdXlBVOtUmnGd1/Q0tH306XI0ZUcBgpwObEFIQS9oG66/nToObqg1OWaUCif94Ba6xLV+C9k/MNiv6GNF6UK/jg5lqSuaH34GRJZcqH0eq19olpoZqehRXFghszF8yWEY6ldqCDaASQnE9Aodn6yXQFSpB67oBV+55uZAuAeByP8mIOD5ug8ItwqEuwqEXwBREy9J7qC4BMItgFgbrmtATJ4XKHVE/m+9QY8BIWoXThkJbVK8sHyGORchMC9CGAuDy4yIsFS6QNB6W4JLEUzojKpmHCGCnjgjqNdD5wL1qJZFn7xxvXwGbzQzzGVCQPTkg1TmCGtjnJFKu6dNVhDdIbWPWS16lOgP6A9qJ3lBYQ/lP4jiB2Ta+T/hjxRlDlIb24nSYDfnVu4CNRt+5Sy0YPkMcy7CcfTyMXjWuchrRKl0sSyBj/Uy8bsgkUmCfCSfOCPRiYOyV66EMEagk8GckbOOiKyZkYHgcDTnPlAqsDj4Kqf4Db29TtUKcgxr+5yEa2BlhIGA6Lk/qJ0xIpKLT3wNubqDBhlBfUWZAgSfCSgNdkerdoEwLSzPb6aSzGeYcxEC+1ol6oiMiOz/LZUuuMyiBOcyXwOpAQSNw8yXIHdmFGUBpicmLQuXtSwK/7RqZmQgIuZxmZ6oFA6jcynXRLWiZW85WQHmBhQ9IodB+5SQNNxOcramJSLLP7RcRpskIYoydUSISkiUfhQahHIXnCpXDG5UxQvLZ5hzEQAEoo5YgY88Y2cgqMxZCTkS1YjUMiJcBiI6C0QsFmtXE7gymhnJU30pgYgawmN+cyJWTItwhH5HMPISU+oBQFA+aucRQiiyRGQgOObK6MYMhClT14ixlxlAagVTzqhd4ORgl1vlM/gZF8ELEGAsVIEwShcos2Bt8AQBFoYX9QpEMtGpiTkQMXMnlCvRJlKnABHDQjMjhzjDtYD/42xqkhSDjUmXpyYwN5i41oOZfZY4KPwcwfJeskQYIOzUpC2zUxP4TrRmoTSdvJxVu0B0ELpDzndEymdgxQzDRSDNieMIPYv8HJEZEaXSBd/NFyVwvWewMBCLZJD4JjgDDgSrW6jOxYjnCDBG2qRl0Q5+oZmBUsHXQM3gWOCGgWtWGayBniOOaXkuuBbnI24vzshL0xz9AhA0yWUgsFinpTppc9gyqSZZjYk3E5EL0Vvc7Q1DbxmTZLldf8otc4AHEVfPKE+iJoN435P1apySlRTrrJLH2nmrl3K7H9yiu5fPsL1reqRPn9rA/xUg/iMGbkBsQPy1Ojcg/mI7NyCepJ0bEE/Szg2IJ2nnBsSTtHMD4kna+a8w5/fV/mjJfx2WuQH+S6V8Ldd/Eoh2+Wvmeo1rBvAbyOQdGUr1inmuWJZlvAHxik3rzefPN++J/1JrVVxNcEntWtHmsPXRa3ZKDy0P/nK6qOc4DEUZA7lF0svFI+8Pao43xfrC5AWtYA4EOxMzk8E/CIRf7RV/ox+VSTGP/1KXfbAF3IoJw16VsZSECIVNVkkEX9E6+Sgxe4dyPUsgTNuGDIQvgVi2+RoQHrFJEpPhUSDcLwFBTIpa/JfHgJjHhGFSULwKhJsDoelrQMRZH5RA0H7MI4JZBWd4buFsd+y4aAttCmFIjDxMp38RdCMzGVASlDMOlVz4HSM4DFwHYq4I08EZJoVRBDlrfJKjlEI1YESEmJU+kLZNEVXmnAw+y5FWwEtZjQlDOvlRJSHQ/qOohaj7mEq4CH8FYg45GE9Zj+P4KomjAm7AcEgKHoCZ2tQ6iQVCAiQeYiPMb5jcg1H5HRwFQLQ3RBeAHYaR50ZfRPlhaQaJUVLmAseC468E1AF1Cyc8CVxG4GGoIohGLPkURyafp6gEaH7MSh9BP8wPQT9bLzgZzO0IvZxdjwlD/j+qodDmYEeuXsnQ9/iUOCpM6BHg0ABbj+MoMDmyC8Zf2he4e44Wo+7xKHSqCH4DmqXRTChmCHENNJhX6wAEZE5CGrzthYEU53+ZC1105DJRR5J7mFKy0/QirIdLDgcCJQ4DBHgZ8I/HrPTBZZKKjvBH5pwM4nbQ/0Hd/msxYQZtl9XmsECApPAZEEfFG+DA/LD1uAHhRFJdXIPu+7R/dAkIDXYRBulGjIhEFhGuwQixKsee26no4A3ZDcIkGYgyl+wNUZgcFgjwGZT1EHVEOHh9Y6gDgYhy6TjiPYzBKGpkTgbGJZe5iLZiY8JI5K+5NocAIcwWBiJrj4BypCCV9YC0gLooopj2t7A62LRYjAiXgOBpg9YIw+/oMz8BzAt2oTPtMC0RYEdEnYrKXNqxHH/F6dSUgOgBvjSlxxoRRFYErI8QMhBgjOD2NWQGhjBS8oSSpybidhDjT86uxoSxQdC8meSWI4LiqLgiUBqYH2U9CNujKhxOLj9V8ADOmH1c4ki4Iy8/QkYMrMDB/A6KTMJXp5AJdbEGIzoDweyIM5gO81yiaCGKGahDYq5wSl5Qz7rcgQIKBkRwYH14ZVpQDcMSCOVO+BEslJKTwfFQztHJ2fWYMBdtV+a3eOa3iKaS6HtAKUSfI5QBL3FXUj1HAUKt4Couun90CYhU+lJ3YvUuMObniCvbnBOx5EjcYk3Eul0y9mq127Rl6boXKyUtb8vdQ1vVEvflbcHTCF8x4Du3WLfrSu23uSbxix37B4F4fNte+j1DP2xAPEk/bEA8ST9sQDxJP2xAPEk/bEA8ST9sQDxJP2xAPEk/bEA8ST/8BkP+vpjKv7L7NyB+R6nyvjYWB9WxPd/gANezQ2ZzPLBxGUMuQ8qRJsLP8MtbvDtB/YVn5X3j5CigL/aml8HkIST/Qk13pB5LJa72/YNAeHO8LYEYnRVQr7ZtWQblrwMRnQsrrvpFaWH92GBLCDeBsIwJfLG67ORW3AuiO+LcQgAmh/Ao7QtXgTByGnEBxPJIPn5wmX+Q2BzrQFfLYKdP4jDEEohYAyJdlYt6aseUZxVWc2uYhiUQzgBh2U0Hpx/vsl/O+XgNCPcYEMTggG+25F0YhoFEFGF2wgXEEz2LQQqjLykKy8FEXmGBnkoZ6kejMlCOAnGQMABaDpgbk5vGgXfRDpazcZAQ93Meh2d2xiHFNBELJKX8P/Cnv4Z3IU4kd1CFESkFr9jzF980IiarozqElrFUSk6JKm6sc0v4g24nihKWd5EYBhHuR1WX8Om6QvQTdrZzx32mKCx9irwCB2etjFyDlqNATHsjUvQa4YT9kl5iUPiCsxEqx8DUIHZGH3IMCk3Zi1094jcUvAu4VT1EkAQIPV4CwdFTBIh5LJUKp0QUN9a5JfrV/LDgXfDUTZnoCs7qEvL9vJwF6wHAfCbVjhzwA+oatTJUJARaB1DyABBOCDVtprPEKECAd2E5G0HjBs14HB6yVyl2i9qikU5UBKLkXbROtRAUCHA8opsDwd0oQMxjqdQ4JVEUPta4JSI4gngHBe+Cp+7pCPMuVF1CmQh6lny2EBrJQJDWRAaCEtbKiCGXgXLmQGg5FoiYZlzlbARRuVnwOAZiZ/QpdovawloyLgMx412MCoSuEeB4lEDI+A16dzePpVLllKjCxwq3RBgVbsm7EMd2MEFnMhNBzzpmc4QCCJovgyHUMBCLMiTuC4R9uJy5LJaWI0AEMzUZzkZgpRC/5HFw1BW1RJU6oMAkF5zqchjeRZqaGAhmrAjvBISjfNek8WJcJZZKhVMSkkjKCreEORQKRMG7kG7yDryLowhZOdEyWgJBTAfoXUSn6h6BPcm1MvwwBwLaGFmxT8sBc8NPucG7UIULsCyodfNjYGqwhobTmCbCHplSRrEGi3zJu8BizTFOIiYqKoVzOtaoSs8RGi/GVWKpLDklqrixzi1xv7rNnzGJMRGd5VHcp3KRR4S9zV3m1hvQ/LylQZOqkSrdMp5KXJRlH7ds6ujqv8tW12OpxJv0i3rJ3wbEV4uuveK4Vk6+hmq/rpVw77HHWh1X7fuLQPxKOdtLvw2Iv9DqDYgnafUGxJO0egPiSVq9AfEkrd6AeJJWb0A8Sas3IJ6k1VeL/nd1yR/omj9rzQbEX7cGb6aVPXHEV/2jz3wK+YJX3p6svMySD3/5PSS9hT0WL+uiqc7NXq/I579JAcCWt9DlqNQepenEq7BxTtor1pbbXM8jOndn3ljYFQ0QsKUoOdRzpTbC80pO+xSjnT1NiU8hDYtVQoQv/sUXqr7glOSeT57ywdqBd6BZdqFWbtkYb/IGE+dEPg0We321M2u9G2aklnUg5l2wDgRsKUpeBUJJDFEcMgaIqEAYtY3r14kC0TMQ/W8Fwv4IJs6JACH2PgCE+21AuPuAcA56GOyQOTD3wTOPIsq/Vm0jOo1XUmhrOChsJO8cA3EQIDITQ10lovox4gwipUAPRGO5IDUrewRNC87DQbQ5iGVBpU7uFeZH0DnwKqKJc+L1c9ukzSHi8OMyeooqftB2zGwOUc04FC1QZshBOC9JWyTtgYsCTx+sa02aZS6OX4HPtj2zJ1jKYKRfhxFHNK6P1/k/OVZVK0P1Bjg8BqU489R0RjQreGSPo3Ic8qfuOKPMCqeRWwZMFZhiqMTksMdo6xFbRb4252uOFT0CeBWtUQfBp+Vws6LMwWp6FNFT8FE7+x4T44IYFgin0AekiCiIWxSZ8ULMl156JaY9cFHIhQulEHE/93DaLnOlAFSIUwKnPaKUCMFFPMTyMXigxZudoRcnrAgFos3hN/oIT7AsQzFRa1pnVDjEo61+5JZlFCTqCKfDMShiYDlHKCmNzkClfgb94L29CK/CqIN45IhWUcMZ7Q4bPUXPytTER4lhgUgvOQepbWgUFfifyUrpFad7uTfFOqc9t8wV06Uoc1RiT6QoJQpEUtuAaJgs28J7GAAEMzT6ymI9Zo4TOA0S2GMGhHRvktuRMjB6VM1DxlENCInqIKVEjczCTn+rqGE0PYroKXMgYrrxyEDExAZRIJQDJq0axT43B4KkuGKKs1TmmgHR5kBi6RfHeJvFDVERIdXKUCCYZHAugYBqR2Z0ZIEgiJJohBVwKTA1gTkhjI8e64lR90AEraAqGeezd0oKy/Fc1F6ZmozYz/lsND2K6CkFEFofcSzCZ8G/YCaZE2YI80E43kpIF6HYp1FfQursNDVVcpnYeG2Kd3hMv+hfC4RZcs6qleEdFmCWmAKDgyawETS1zMQAN8RLfsRLJGZGSDFOQDwM8jTBy+LZyeKqizXxTCKfo7xMXBs9mCPnHM9F7VXLsjYHiyFVo6cgpos+VcTMsWDmSFb7QJQYMEMwvqE+gmVX7RMuypCmJrNY13Idx6zF8fCTdVweqYoyRvfYFq+UeK28NWvvZ1M8vsWVY37FGhtHxh5ZachXgajzFuIvNLRW4qNAfANj6GEgrvfdjR7Z3jU9iTUbEBsQGxAbEBsQGxAbEBsQGxAbEBsQGxD/251RWnLNru+2+U/W9fcr+o8DMY85ctu9Wx6+4L1tLM5YFoZ8o02RrQfeH/GFKjEeuM6ob1nJv9fOKs2v76SM1YYcyafSTP/x1kkH7E7LlPt3Otd0TVc7W99e316ntHsq8S2V41CKdvbHux7Fv3uta7LqFbmaVGBT7U1hSAy+4qh3S7kEVwbBSIHjjfPfF6C0LP+ARAGkl1HdRW5UpX5V7wdPY/6FHMpoZ5YYtQ4CokMDCY+PD+oi+n/qvNRy+vvaUcftTrsTzmLbVbq/SUd3ruvo7841jc1BpTipa1eUs+NfXFdDeKHAVEuzW7vaRcI/2HgjSyBq0UhC0s63o0C3g8t6FlrOwVFEEgDhowAxxGBjmJRAeCnjsADCWSBOrtMOROd0TXkJZiBe31/fu+b6OGgMTJR2NzuGUm4C0U0oNtURcagyJPi7/XMRb8TN45JUo5FMMIAv0bocp0VjpiSXfoC7RHQ9KEZKwC/4/6aOjshJ+h5H4XbAueT4U3L9FvuomhawSrRDyJKp6af9W9dNE8Hp44O6qGv2b9N/bv/2Ov37442miDwi5Cz+cz/e9j+nyYTT/eBjr5zzx1sGYj/1IJ2hnNPem44IqmvHZ6j78W8C4uS0pp+plp9UsjIWZgwJuC+LeCN80ZVxSZbRSMiP1TNfAjoemGB66Xry53EQDZmCjgwOx0iJ+AX/X08685zzU1QAAISWgjIwiaorElZDO4SAaLjD+PLrdETQf3R0Ota88Lyy73SNwFlN89J0kuNl+o/+RXkvTQZiN60WOceUQtYIjAgc5XHYAAiqS3NNVb6kWl64RLAO5gwJn6I7pHgjbq5bUZxNI8JJ4AohFUhJcJyWIwK+ZOr+aRIc9Bf1ro6IORCtqGnkESFAOBUkAWsDI6JRIGREvL7ToilX8jvD4Xad3sngrKbp0EUC3jRxOQG2AKJxdIb6f+red70zwohIQEx1AQiqi1asH1TLO8AD3GSTMhbmDIkhhdmQeCP5mky6FW4ZjaQEQuO0iL7HChAUwQSkHvrF427QK/8zFIv1qDcC14CIskZ0AoRZI94cTRdvDNEMCJydknGaafo4ZSA6twYEneleCIgu3aLORgR3tALB6WFZk2qhdG9uhSExpKlJ4o3wilDoVtSikaiWB/ErcpyWPKnISsB3TQoEB3dhIJwEVMt3TQyEKAvYqSlIFJOQrYc0UJqapvYCiKZxi6lpmhj4RAGEnZp4limONY0BoktATEf2P5FjBQiehBSIE91D0DT4ggmJa9GpqcqQiBwfZcjxRkSXwupWDMtoJK3EZyF+RY7TosssgonpcwQW6+lYpNBg8hzBehx4jjiy+sYRtQ8uUy8HlMErTFqsodaB6Cx4jjjRQvlz96bPEVgkaaHevXHnvZVA5MX6lRdgJ0sxzuw4J3U3niOmv29YrF2DUu1DGxbrV5Tx85V/JSAaXqp/plp+ptxrEUf87Amv/HU7GslXGBT+5tF7hBcfebL+eK8dv3U7W0/58f7Yk/VKLfGuiuPdFf1OBsX3AVE/vn+7t64y5WN13V/LLzT/b27bS78NiA2IDYg/X9EGxNeN2IDYgNiA2ID4S/2zAbEB8b8ExLPTa6x9z2vrN1i2AfHdVla37tp+Q0yEVESjJ+mfpsxY5yY06d+mVkcuY18W1sEXTC5IvM+ktzPwDeu/rtudXt/p/TS75qXcrDCO2Niyrb+ELM5w+ngc4N5d6AyE67kXJc2BUO3z5cdtTRWIXZFgZ4E4ZTy0ExvzT3MFiF2lzgzErkzeMTANnd+LN4DdjI7f8yP/G7m5CDJ2zTvlbWhzvVsCUREVKWK4cHrWFnes8a9n6kD4xZFSniIUWbJl4V4g3FeA6KpAdOnfxj0ExA/ORQD8yEB0FogduVkUCLGLf0eVUL8PCLcEgp1Oh5tAuEeBEMtmQPx4U1YDDfdT84N/8/+Ojr2DcfBGjQOfYXK2SK4fb5gktIxp6jgpP4Ly7N/eUfIJaaZ/f+5Neir/R3KpgO+wT+fg+522kzoo+e39dBlQPtf84PzkhOP9E/mpM2+DvvEFEKoaAlbJYWxTlBWwS0iDA/FPDmOOgZI/uFcJl7pGB8eJOat7KvFJTBwVuMOoftrLltkoK1Pql4Z4UuAndM2JOQsNsxeIZeDcPw079hr2/DaZf0C5XvjSZadj0yQ/LPEjJu/TC5Ej/pGSUOZO3IK2zkYJVyl/x+dA3iLOgwIhPq9u98b1dsSvmOrpUA51fmN4G97EfWEWSUTsGI6nAi0NOFr5Q3dyC0NLI6SoL+TtO0qIm3WNDnyaL/FRsk8/xVFxKQYGotVky7Q8KHOQ4+6dneAMxD/MWWh4RoYf9h/hMTAQhn/AuRqdb2iPGBDIyf00pXh9/0eoKHueyBphLNg6G0NMpFrAmOiERMe+XmdHxLRw85Kv/xIQrtERkXkbmdIg/JSo3nkwUMA+OV7aCwNBmgcaU8aJXoiqPg1poqlodLTCUmGOy5T3eJGANpKjvSQgmIeSLbPlTSdfcAUaIHgif+vsiGDCCF9x6I6T5GqYuCUdRwwI+v9FgKBcmpuZE8JooBS5TqpLkHgDG0LL06mpBKKRey/9lzl6uoadMm8jJgWoORDCQEE0FpF+YCrCiNGS9EIECLNGVDQ6JE4MjjPPsS+AyOF5mHEyZsuK8sI0R09jvNGF8OdPnRqoy2juLaYmB/4BzdLIJVOT0zLINf7jjZCgdAIsH6W8J5macp1gQkyj4VRMTY0dETQb7k8KBC6FvVBSgHinQHSZtzHIhYwpR4DoNQSVc6oEEvs8NZHGkipx0F0TaG8JiKpGR56aRF8jKMtFcvQiUiTxnLJlIWuMUP7OEdMB/IQ9z7h7Zid0NP/SiAfnYJqdCQjwD+i+hfkRDRbZRsqYFnvKySxpSkeLP5V0Qho6y4yFXOf+1LDfdi+LNZZ8OgcgaNmfrvDTNG3hZoAmQCZF4l88R+w5FT1HZN4GNRdLooQmOyN2DHMER0RZYX2N8xAQRYV47O3Za9QXeo7g1SHFsKtqdEA1JC/iUB8xcVTOzHkfNVpNtkzLgzLH4rZ19Ra2eCa8P9f1FLs783R31PHrT9bRfcd2vZRVy/4uEN+5/cuB+FpDn3HbXvptQGxAbEBsQGxAbEBsQHyzlX+viA2IDYhntPL/27ueHLdxp8sQboDQygNM9kSvBJ+iP2AOkEXmPkKvfAwtjTnlp6pXxSpSlP90upNOflKQxJYosshnkbbq6b1fV8UOxOeI7PeQW/m9xGD+J4C4/vTS9pNLt0v0Sm48vE9b7WZCdxFPEz8MPHWfBeoI8Oa2i5w4dxwLJ0BLihynjuVycSVZMy1W7Q2NO8qgfcjtme35paTnWUzd0ZtC5YLRGYGmxHocJcrkSyaHzIZbihxM/MBnwLOkaxRVHiKugIibQORqINWOvg4lpTUQaQOIxsPEsglTH4hYlYwtENtb2trbEzCJrgAiTL6TaV3ldSBCcDmlzgmNYEoOnSuCGQzGZLgLiPAQEOERIDolXXRXtnwTiNwHomJ1JFeSkiWinCGi4cQpYEWL4nwCjgKAIHaCZmvBV1B3lSW9kTlNEsyDxFxXaI8yGIzJkEn2gdrjx9YDA3ES7xLEAnl05kucpbbZMSpctPBhoSgvZa8qKETnAvPsYy6OJ1FVQ84WXV3/ICkmA9YfBUtkkBGxEvCpYWIOOCIqi6+C9fohySM9zRxDSj6VqJOP6hGocoYyFwATsrPFXQXuJZKcBF+CGAow+0jsM4IJwJgMpwmPzLOvyURZMZ/pzeJ3EpDcRBoz5+QYFS7akScUivIVjiVZ/C+SksAQp3InEpeH4wlKQsvDoqvrt7EIvaNgiciI2GQ/qleM8EeiPG1e3Gm0JBWB0IL6ghDLgRUtSuKd7clkaELSAYriD2I+ITnDr8v4EsxQyGq8oQwGYzLkAMcTXBHkm2XeJfAoCeJiUlrJ8EAR5oSLltrCFfHKvIjlk3pxV8TF4jzJazqOllESygUWXV3/ILQBm1Oq1jOcUpJ8ATH1kpyK2wwDkaSPGO8yTRkQxReE1qoJMy4+BexncgWIcQMI0ciYDAjz88oCRGLHE74WCzMuM8eCYxELDQMiCVgyJC7anNTn6rXwNqCyE8DJsDhLbbFoeSgQw2TR1fUvr0cFIq+O8hUhLBC3EukVMbZAqMdKqYuKsDNJcFMTVV6mJpnBMRhyGRYgQEuckzNWUiDEwmVUIGLhVhiTgYGgz+a5uJYJuyEVwAkmbnsUIFLFgCjRUlsFiJGBGHnmPzMQxQWmTE3C3dAzVb7FomvrpyR/Tm5a90czcUAwffqJH2uEus2kVIAoU5N+xz3DmQSMBixASRa3k7ASbLHGLwj5l/UveKHC4nOGMsdJeBDP0OU4w3UlsX0OGAyOY0GmTjO+bNDFyhPiDO8St1ifeaI6w8OF1ydlVLhowcyDGxc4G/QeTi8hmwuMLtaJuRvieMIlsfpZdHX9y9+EVnXwqqOkZHIGC8SVODPpbMY8gtFpF+sp3LnldyiBRWn7l/WH+p6kR+IU26jV9t/lWpR8dGNsMBF675SOj8rPBOLaLY6PVO2wuvNdpbdvwFyP8hoQv06V5EEgPmOUf/C2A7ED8YYodyA+SZQ7EJ8kyh2ITxLlDsQniXIH4pNEuQPxSaLcw/tDInuXuG5ruu5A/Jy4pk61LqWeN07DXbHizmy7mi3Xx6/c8htciaFbZ5Psd/f485UO5tW/sd96t4v/XXKzb1jVfhcJ4Z5rwO9PV4FITem4OpiqfckSL1DBSFfjSlIqxKZB3NtfA+GTMy03IpYItoBI10dFEqHZH0t1z9MKiHh1jB/YUsNteByI0ABx7xURXYm4alCA8KyLG0CEdwMidIAINRC32SDWyrMoSAwBbAR6bNX7pwzCPjBuQ+EszFMCB8KpWkyoGCyOLW4HlxfHFVbBOMtEMGs8yq2gfHcpNaN1nEdtD5xy8pHhmKlcEOVhKHwPZoYEU8kIRadDW2w1OqKky1RJQ5M51MOBy1+kj5IeCsYxsbguXHawPhd+y6V400jSkbO3nACdoAzBwIIHAfaB4zZQtmmCb8ooSdSiaiFAjFe5HcTBUMcVTleOsq6UBKsmSpl4MKKUXOYJmhqTpB29uoUeM5ULpQsg255KbFaCY5y0xaY3Qe1IKCONvcq8UG7Gq/QxmnCE1G5xia2J9TnpQ/ivSb1pwN7glCXYCGBA8OfzAh4EsTiSY14QH+IUYNcB3odTtRAgrnE7LG2PoGLJfIFpQQlTiPUIEKqVcUHrqOdVEvGni4+sZLmFqQH3F7h9gQuiQ2VcjuVVUH2OVqPDgJCaLkqWgXqHERXYkCREsb+i2i2uVy47WG0B3BgCQr1plE9AnWI2QtGYYEaDDNOcPPMiwVtIgfCqFrpGXOV2FCDYHMQBIZ9i8WtpgYB1h9p5KBC5isyAyHgP+yK62uZipVOVSHD5En2O0Gh0lCizqnR4IKi8AZEx289au8X1irJzqQ31zHxFGOtPpyawEc5nEb0hbQnhOdDl45gXo7J4lrKNqkUsQiRb3A4hZaUiJSJTk9b8PHt6mZ+axOFlBPvhfAaPNFeR4ZhyPMY0nL3iRbLJo5RI4sKSQpmanEZHiTILZCOiiOUD1J2aUq26YVOTl0EpU5NO+INQHIULkVkZIoi2xBnMhui4DUoNocUGHAhTtYD3yUkolD1uB2rLhS/IKhi6WDPTYhAlCyyNJ1nST/yaeRHQ1MikuhTnoYoMx5TjQbZq0RZrVsVAbFYiwU1GWmw1OspSf2bSD3ozYcBBrqQvN+CLKO9Fa7e4pqQ0UuOesMvMeUrqTdNsNb/Asxbu/Z1oX9fyzbI9VsTts9rtWmTvwQ7JV6nZj8bVi+hmjZ578PgP9hweqf/+sx4B4j3YE2+tox/XD0e032v6JHHtQOxA7ED8Dt3dgdiB2IHYgdiB2IHYgfjk3f2fA+IPDuyTx7YD8Qu2LZWLhp2Qm9Ny88r/2yXkVPQD1KzaGUkf2NyIcPu4RVjrcPS0D3wJcs2O/+FOaJdxUYkf5M5IrUclX4nu2pa3gdCcv5cvyMGbVPjTt4Hoe4mEwoJQ7Yw4kdl4GbAAjkbR+0h6fD0SpvCSmqHMLhrJ/qZo0VO2LGfJHTwCRHSMh1T5e+dVfHH1Pn0CIIqXiANCjuKJ0t5IIBI9vg3E+lheddFHz1lrwmLJH3cZF1euiB4Q4S4gwhYQa4+QZ0ligLGgehzeH4TSGSdhInhljOucCO8lMuDp49mUNugT7xgcQTgaQSPBFeHrww1lPPALjkQKQzDOhvZG9BT0UeHiqfJcgHhOFttz8UVBukpfiYoH5NhFxqHu+8BqIaoh0uqZBFUUkfOUAwPWDGWyVx4h5Ql6sXYR9QHnDzIV5xHNEd/DifBeIqEoVLC3ScIaoQwOyhaOSaYmiQRrhK9PM+tRvEzoee7oOBvGGsGEgNaQXuVrkjKRmVaIfHaxSRozCV9DmSHwPUEa2R5at75zMhjZ2K6eSShOLSqbAQ7MhLKTMCe8RwgYBqrNEauUPJgQr8V5BAyJ+zgR3kuE2BDgPajAgyyfwuBATpgfJr9oJKGpb5CLmsqAI8GSIxwR8y2KAga6j0jxCkoGBAQ+AT62E5+p4ibKDIHvCXRJaiDQd6ZHSO67p2fSRqLUC2StT6EEax4hYBh4ixe9IjStj5NFX2m+lxPReImkqMobBQhjcBgQzuMk1PVx/VwjxRtmKc/tZlECCeW6ThIpXhXtGwBRxxY8EH4YMTJrILJAPEzSl56eiQMC2iYeiOWKAEei9ghRpoVNTTSbmT8IMT30Er6fE1F5iYw6NXkgjMEhQLCciEaCYbH6HB1nVEEUxxUZjckRhO+hQBQCW87lW1OJTfu/1Mr1j0WjZMw2NUmvre/4EBg1rtUzcUBIhODAnJmWkLMSGp1HCPMLmB8B+iL0OLw/CDE9ojIR5ns5Ed5LBMuaLtZlaioMjhPzPJj7cY4mMVXVR8uzOZoMrHLhCJZnVQQJWBukNaiOcC8Seabo7wiNTVktiWmejjWi+h4zaoNmiPUdbZM6SV/PBMJbFolyYGhJT/eTHHL48e32r9dHW7mv/D2d3GBcPBrPA1aZb9w+JxD3bffwJt7nFsd2Sx/Tsw8DYo9tB2IHYo9t7+xvEtsOxA7EHtsOxA7EHtve2c8b268RL9klU3Yg3hDlY9sDmgNZX6Se4QY20rLvnZi7NeHeZCeoat9Qml232iheVG0Mpa2hUeoY1syJ1Ovl9lba7eTy/Zi07QzbI7seudSNON0AQhKV2SgBBZ7Jp8+NUpBgL9ATgEieNmAcjHWrsctu8NSDHIpIhKspbgCRGiBSHy1rN8VVv1qFjXj1cx9Lm2teSjfiFRCN84lmjDsfqOtAhO0Od4BoWv31QIQeEIXvsabKhM4HP6/a6AJhDA3Tmlg7n4DMktV5xWlrRHM3ORu3g86AGINqYIAHMaWaSWEcjDCtW2XFi7nVx/AcELioIF2jNalnxFD8U6B/QVL0YJmoIkbNCNGkkmv3PEgt2q/nSvmDWwwXidE7p0xJHv3l0cH00/JS6ogluZeC15pYO5/oFRHVVKBoa0RzN5GHwonvsOTBMvJ7IWkqFOodnknBevvCwSAg2lZj8U3x+hieAzImTZVqUlJdVKjuUTUvkOrMiFS8T1aMkOR0BaTdUXpZOCvJcVEQewyvEmNJ/nI/oWuC0dE6a15KHbEwLpA5Vq2JtfOJXhHivOKyw9E4HEUmgdU6LkHcUJR0BdGIhklROBgERNtqLL4pXh/Dc0D0ef4hqEqAuajAOwV/FQg1+Gg1RJSLYgIP2t9BuCvgrKTKDQb6IK/iOJNdFh77xCDFiUbUvBQfsbAmUvBaE2vnE3dFBDAtFAheI5jHAA6D1gGRCRzrASFMCsfBWLcai2+K18fwHJDS/VlLm4sKf4Y9m6MBomWE1EBk/czPo+OsVJokMriv4jiTxTkllRr7QBgvxUcsHIqUvNbE2vnEvjWl4gxSgFD2hzisiB4G6/UnnjaS8C2IudAyKYyDsW7VOBleH8NzQPJqajIXFXjBZD81iTiP7vGMEOVjRN8uT03klFJoZ94NJkEf5DVZJMMZQChDQzkbfV6Kj1g4FHH2WhNr5xP9HSHOK05bY/kG4RQmhNsBsuIENxTeG8AOAbXQmBTGwUhh3SpPOnOrj+E5IDlpDbrYmosKvGCgeaFaHCLllbDHM0LUyUWVNopOCNVSOCvJRYeWT7IwD8w7kem4WKKdWHeEx67DS6ki7vzgeuNv3GtsiXzH+e/5y7qyUXpoizdivx7dI9ofd/iqvG1IflRl4j2BQCw5vMf2CBDv7Jyy32u6HeUfPCQ7EDsQb4hyB2IH4s9s9a1R7kDsQPyZrb41yh2IHYg/s9W3RrmH9BvH9nBU8WOq/a2BiA/u/4Go+G4oXk53VvugY8hQPbJvPIy33RXynAnlXeA+7zq2zVZuuFe4CrbGZApNK/0o09URq5gtKW3hGx8AInVeuXocEJb+T50upE4EaTMqTfcrQ8QzQe4DojYj0SR/7Ax3v2+9XqTgCQJbIxaDKZKsTrvziug4hnwMEOFeIELsxvYIECF02CfTOwGx4bEiQg3ISKfCqSiXlWpKFCcQYkCojkSjX8HOKcp3aJkX/OiwuaNAXUM8Uyo9i0493tdFYwD3ASXRzWd5fBceKI5rEdYqIV5vYyiMDm1/KLF7roqxQtq+BfGiKWyWUvtpdlocUwj1iBXNEn4s2qUyKL9UEqAs+6GaEs4JJKeiI9HoV4zIBVsS1DMvpsqCAGwRSRROXs9iXU/t66IxmAdMzbsghgid4ZkgrUrIkh90ehvyMH1JGMOAgIkbFVdFcuBrD5kQxItGexGk9rE8us9nTcHxUzRli+WAFFAq0hjxCYZgcj2qKWHuJxnZUSagNPoVibQnoOEBPQrPvHhN3pQDbBGkzk/B61l06ql8XTSGoXjADDo1FUUMOsMzQZCm9SohXm8DfIrB9tN1y7HTPuOqGCuk7RtfEa4XrALCPTEgsgBRGCBUUzbdESjTlKkJrAU304mmhLmf1EBU+hX0SQ7Q8IAehWde1EBkZ2uBK0L1LNb11L4uJYa5eMDMCkSWNYLO8EwQcCy8SojX28hFRQf7B5o6EHvyXBVjhbR9W86bakUSeNTUVwTWCOf9wtE7IFRcAYwLm5pCME0J5wQiQBBLodavoLldNTxQxjMvqLyfmsAWAbnF61l06ql8XTwtBh4wNe+Ch27MDROkVQnxehvC6BhTIf9EjX30XBVjhbR9C6K6UdgsI2pfZn6nxUGSKZ4BstQx2TVTfYtOwlqoF2uW3jGexhkqG9SlWr8iJCzCsfiheOYFL6vFHcU8SIbK36RbT+XrojGYB0zNu2BFjLN3MznNa5WQSm9DYoqzEUQl9rPnqhgrpO1bENWNwmbBlxG6ZmfT4qDp1KLi3gTTHUn38w7W5Zrfie/gXdKv51aEHd5Fevsv69tclDvYF660vc7Xf1nfyztYl/tZQDzOjGAzvjdu11rLj0G6ev0/p438x9z0+507uwOxA7EDsQOxA7EDsQPxyTu7A7EDsQPxxwHxsYSW34Mu85EQ9HreHY0diM8AxBUNjmHedi0J/BRdt/Xm3u669lt3kbbdSPJqT9yKLoTejcRea7PWFKfl3uhkKiND0xtokiCBJsfyVqBJ+Bsn/tffUuRTLhiNm0DYY70apClpODWO1GNZ1ED0tDZy2rpT2JdYsIq3gLgSyeb5qfl/CMjGQuxdoKl7M+lARnVnn9qaJPYpKm1gaoFIblTWQPQ1ODquJbUaR7gPiKb2fOOWbdzYdwWI7UjuBuJZ0kYxV+4wDRCxPjZtjU0NxJjqiHU0ArgJz7c0OMLatcQzHCqfksqRhJIkF8iRd7Q2cqjVMAbjQTBvRPU2iLeh9VIayPgSz7PnVoQrkTxLK2sHGE2GpTJAsAFJxRmGstiaQlIglgQuxwnWiAmyF46H9CBMShugf5+TaZUweDIqQdJ+8gD2pgZHaF1LaoaDV9eoHUnEDoFq6WhtUIaq8kdJKukP3gg0NUZ7VF0So0X9o6htaIrLO6bUkYzSytoBJpU0qwMiI7E6SjSxJFXtilhWCtH9QCo3FK+UxFMkeqBA0ES3TE1nxKuLxCCjEaDFodoQmxocjWtJbhgOXl2jdiQhtgIBsSx9Ha0N5qw4NQyoVARuBzlu0duQLLUyTMzBRdU21MHlWiTUSs8BxqgLBkScGAhhfYnMSvF10TUCKhqnyh1GOR7ag/qKQD5beDJyRcgSwTwN0YbY1OBYuZbUDAevrlE7kohTywJET2sDQJgaBlQqlm0GFwPz8Fh0ZZRhYnwJVdtQB5erkdAVUTnAjJtACNEGZ4ZYVDNqIORaof1TDgoEOB7CfFkt1lkM9QjeUEZDvrkkubw2NTjWriUVw8Gra3iGhJ+aerW3ahhLrUGnJuVTYI1IQXU1WFiieL/oXnVwuRKJtOL5Fs9zKvohOBd/mNMmHnX11CSTqizWeM9T1qQSStorYb4wEAOvBEzyKS409q1JRoPnaRAbNzU4Oq4lFcPBq2t4hgQW64m9SHpaG60aBvgZaE9UNvgrJO3P5euC40sUtQ11cNmOhIg6U+MAk0EzNRcWcliZlnVmWj6vExy5EI30/ywldLFmFQ1exImlMqlXSpyTMl/4d8TAvyMGviIGadt+R0wtsTZ+sl/WOhFk9++WkOPbtrz6Chxv0LCtRE+/jzbP8bAeUJ/z73qLQ3/p5u6ghSt73wrEe29252C/1/RJth2IHYifXfsOxIPB7ED0R2UHYgdiByLsQOxAhB2I/qjsQHxGID46kF/T/c8Ji/+7A/GZgOCHsmdfbGj+t6N5XecqTTtsNN3uz/dGfUWWYLkvOnFdqR/Ycs9zqttc80bcHk1MNBGW/ld9zKEwOG6I/fo6cTeW7sXaX4nAfE90i2IlX9xEiqbFFhCxIgm4Mx0QMdQ5/FxK+7LdEZ3q/dEZc4QVEMnFsdx2zrUnik+JlpFxLdGgKakil/633dGxiDeAiAWI0vsJyWLkqvFXuCzr0PSkGLxAyL1XROwc3AYivAGIUDyFFIixC0Ss6Cs9IKLzbLkCRDke+0DkLSDqK2ITCEGS0yqqaiEOJZz80cfchRkRlNOgehyFqTEP4mniXUYknSRpEAJiKFLklLMVXkZQbYzT3GpcaAux3S/PhudJgTBuBBgarI0hGbHaE4V1DQpvhJI8OYjeCDgXmgaaTbVDR4fSQvSgMtxcZKRoBCbaB4UQ0igxNQ4dM+htYEzDBBqF/QWXRVTcVdUCDiVRVAjAmSBmBGwCxuT1ODhjPFoiFbCqy4jQx5I6UsSGTkYMCmhsBKez4TUutIXY7Mdj8x4Iz43gkkzfwRXReqIoEeFVekoflSRWBpZHD6K6Qa3q6OA4+bFE9nSRhC7FRx4tExxjZIxULSSZn4ooOzAQYba/4LKEZ9HbGJxDCXxTYtCuxeItYuoDA3QzknIuwEwwlxFMHaS5McjUBK6Fzc3Q/lCux1q/Q1uIzX6Ny6Ymz43gDHwGawhAeE8UAYLpCsKlSOg/uBhgksAHBtOYjU4uAiis95FohKC4QftOBV5EPigXpbhHeCD8FSHQQ59iyZYzQ8JdEeWTp0CE2QNB5dWZpDifeJcR6GnQNSNTk/H6jMpi6hxr/Y7SwlTvVyDy2RZr5UYoEFkX69YTxQGRyxSeWBcDwiWFTKFMpTI6Hgi0F8Vnha+YyYDgsZxlXCogemuEApEVU2FMAAjxB4kOCOU0qB6HeZVgaoJsirqMgM2QRyIORJ2aUjU1icaG19nwGhelhaneTy1gtiYg1KvFGB5JP7UTvjXVnigqMqRTk+qNoCWocoh8UCqWP9XUhD7a1DTheIZjTG6mJjC/orBFNr41ofNFk2LGN21arCKWT1mssY4op0H1ONQ7pXA8gncZAZthOU7MiaU5KFwYEKyOAY2NyXQ2vMZFaSHX+7m1IHZrU/FqScrwQG+EOzG1nijiZ8I+KMzJEL0RcDGgyhG5P1ncUVKR+qLRoA/WiT1d1MZs4n38QQC5tCzWZcwm9hebldXR/o6I8w26en5IOrH/BQ6fCPuNu1ayiN0jW4oX/13qtvJ29KXVra+WtzgbW5E+urXn3XGLow75PfxBUMevvcWRw+fa9ntNOxA7EDsQOxA7EDsQv7TVHYhP0uoOxCdpdQfik7S6A/FJWv1gID5Hp351FPe2f7vcm3uyA/FTgFBhduVaTKnhbhTNCPUu2dpy5/WwWdRK+zKpU1cJP20pa2R/6vXokkridsdiGqrW/T3jho2y1qhdD7MlSuy0nKxETT3AC7sbObncQa0ZYen2eAcQygfx1IFS8eSZMp7PoXIO9dHogKh5Jj0gvCREeggIZX7oUZ9FaUgQymGpKAseCDg2tGXxjK3VkVogtMMx5JyaQVDNiBgeuyJS2NTJuALEluJGUa7oMEJy5zP6JiBSA0S4DkQINYnnGhBhGwgwGighdJkHeUx7mJYsuDAQWs0I9S5ZEivBfFBU5YJYDIN/LXyQQTgcnMyR49FpYrBrwxxE5QN5t0pxg84M6uLCWeFZ3VIGYXiAidHzYDE2yXPxUVFPk9ZtBVcEmB0Ni2P2bBTUo0yUE0aK3SpoHw0z+lq8WKqyPJ5BeTDM4gA3gnQlXiV1iEdYWcO9oxmh3iVTecDbq1xMRTMDr5UPohwOepI8OemI4qRi3hJUbqoVN/jhevZ1sShU8F8VOSRrzO4orQeLsUko1pETofSKziCtj5RMeYPWCGFpTA2Lo2KjgGGChOmY/NQ0JojEIwkd+2UZiCA8GOqpcCNIV0KB4OtlSsa6kCsilc4T9q8JXI9a5QKAgBHyKkoF0LIQP5QMvY1BBlN9UFR3Ax4uKrpjihlRfF0sCvwL8iPkGk5B3VFqDxbPJqFYESG9OjkaA3gc5YoglkaoWRw1GwUME81MeyAoGgBxuqiiyaosroiLZcYLoyHrFQHOUFZHk1YzQhkbACKHWuVCrow5BwMCWhbQxxgm0duYZY1QJ5Usaw87LZTFWhQ3OKc8AQiNQtaeOSiTgvkoIn9SebB4NkmRYJjlGgpFkWMqPI3k3Y8cx2X2bJSsvKsyuJiaMD48zHBMmXplsUbk5IGARkWgf/mTe6Z/J5EL8d+aIFWgzITzORZZHFO5OJ9NbeJ8Vn0KZTFwSKNqX1AjJRA/NTEDwytuQIgFQGgUKdnUpPwMdUdpPViUTYJY5a/YShVFjuLOAucX8XrxLI6KjYIawHoxforyQgAY7596ZaUmkR7iKR7cCOI5TOkkzI1hIiCG2XFALx4I5ntkqF7UKhcUsi5sTLsSPohyOIxFEVgLsDipnE/FOox5GMErbkD/g1YtjeI0q1tJEDUO5Wj0PFiUTYJYcwJHg1/NRZED9dCXBGJ+nPUj4lgcFRtFahDdEP2tcRIVkeX3xoS+iBdLU5Z+RywlzuDBxLnDZ8fnN/S+OL7Ddrd/y8aZ90XRa2W7Zd/WLSWOtv28URJXzrWy+CJM2wZfZYu58T5AvJ0X8ggbpNfKdsv5gSjuB2LdYr5a05tD+DXbftNvB2IHYgfiUw3BDsQnGYIdiE8yBDsQn2QIdiA+yRDsQHySIdiBeHP3Hznr/sBbGkqflnKtvvcE87dgJe1APL5lL9xQH8EzX+AyDKvyGzfJyln6/lrjqepQ9rttu9jA67Nnq/eH5Q9vR5Xl+U7/PkkVT/zq6TtKfv3GOw/bcR07r9qjJe6DFqL/DvUJh84re11FMPUGtZIsYEWOqKMz1UDEzbO0/jV1IFVno2xaAWFyFO4K0Ccy2/dHdIrw+PdfGowvXOKLVPiFX309AogjBuDwpTtKfvi/bADxpQbixfBQIA7uv8NVIA4PABHCJhDhTiAclI0ixhYQ4REgXjBih2WIbgBxDEcFItwE4s4rYhuIYxeIY6iPEpMgSk6Yk/thcL4gRbKAFTligKIFpBMABJcVNsVlfkbqo5x1kfKipsES6K3rCFQzLkUvY5v1gK6XJ/ab98swvzx9Ox6XSefl33+/fqehf/r29I2GH/8XIF7Cy/FA+57+eQp/Lf9Sia/f/qI9375/o3fHI94t/1KJ5X/a+3LAa93/778o9fTPFy5FdVDLKPOVj1FUy146/x/Uz7W9oEx4QUlmGNRAhOR9QXRIT3MslgDK9tBkpaYsX1OjOEEPvqcxxVJb7LiOQDXjtehl9FgPg78iVMeieU9XwvHAH/WjXhHLe558lp0HAPF0JMi+hK/faN/x7y/h7+UcKrG8pQnr8H9UwbJv2b/8ORy4xPI/7X054PVx+Z/2L+0crJ6lAvqX2+PqpH0qfaAW/6Yp8f/kbNQjtQfxA6muCOYrqC+IXRGq0YFXSipQNg/V9JryCojhYmoamGha1xGoZrwWvYxt1sOtK+KgQJQrogCxLNIA4guBtJT8K3z9/vQdw0RAPPHaQvsIiKdl7lquGV52qMTX7wceXhpEer0c4TOpLqvn6TtFgPlmKfOdjwWUxvlP379+/z8AzC0eOWauPYtCRAKXaArwThFdDL9GqEYH9mZZI3IwNoVcEaOd9QqVF1HTMCC86wgW69eil7HNeri1RhwFCFsjDIjjwYA40nuUxgDS/P7twANz5CsifCMgln8FCHwPABD8+ttRr4iAesILPtk0rEEGmFeMb/T3bwGCzgYQR27xiLOpxpxqIFTyRxQiJrVnEiAKAawQEwsbiqam5zlROryc9crsBVXT0Bpq1xGoZrwm43dssR5qHYv2/TK2AOJwWAPBk4cC8ULr+pH20V4eFJ4o+Ir6u5maQpk8lr3//HOU4aZ6aY3AOcuks8z1yynybYzL8LtlOlve/fWNp7alnIDJe+lcjpmaZrWIrAYVNCgQwRKFCPlFMBDhTDQ6Ev+OsMXaFmBigyy/N5KdxW4qoahpTD3XEahmUMlY9D5WDIkLsx54WS8aFs37sHSKl81v+jsCizUvk8v+r/yqAHHgpfofWiueuMSX5RNK+14OKI9lmOb2v/h/LOFUmsotn3xuB+csy/0yv3HbDASXWd5RDRTV8mUXi/U/tODT2S8o8w2RfPm2+lq29gXxjIPe5s/IwfgSelauvqi2tWMq6v1CzaGNQSc8/G3f16W3410fO175WXdtO1Z1Ha+WfJft/h/yNHjKXrhPCWObnZG7MXzELY6nb38cEG89a7/XtAOxA7EDsQOxA7EDsQPxIYO3A7EDsQPxUXXtQPw+QLxPoJ856N9mQHcgPnxrCAfl7VOzf8lpfdekI91xpxvpdI9m2fuCJMySBPn25QWv6A/dEeZEvbQj8g0wXy9bq9Mh712JvIqZ9+TTxI9tb5aKvXpXtRcg0tqS4/o5+YFRzpvvcw3EYQXEl2Y/ZasIHElqLGmNgFu+B05w0TG6r865riNTKQg8TtQHZVYYEMbfaJ/Yi2qPYYmk1VjwngkP3ba6HWkFRMd/OrWAWWx9WYptIHr0CS9nkTpApB8B4q+l5F8OiC/HL2BCHPH+L7w60KsjUmIA4gX1fFEdjVx3KvY/x/cAkWLOK92OtFFzvHVFSGy3gYjtEK6ACG8BAukPSpkwpwDTEnMenoSHAF4C5aO+AJO/idFwkPQw8SHo/Rd+RXuoJPEkAueOvwRjVrCOxsQP4ELIPDDDI5jmxrPodkChgvaaXgeliaakKhmcuyNvl0a3o/Zkgd+L+rKgFtROtVzAGQnDbLF5BRBkHsX75QxFDuKihGAaHvCEwZFnlWYv7jPmzjKUx5d9G/Bh4dKaEAxB03yh5HKRzOODnDAk7sOyMvxDTIUlV3VkAF+Ik8CsoW/IW3FJ5JxeJJUozArNA+rj6HBqUXUP4nuMaiUwanZbFTmgrDHxQ+ykCQDiwZhWuh2VJwtSuWPR+sAxqp32vTJnBA/Sx3JtaU2juFuIVseIx95hfRVS7QmDHDs0QZACZl0P584iD/kn3yf0QB7Mp+F+4ino4MiJtDB//a4pcJmqOPu7LMdMbSQg8Pk/8vsvuEjkldAf+YowZoUuEeJzUiRYoLkxXNTPBaoYCoQqchB/BBSDU1AgYL5hpVpPFvV7Ua0PHNNBeBVxFkrV2vcIbc9sRpJYgOSiTzJcGk8Y0T8gTZBU1Bm8Owu0PMCQsWhZIACEDB68bwAiaKaKcqzMMAAQmJoAxAHDzN+MbgGBNcKYFbpEiD9L0CtCNTfKFRFqIHCUwhWux4Q1Qu1onIVc48kSxO+FlTxkdi5uSuWKIFaULV+lvQRlktEBod/SDKAaCJQw0xNzZ8lgujBDxqLlj9JcgACNCpwcopa4qelgV4QSU2hRfnpR2iKxq+i9lC/ciaMCcTRmhUzDbmoSliHCHp/n8l4VKoJndiR2LEmgwdG3pjyKToLT7ag8WYrEiTBNcIxqpzXilT2yTONjCnV7MWA1kbMwNbFOSAFobPRAZPLSHpk7i+5p+qRTUwCbYZnRn14OnLt90t8K32T/NwDxVZb05dfCy3Kk/I5YPuv8Xsq/BHmFY/Q7wpgV2lkobzBHo6hXsFdJwHssp+B7qCIHlDVowRRmFP+OYF+Wyet21J4sQZggoHNCW2P5qnBm75gzxMVOLHSlsVlNpCQCrTM9C+4rcHPxnjDq/gJNkGhOKs6dRRbrs+/ToMRS76jypfo6e2s73njvvhZ2X4fuV8D8JsuNe3xO8jVBxrD9yzqnaz/GfqPtZwBxz7ZW4rgPCKjX/AHbfq9pB+LNce5AfJI4dyA+SZw7EJ8kzh2ITxLnDsQniXMH4pPEuQPxSeLcA/vtY/uAyD6+s3Hj9c+PLb4pig+ILPerTOZdkrfKpo07MalXvuFrTJPdiJv6pXJbR1rHtunX0mz9Uvx83tREYTXPed3rSjohVzVbZPnWeKfO5+F+IOKDQKSOyrB97qbUjyc+CMSt5/tSt5RQE9J29OZKcw0IH28LRN3jikggSiPeF6ZLC3GMix+4ItJVued57ANxxxWR/Y3aW1PKVSDCVSDCbSBCDYRElrvVtkA0BK9nx02ArwgbUSjjwjENtKyyFpAeUf7Cs0izq2NJ44nCihzKqaCWl2xb1hQKJgL4rAwBTifgazzP9g5+KRbbc1H60OQMter9UaDloaWGUhe1HIoMvTI7tJXirRJ8BDWbg8rDM8b4IT4yx/AoCSrH6IDzi/jCcGviPALBfk7Hp+RZDZ5poGWVtYCEYRD+gjATMhxLUqoZEerKIPobgVUJ2DYh5TIRJHU4KmdRbZNyPOCX4hgXCVwLVQ8JnIMek/YDvVP9EOZvZE1RgsRwmrVl8c2Q1GaSz7uPoGZzUPmpsE6C+KhoZJ7hIXLtNaNjRBI3CQJBHVLwyViS8ewCos4lkCMxpoGVHUQvwzxKTupZkvHOMyK4LAQfVH+jAHG6xEmvTQOCeRTywTghOXlRZoRnXIA+ILwM/T9pP4T9JPohzN/IwtDIxfSm9EaAcONRR1CzOaj8a2GdBHGNMJ6KMTyixO8ZHdAj8QY9wqHQhhO7gOTRWA2eaeDKzjlYkp2vCGEoyOelUFOydggNCqcCizV8WxSIMCsQacpuGPJkXiihuKpkWYPAPuJPmEUk/VAgJHbxTVkDwb0ZDQgbDx9BzebwQEidyfNUjOEhSiUVowN6JA0Qo3ET2FeEUuKOceGYBt5AKZcLDOyLMlAZjiUp1IwIm5r0uwgDkTA1oXUHhJ6Vs04no/qlOMbFUob5eCPTAZKqiGg/BIhC+lo6MpWpKRkQnPY/FyBGM2nyEdRsDooFnjHG47DIPMNDjteMDp2abOrCgircBF7SEs/DhXHhmAZaVlkL6D7YF4NxMdixhIfBMSLU9kw5G2ygZuobweiIp1mt0OB64usN1Yck8Tf9E/ulIFKZLLUfAVoe6smixEv4utSLNS4C9U/BB7OOoGZzcN/ZMwb8EPioaGSe4SFKJTWj44zlG74weW2qp8vmY78RH0365x+iuNex5Y0aH97S9tfoe2N89/sRw/yxQLzdV2ULiB+rsQdEr5ePRfYu237T75NEtgOxA7ED8Xt0dgdiB2IHYgdiB2IHYgfik3f2fwyI3zWsR9u8r/ytUp+cFbUD8eCWezs3ORL5ag25Du8OTsNwLZ6Me0i659Q3HA6nKaTTlKzNcu+JEzp0R/fCd3anOJ0u9NA8UqunqR1Cf8+qxHHpDLQaic9UzHr632W5ozrpvebT7KSG042xT46j0QMiNu3nTiGr4RoQK9+VmsPgWij7JrmfjyOcr7HysdRB2ihQ+1cgkmNQpJKKyRDnL1kS9QGwmLKjdVivfCmp1ZL/s+8p9BDsCdMYekCk60CEHwZidUVschquAxEMCFdr6t7kxfDRGEdrs0SmHhfZHsIN86guFR0gwj1AhCLrsnxosu9p1Gx+uA1ErMfLaV0E5Ul4foZxJDwvg5kSCUyIluWhF3mf01D0KmZ4pQi3w+ltWCSReRTK6lgmpgmcB3A0oHhBddDHHBoE0ib7v1zgCMPXlQeC8mrPyOQuQKBFqpFrCqblkUUTxEoVjQ9KHk2cGKKPR7CeAojnCgjT/wCPRHklw2SKIJyYGuXB8sKvEL5Ey5EYW2UMyfu2LI/AJJGwwWlI+oA3bBEk9Oz0NsYSCYHEOWmeolKOs6R2cwDvJMFv5cQ2CgBCFTUiu1mQGU/IyxpBOgWczj1NlGnOZ9pPQyzqHcidcoZOtTyy0BxIIKLW+IicBgV1KGcTjGdjE2rljEzkaVauSdC0Mo+W8kpM/yCnwrQo/iaFL9FwJFKrjBGnPstDrohLn9PAahjMZ0gldT4EKFXkZEl3ABH4U3UK4ILoLLx8fpmXQZFhjUgFCOV3REnt0yf6WQ1CWCvjVXLGkV1foN6RVYIrmZaHAmGlVONDssygDuXglDxieKZRG3keTc/yoSj6H0pYKpoeqgjCfUMyX/1NjC/RcCRSq4wBINYsD5lhNzgNvAZwGt6AoLOVqaElmekxiUMRrxW6WCs1IEr5IGsA1gjld+gVwWcs5z+XqWkq/eM1Yl4DAS2PLFHwGlFpfBgQ+OBZT3H18hVArU2qJSVfDwoQpukhV4QxLWwYlS+x4ki0yhjChGhZHgLE2Oc0JFHVIIUf5TDkSm/DMT0miSBjasoGxMjxChUs54k9kqLITCgQWCMwzFmS/e5bU5CpidU9lDhnWh4iZcK11hofNjXRpUhmIkVAQulEcj2Vb22jTU3GK9GpiTgwhWmh/ibGl2g5EjwMThkDXidrlodMTec+p4GZEMxnSAEcBlusRW8jFaZHRgTK+wDLA2IlzMs4I4bTNAX6HaFDAoYEKW1gAWaRHi5Hr/R3xOly4mU4CnMDvTEtDzqH272UUqLxQS0zy4OUNfIQKiASWB/gtQAI1f/Aoq28EizWUARZ5dxbNkTu7r1v+7Hfm3n1SMDtLXaJBeuvj/fHmNO6FH6kec7623oa+3Hql84eEG/jSPwYENpm/oE284Plt6L46FscOXzott9r+iTbDsQOxA7EDsQOxA7EDsQOxA7EDsSv7ukOxCfp6acc/t8rqgdjuSL6/wPNxAf2/vlAtGnozrbcY5mqMhPpDvO9WehXVDV4I5JWoKGpf/J7cUqGGkTAndXTtNwVnSklRa9yklts89YdGbrTqVwJzwgZgm9hFZXrKd3tXd9N21L4GG6M3F1AaCzVaFxXQbC3k3IqVvoVkge7C4jQBaJwLuThWbmbn4uPfBIuiCcFKBCWoo+rf7eBSNX56/vL8fY1XdmR+MjS1TM7H4qHgZAmGzuaFohKKeMxIEZNqcirMTnSTLeycQVE6ANRR9UCEX4MiLAe12tXRBMLlAWUt1F4FMS1QCK9aGwwlyHyQ7wEBJvc+6MTEv2tHsXavaTmNKjShcK4ZHwLEHj1zIOMx3CfyyTCKhYXTCfPAoToeMzq1hKDb8FriyiTogXiMpumR1EOKT4owhyRer1mBxQ5TlpW1D90VEuPhY3y7EbIj0ZACm/0PAqV7zGNjRFAIOd0YmuB6ugUxEUkN0oZxF44zebVUHMaVOnCgMhnXhuIdcGv8hkPntOwjZLRFv+JLHnjcy4eJ2rZkZkzQiuOtlBpiwiTAmwTA+I1hZKwDalN4UoyVTVLkimOTMzISOWB/bFEObr0MrfCdVgsfjQc38d8S+SKKBob3GVKhBN3I6iniDs6aZLw1ChltO4lPU7DKTggRphhZH2F3HBRO0gBTiTL3gl8ESrHn9KLA4I5I74FxygpCfz2inhNFhedT8ySLO2pugfqVSUSjNUr55+TMFFEdEJGVV1cqB5ojCQXix8NB0TGjBsUCNPYyLJGBFbUUP6BHS00rpRDrZTRupf0OA1wDCqLtQKRjJtRgBiFBzjHQsTU9pVYAO8U2VdacIySBohnB4TX9OB9c9EjUZUS9HXW+DwQmZkoNRBlHJhIijqczokbjVDYBQl6E6KiEdThpEjoTBhqXaz9UQdEqpUyWveSHqdBqGST+9YU6m9NiGlMwzklnZpSOSb/j0quBAGo1tKotEWKiJBNbSJxUvgq6vni2CVTFi6KKZEgrvOZgUg6NYGlUdGSZGpCHRaLHw1ZrNXxIwfV2lCHE9XcwO8IYkfIYu2OYrJGLbVSRuteUnMaVOkCFEn7HQEZHvyO8It1SLGYnDHHYlLlDfTjVOicxBnxLVTaIoX9EeQ9FlfSBgm6WJ89u0QXb3BRTIkEcbHYEfFS5sIymU2BRHkrg2iMtEDoaDRbbt7lja9wcfPotl1HXfq/y7WW+1u+qZexxTi58ss63T77fbdeLO1oPORw8mNAvGW7h03SL3PvLY530PR4ExC/bzO/cVQ7EDsQOxA7EDsQOxA7EDsQn63LOxA7EDsQOxA7fO+1xR8o+aM8jB2I4LkUobohVPMYqrth06qSqa3PZ6Wr/QGcjJpx4RPquSqdN4PQPcYqQdnlTuk04SHkqujmfaSct4FYcT/Wo5Kxt/vQ20MYVB2f/Kc8utp7n/l0BVgIMfSBSKllXGwBkbpA1An81NiG0POuk4jDV7oXaetjlVd9SRtApA7fKwevzhFXcaebo5ZuARF+BIjpChChZVx4ZsNtIOo9KTRALEM+jx0gwmNAxC734xYQ4UEg9EnsmkshXAzsJd+D01zYCBMub+FOOC0OcyMxRxM0YModqjiBulNoGReeZeFdTMyTxfMmhsKr0MeQvU4IAXE+52C8DxUbUuaJKWsgUs8PQYoMKiGVr0zS5I4qkwwyKuIWc4afzBAsbk0Vqd/Mc1Egwaj5caq5FJLoQ0ZapyZhI0y4vHGBV1ocxY3EHE2CZL7HKmetdaObnnHhWRbexcQ8WTxvIloSdEROzPu30KC9Lvk9430I3wOPs8vD+WNJhMZSjmsinRPhePiodH+UcsJ7mZRZJZYJOcpVavXzg/3FcyaZ7cOouiVUuuZSSOob7iYGBLMRcEWo+IHT4ihuJM7RxJSS1MHktbAezNnBGBdrlgVcTLSGmjdB1+rAnAjNCXv/FuKavKpQRVJ2hQAhnAqUHYLyLpQfQr0aLrlMTRqV7Y8172UyriD8ZCLzSbR+sDjYSYX0SpIKX6A/g01NqeJS8HyJtW8eKyCKmNWsQDgtDnUjcY4mZY3QAWEgcqiBMMbFmmUBFxOtoeZNsEfJDHJBDQSXXcpN2u6o7AoDIoesZWc+Mhk/hHplrfiodH80uoVIfBXSJsU7OaELjhpA8MjMYwFCRm32QDguhQEBDQzwXJWDYK4njRZHcSPBxSZ6HLlS7tCpqb4iCuNizbIQ/Y3Kk0V5E/XUVIBwZQGE8j6M75FMC6RMHWHy/BCaF5Xj4aPS/TEUpkfyPBL0nYEYjVSGPsClJSdwZGzU9Hsct+25FIG5GGBxZHiblAUsOdeTRoujuJGgatHjqJU7zkxMnFUTqWVceJaFdzExTxbPmxCGxdkW69a/Bb8jlPcxiLkbqzGJFggWXyh+eH4IWsFvkzYq2n+aVZlkkP6Igp8svSdIfrn6ofWh+hxQIMGo6S+bOF8hLeQ79hT2wRu5D9b8R/2GzetfHjci8Vs/qrczPfK177F3nnR9e2No9lv3o4C4l5XxCPfj7UyP/C6j9qHbfq9p7/JvF9UOxA7EHtUOxA7EHtXe5U8e1Q7EDsQe1W8Y3s+M7npbHx7Jow38KMMjfmh0vxsQafANbOhabGzT1oFt7Yw7zs9abjIzik50rabGcpc1NNnsqz05TVP676KP1/sR2R7q4rPiO5FQmzs/a8nSh3uAiFeASJ3+pI5FR/+z3tPOaM/qXBVTKkDEK0C08hApuYR/E1jqRJrzlKKo7Ud/1AHRoQfMOBobIBxzJyN1hqTVG4CIm7oW6cawbwER7gAiXAWiuSKq6NYfhweBkIRubiPqAtH4rLi9gMDdQudEB0ryUVOLYO8I548i+8/gTxRtiqlhXCRL8oSiX8F6FOKL0uN8nC7Ia29pZ7SaGeBGILmkbivK2pgSWhrmKXktkEZTQ6Jh3wlwRCZzj7kUT5XhPDgtDX54d1IgBvF80V78+2/bs9pnRb1hsFeSW5Q+ywMe2ZeSDIRTi8jAX3gOup/dS+wBcM+4CKo2UZwcgiQIk5oYcDM9zgc+KVvaGXA/Uc0MdWyYElxe8Ii8sjZe4cAycoI01aobpqmh0bDLCTgiUyjuMa9FjYD7G8xDhtxaltgm5N+KHgif9e+/bc9qnxV1qVGLg5LwzcIHKTSmnIOpRTAQ/Pj2UNLxpEFB7iUshiKsB+M2eO+VQfYMzFBIzqGlz/mogFhpZ7RSDZFbfk2aYwcQYG28lsF+TUPDBzFNDY0GTiwndSySHhAQgzh4UX+FeyIEmSj+UeBqmH4G+Up02CzOZ8XGIPGD/KJekkmZI5aSdNRUM+iNABGKEMQyRFNxORHWg3dKKel7dkkJ4DKwu4o5tPQ4Hzo1bWlnrIGglhUIuK0oa8MDEeaGD1I0NTQaGZzJGBgKBHog/Q0FiFQDwW4wcoyA6LBZis+KHwPS4RBJMLoiIKmR1QqIgLCpZZKpqUw04uA2qh8KBsFxG0ax/CiTGdgK4CqY0U2P81GuiK52RquZgamJVC9UaoTmKQzH+RyFQUJ+Wg0fxE2pzsKHJ4cYzD3mtbTF/TW5oHJF5EJsS5heRjit1D2rfVZS6SEDIT2iNYJ5TJOWZCBMLUIkoc4pKD9BVCTO0KDQRc9xG84ZNnhYFmWpw9lxNieUHueDv1Vvame0mhlYPrOyTmhfLhyRfBKOBS2CreqGxn1yzjHgVKRg7jFTMk+VZTgLo6OsERfV2RigunGmJZiAqHtW+6yoSw0EXziKiUudeRSylrzrR+vaY6Uxy0j3n93jfLyFE3HtnGu/Z+9p6/54vH+L9qzns1LZAq76fzdNY81ZqLt6ndPQPZruP/+BWu8A4u1aHne2lXpHe7/J32Xbb/rd19anu+m3A/EHdHUH4pN0dQfik3R1B+KTdHUH4pN0dQfik3R1B+KTdPV/Goj3sXv5PEP/eKTv2zeqzWp8oO4diF8HBJ4Kk+flfdHs/m2rL87VlPabozxsznukTr2r4rgL17ZeM5KP8IeEl5ElS3ixeE+XXq2949nlufG6HOM8g3eNicEZ128Fum55SW8hd0d3Yf/998T3srnuadUXr5uRRWr8FhDmC2J684M8wqolo0oiaJHpLmrB1B6J5X5mJ4IJQCB1u41kmHrHkb40IJxkQ0KS096SMn8PiCbSVeR0VuSRgoYBcipL3Xk9mkoZmjQTFTeBiKsrwoB4Fll1AyK8CYhhfeQuIBT87MpEB8T6uDxVzn0ee0CEHwMibACBZJJF6IBASohzRZJRoUnHFDlUF6MwESRJ4oJNwnJIehYrA5xVowNAIPGDKcC0MC5IwZxF5WLyR5i7cRYljByCq30GCyLKQDMPZFqSnKG4vCzv6NXEXV8ffw7Kr1jK8eO5WRksZ/SNSl1EfYPaUU0QuKLUfaAo1pGr4Q+BwFflFBKSSVyOXWnUhybqY+BMLyHKU0YmFqoaXhdDk4DqZeKAyJE7SPtHy/KOjq8xqSOFXkNJvEReJR0prIbJH4Gxh4gs8L9JakfqEwM08CcO708TtDaSfNKJ1xG7x0ccH6O4uCy5tAmsEbAzoO/xyk+no52iCVJ66SMdQify2QMRiL6TcUUkiR99EZck4ha0V0RR1ah0MaC9ol4mBkScqFNjihOmJ2EoJMfXECBOxaYsCRfiVRQsiPcBhQ07IplqhuAUVLGAah+kjVjWAFUDSUWHANyR1wJEffxZ8s+8dwRdQPLl8iGjfeSxkjw5hxVA0Ms6UppW28g7VwS/o77qlK8+NFE07yd81nWNyMLpqXUxmH1TvEwMiFRkRXSdiJI0b4HICsYsmhjMu2DmBK8R+VwdKd0pV4QEzxoX1WLdByKTb1AXqDzFWs5iAwhcqSp9QpErEG2k68hh+OPXCPmYpxYI6g0ayDo5kEkHcxNGZrk5XQybmlIAlwOfleiqqwgoY5ZLHwFgWilTE4gx46sQXnjamcCk0CO4wGPhBqmShwxhAwQi0YGWs9L5rEDUx0FrK7XTZKFAjOibAAGG22TkGgVrFekqcvEimxwQMrop+KlJ5IaEfXDW3xEDFmsWnvK6GMpPEC8TlKIFh50UmaFwUfuwk2hTYDEcyNVRFChkappZn2ImhQ6uBRyQDCaFHmHuxpn9W6ReKHmIFJb7HXGSxQ/EtkE8X5hhkel3xPo4NJcQH5gZWbREiAKKxToxtyPyQgomCjgeGo2PNFzWkZ8CnGLsdwRNYABCF+uT0DC7ihv4upcbl4/YKRVba8DVdruEcpbWm7Yf3bv/Oj/Z7Otp6H197R7vlWtH4O2bjzJXv6yt7tvjUoC4p9T7bD/mY7Lfa/ok2w7EJ9l2IHYgdiB2ID4o2B2IHYgdiB2IHYgdiB2IO+tGsY8Z5LrWOrzfAdCf2t7nAKJVC/gfAgKJH9wl5GRptT/3nmhq9uTKFsSzIFAzUrAZSQ5pyeQajOfB50/yalrX1EA2r4/nzdJ5/cppMjjxiI1neoZwf1vXNyNp5F5CXhwTHC+ipO6n1adzUmWKQniZLLTUcZ7Gw7CUp4rSknUdT0D3BiuldRes1iQ2BmYNkjqDk5phczSCKbv4DIi+jsh6b+5EJZFVI1iPRHIkDR03QdkDEd8DiFXzsQ+EPBSd0yYQYRsIaz11BmcbiLACQq4IfUQ7hZtAxE0gYjfuurYOEFnEDZhzwdkk5JQGUbyAJoakC/PzrHoY2CNJINkDHgSaelY+A3QrJsn8ZcnYlbyViIOkodTDH49uTS13YgjFjWFWvQ7zYKEavIeJqYxAU+N5hpvKsyh1OBUP0QfxDizGHwGzhRJl1paPKkpEonIiGiDqqDJwacw7qElcbOBcIulIZhto8hFCC5zKmxSIMRERIrDfCrQseCgSHFjAg6DhO80JSgCqTjDJFTEZEGGudTO0HixA65rCijsRWdHfpqbGgyUn0e5IpcYsHA91d6FXiRU7Rq/iUR6TH8U5xZK0QZgtuCK0LR+VRgSVE9UAGZOyN7i2SZQ8MsaNknZBxRgAhKa7HRDhpAn0TEMCfxC5Ii7iF8JhDmCDBDWvANGE2RyTKFeE+ooouhnslYJ6MGF2arqEFcsjCQTqs+JcXJj1w2oi6hGT2dVEyA7qzcKJywv976PhvggYJ3FYyXjw/aLeFt7vpY4KEQ0hOEUS6SFHmQqNACwArj9zdlpVMWyxhoaEsDEmrAgKBH9uJ/HeUaEIGux5PXzQqCiL9VSvEU7Vw+oJIfRq6nEnaiC8B4sCkZXVgRpnBQKDG0xpJPloonm4JDiwCOciqGqOAsGrRMvoQERzcIokI1qcwffAeIyAFBwWJdJAM4KGZxDeQ6HF8LUi+kKJ9TCI44E9I1/+I5gICBHSIzZ80KjQNaIGouhm8Dwi9VRTk6tpXHMnChCpTE3OxcX8U6qpqcj9BHZTUaURrzESi2dKNp2RpJofukZYWz4q/T6HiVI1QOCmIlNTwgjQGjEmcbFhtsV0EgUN+nY/BOE9lP2sk0LMiDO6xxwP2cM8cDAb2I7seR5k+IjwqBoVSdghYmpUfkdo10mjotQTwJbQxbrUdG65E6dZrJj418RpbjxYzji3aGWcn6WXWFqB+PMMpREPhCl4CINFVDpOszI9MmyiSlsVo0MiUuUPUQwBu4Tr4NI8H3AvxcXGvqSKOFrhcaS1+V399a//y9d/fTT1jfxH3eLID/2UM3q2H00CoruBWmwhXeNWPBKGfqn+s4B4ZLMnR+r28q1g9pt+v7y9HYgdiB2IHYg7BmYHYgdiB2IHYgdiB2IHYgfikw/Dzwn3Zw3KR37QdyDeEwj4QZeXvOVuyVwODFsMC72V6J4M0xfM45BSci8Lt6MvLXNENrd/a9M4ctN+XUZj77NRNoemNwiez5BVP+CawoZyZIwlI88UllExEKz+Jo2f1pFJk7FEtFKh0L1z2x+nLqJW5skrahgbQ2uz/VsJeY2jBiI1ZTT2PgmChuZRfRA7myPMbWvBjRT+9Ym3lEw4wkm0OC2JO4BQ5kfPgSQ4/Yzorgik1N3tY438PYAIPwZEvALEtj5IDURfYWPoAGGtFSBwWXBCZQJfwhxFeooTg7AOMh7IlQDXKhTeV6TwJ2gfq2EIm4GSK/Azoc+NMEeQeFfXlXp/q+ABVQ+NY5it5nXZ62wUStI8qg9ykbNjgLx9X2Gj5sgYS2bQLDv3DL42cAiZKKs0JhVEUD5FzU/IyjqYoIYB25m1CoX3FbGzQN0ZUxDh/wEOKvxguCpqaEI/l9nV9rcKHlK3xMHxSs3rstfZKKKQ8ZA+yKucbeoIPYWNmiPjWTL4yAmxgfKS4h8yBVVcqvgUNT9hkAz3IGoYckWs9TNCcG4hxsgYGGC0SB07XeSpe/nkG7OiMCHd/lbBAzVqHENwNa/K3mKj4DP6iD4I6XSI1Aq7wfQVNmqOjGfJoKfoGa+XUIYQ3lpa8SkqfoIwIVgzI9sasVahCOYrEhwjYzkvOdWOpJ4nRcih8JOYvnWp97cKHlD1KHHMXg+kLXuLjUKf1Mf0QV6Tnq3eLD2FjZojUy3W2B8MiDHJYAgQo+dT1PwE4VZM0KOAckdHlWL0viLV1ATDozEpG6IYJCmpTdkYuVqslUXhFTz81KSyLFpzXKl9XGejiELGQ/ogr0nPLlNTR2Gj5sh4lkxhVal3C/xDUgECehh9xQnhVmT4phCHYghrZgUWa3MLEf6E7CMORBJNC30XiqKGsjFKvtztrxU89Bu4xDFVNU+t2scNNgozTB7TB5kSzrbFuqewUXNkPEsG7JPi4jKHa1utfNHTwcjdM2Ln+K1nHbZ8Wu77zRuvvLt2VCeTtpX79EFyJ464EVsuBIr8O93isE/IxwadN1u5Rx8kP9AS6tvvNb2x9Y9pZwfi4dZ3IG7u/zmt70Dc3P9zWt+BuLn/57S+A3Fz/89pfQfi5v6f0/oOxM39P6f1HYib+39O658GiPcKUMkza0rNzxqaX/cp9C2/OYodiJ8LRFaeAbbCQrg8oMrRsD+Uw5FFd+Kk6huH5Q9vR71Qv9O/T3L2E796+o6SX7/xzoPWfXT/XtlK+S/fVkNw0NPpv8Oxf557Za8PTSPl3Kdm/xfa9+3p29fvh+MBfbYeJ+S8i/pIpf0xaXo7NP8/IAZRkQ6YpTDhnSReJvl7RFiEx7//Ume+YMh06PjV1yOAOKILhy83gTi4GrSFL+FQznRAvBgeOpgH99/hYSC+1MWOBMxxif3IoHCfS48ViJKD/FggjIpiCRH5+6JhfbkFxDEcFYhwBxDtm0O19y4gjl0gjs3/N4H4qwXixXpMGSdjfTggLuJOIjwDSVCKG8ojqhw1+4OzZqKoMaliPP1dgn55+nY8LpPOy7//fv1O3aDLWC9nB8RLeDkeaN/TP0/hr+XfsPy7/L+c/bT8j/LYs0wD35ZJDXvDUn55RXsOAXufvv37L8o9/fOF6lqOLiW+Bzn6lY9RZHRe4Bqevn3n+rEXR9Fe4BpkWuKYJQKp8wvwOFBJ6iUd1x6Hkq5VBxcZa5dKzEaIQvp+eECVo2Z/EEuhaHOAyTPjL30uls8KfdSPekXQp+cLf4poEmJIjhT+l/D1G+07/v0l/L2cE5Z/D/RJWwA6HPCp+3v5Q/+j3r8PXN/f9OrA59NeOmdp62B1LXMW/cttHvgdYsB5S6t/00D+H9evNXwp7S2jrJNmOe/IdRwOuEKWQV8+LNzCC/XZeqx6H+bgIs4zr0kdUNZAqPfCPaocNfsDeeRBnVqCvyJKWOWKKEAsizSA+EIgLSX/Cl+/P32Xrh552Ja/X5e9By7PE8Dh6/djEIABBO1hIBiM73RdUH1W19N3qgfzzXL0Ox/j6+fIY0klvn7/P4H4iSeyQ2kvcEldqL9+pzb4OHBAD16O+FrCV4T1OIlIkPpyFOeZ14R5HzyDUOgfUOJ4RJXDsz8i1ohZLUD8GnGUsGyNMCCOBwPiSO9RGoP3okDQXg/EMbRAHIMBwWW/8ecSJV7+ls8vfQx0AJdAvtHfvwUIquP/pPbwjc+tgAj6fWw560s4lnp0amKgORruc+lxLqwPBUJ4J6/iTgKegbsiggFxnyqHZ38ACOVvDJP/1vQCIA6HNRA8cSgQL7SuH2kf7eUBoamJzz7acMmfw8EB8bfuATx0jkwQS13h5a9l6tHJhY/yu+OBIvvrG096f1Md/1f20rkvpT1c1cvV8FJNTQd/RRxLNNRn6/GkOll6RYxwcQnMiMjKM8BWeBSXR1Q5PPuDWQqizQGWhP2OeAm8ZH7T3xG61PHC989XflWAOPBS/Q+tFU+8SB5o4VteH2ShpO7ygsmLJXX9K52xvKI9NN8/8TkEBOpahiVw+wcstgd+R3VQZEf+7UHllvcHqucFZfiotPdC7/H74aks9V++ISqKINBUxws6fkdYjwkIrBHFwYVGbf27rH73NlWOlu+R3+WX9fEQfmDz9V37QXi8o64vD5S975d19m9q3oJnHfy4Ksd7APH0LfzA9p5AvL3l/V7TftNvB2IH4kPr24H4xT3ZgXjH4diB2IHYgdiB2IHYgfjI+n4yED8rwI+q+z0/SB8d8w7EL6jvxsaPtYZr2hdQvRia97ylflm/5aotl0hfHe1s9JTqfJri9N+FcibTklChO5gnf4Ox6Clo3dYGtv8uuduP+nV2/w98ZF0faW/cEXfFbRnkfb4FBAaltauo6k31c2IptU06YYjUB8KZhtwEIjk5BeRDUoYfykQJlZxTbrs9x+7A4Xnb6Hxa4kZPUxmDXPy4e/X14o5hbQDjuS14n7fx2gKif0LsVZDaF+unAeuQHaPhChChBkIevRUgUiMNTcObZeC4bmujAWL1MauBCG7w+vWZzHS+NqSPADGohoVoSES57a3+H6rKQf/yI6ycoXsWEfJnUa8gJgLzOkpaKQU7E+4iz/zQ6yAKGsZogG8J8RpQ3juZDEG9T7aBwNlwLkkZOXLUbW2cLkgCP5fYfD/ULaV4skBGOg8BDJd1fQREHbf1/hnnyqgUbxVq4+x9WGrHFnUJwWPv4oxCQIiaBPMyTjP+XU4A5yNDlp/8TqBeQUyEiQX5YRUQWCcej3PDd2QsrWS5zPXCH6GhMdnj3+ZkAl0LAHGaRXtjyRe+JuZBMGMuJWVEnOaclawQg29Dr4hRYqv7gb7npJ4sGAP4v8DSoK0PPfJxW+9Tsof79fl1Gc2x8nypHFtoeb5wl0g7glkZWADR6SEUPwYOcrhIB7gU/FRehYnwygPHuhW4xNVUBh4n3CR/DrkWx2hgGsJJlClqJ5MYssmaBChc0FXwiiuCF1+GPkucAgTqtjb0ikAsbT9oQh4uQ/FkgTvLSdaIoVtfG7f1nnuRxUvGpDWSmu8Uz5fKsUV3FA0JnUeBITQuDIisnySxp4LNBpgIGkou3iYKRJb6lk/EDHmFWDEaeCgndS7xTiYKBK8RLj4BIrVAQKJE67Y2yhpRzvD9QN+XqGc9Rq/0iiA+VlufegtZ3NZ7AAFPlj4QedVPDNRYZt8RlyURaGhGL6oSBsRYOpAEiFGnJuJ1xOIzUgGRUB+ly6PoZRijYUw6lFY+F2OmCoisQJzP/G6sgCCiVsYUgrqtDQdEpx/ouxwrQwaPMHNb8fXJ1OTitt4DiDI1JQfEWHm+VI4tQdgEWBrPzyBOYsqaC3cDmkIB7iOiRzHL1AT1inPAhc5eIrogqhoRHBRobVBfktNsjAZmenApicc5mZykfeKEZObHYeokGZ/ld8RZJhphRAThlGjd1oZ+N0IsbT/Q96F4smCB5V8uHMG6Pny0fNzWe+7FWRfrKHWhjcrzpXJs6f96Cm+3fwzX6nTf7j7uV2r8s25x5A+oz/NB9lscn2TbgdiB+OX17UB8kvp2ID5JfTsQn6S+HYhPUt8OxCepbwfik9S3A/FJ6vttBvJPj+5GhLe0WeMPdTW+S6v3lronulsR3dfyW2PuRignTpMjA6jPB0TPI3mhTO6U5BkNep9W7yVl0+6QDdYIrsW5vJygS4Fzp7VVxLTdGT4nr2sPFX8j14esbtdfNkEIapagx3EfWo/Ts4aUyOF/Z7BJOIpSYZbUko9m7RSTVxEG41RIyyXZHbL6fMhToi0pI3lGg91Z16Zi9b/1XnkOaTXUOHdKsQXC24w0GwTZ67u6qemmprB8geT6GyUdJSnQpv7oB688fStppWoMU+lvvHF9IZ6W6lMVmscS5CTNJHuAfQVEYTRYlq8FYv0x7LijVECsD4erQJBvSxcI5W/cAMJcTTwQ7QdJgRgZiPF9gJAI5YJR7gS9W/JeGakYziyz4wmlLmK53Jb3Gfs9o8EDAc6CancMQdgVwfMcPP/DgAAXAmcMs5NiD0hXPRfWQ5kGk3jk8ICCU4FJ0vE3+Bx4soiHi8TgI6JJhdVDikmNAoGzslwFzwzEswNC2S+nuXBgJj91Kj+EzX1mGZX5NHsWy1JOWRJo/jUhecg6+uJ4ssyEcC3JmLHIs4YebfeMhsycDRucMZl2Bww9AITyHDz/w4AYOe/NZyxpy6lMdFGSkikJb8KmwSyCOxnTXZb4jXcB1kQoSdjRMSs4zywRnfDY/vI/OGtwixnmwmOZEidO85lK5jNbeGDAk9okFA7MVGoobi4YQ4mcXVo8i8Ul57GUAIglAU9IieMJOZXYFUFAkHMJddUYDTk8cyKUig2XoB4KzIPIBQjjOXj+hwGRBQi0ar4psIBhzxJkiZknwk4pE4CA+RgyyGCRWHRJvFbgv2L59FBFhDWC/5/cRMKJWT4rRFVYKB4SckVc4IySjANjV4T6qXAp+L7QqFB5z2JZypXkvFyq4DVESs2m7EhdukYMsLZgIJyuBNx0GIiSjOe9SkWoeQ6e/7ENhNqnRfErSZquB09kRq2A1QORHe9C9iZxXQl9ILIs1jmsgaB9vDJM/cUa03iSdmLFlfREoIgIMFa8RmiEzHOKhbdUgKCwiSIhjifgXKRCHcys/UNTkzEaisAHe7K4qSkbgcXzHDz/I4Sy0iQqE4VXcj4DQnUeocGkdL2fmrgj2YiUWeI33oVMWGMy3kRhVlS8E9UbqRzllMdCa0TKayDYMQYOLlSLcGAcEGPhKI5FCgW8DxfhUvsYToWlASDgsAK+xSAEGGI8pLI0LQspcR/OntEwyAoB47Agi+swg503yJcB4zl4/gfrUkzKE8n8XZ01PbJ+i6c9YEdkMDbKYr1MQhMZ6OAbPdgcURZrjY5ZE2d1LgHXI8pibRHRKkcxi2hPWWylHyCLcWxYD0mPCkQ6Gh2KLRYOjFWgXwzAWymLNX39cRFmLvfg9qjXSX2Obq1ax9Yv0Xzjfeh8XXw0urt68OD+ezaL8E21vMXr5B53kh8bkF8FxI9sPwjER3X147r5x0e4A7ED8dtHtwOxA7EDsQOxA7EDsQOxA7ED8YdE+BnD+y3ZTLe5GtdLfHyn4wNR/1QgcvUqNaoKbkvXakn6qOS0UWAqbU1di+KEBHKtdnFvDPlKYKtCU+fc6VotTZo/XYlE3CBy07y+N+7KiueSikIGXi15qGmjN43aRqpkE+S8ycezloqQsFK/9lh1uv1ApM3ue3rAmnVgah79fuWbcL4VCL0zbKIPyYmkeHpFpQyBV6fL6XIdiF4kSc+rgAgrIOI9QLSeIz8IRHgPIDzfIjUAbwIR1kCETSBOs3In8Io+2aoVQUoVcE05rdQ2OOHBfixgTuCKUKbCaXW8mqCSqmfUtXsVDQDR6HJwKdUOyfkiuheep3GaTfsDCRnlkUThqwhrgreTcCo4tPRcEk41W8RiwoPMIqw+haDjNEhP1B9Fzg/PzOg46dE5OWWSwnOZIUlBHAKmdeEV8oryQHaGVkRCpniseRg09bxK6i+VB8pDyRDXx9FtYYJAgWJVu1fRCGGtV4FSwsVYzn+VR8U9TyMV7Q9Oh3KKchQrksA8EU2UUkaOOzxpmngsKdiaLWIxTUgeZ019BhkntGn+KCO7zVDL1Ceob1CN0CfJFc8FYymD+lqGN4RQeB2ah07gOaSWhxEDVDhy9lPPIGoUveNGE+jV7lU0RO2i0qtAqVzOf03akvE0khzFdAlehXrCDBd1arOpYricRHvkVbRCTGVD2SIWk/ZHE6rDBeM0hCycF4kxQe0jCxDwm1EeyqniuQzhBhDkhyKfWfAcUsvDiKqIUQ/0nEPaOB6t/k7tXkVDFZG8XkV2++j816QtGU9DXF7mIOVzkbSA4lMLBLQ0DAiKPjRsEYsJ/RkmpaHpOIV5NJJdUelQIkFSvxmRxcjB81yEjZIKdyKlGggaQGgsJWFCNDyM5KaeZECotkTveCr192r3Kho2NZlehU1N2aamUPM0EjgXKj4ygmPBQz3RxKHDr99iMNHQa2KNmG6IZ4tYTFxm5CEfweDQqel5Ns6LADkWIMrUqn89zwVjyRwHcCfwCkMgWhEkPBKEodCobdB34hPTIsGcsC8eqi3RO47PnnA+VrV7FQ1brE2vwhZrnD+JCobnaYCRUcSxzsSeQH9CJkaKTEizfq8HpZRjy8ZEqdkiLiZ2idEvAWBpYE9Gr8piDbUPfFCi6HXYX89zwViG61t+c2L7v8vtMuva47v8il3HnNOP1NDGlDsSiW8dp7sDyj+59o8B4lEeyXUgerXl8Idt+02/vdO/VUw7EDsQe0w7EDsQe0x7pz95TDsQOxB7TL9tgJ81vneP6y0Vxg8s/eEd/shxy6s9/ITYRDlo05zIYWqkA3LwSfW+N8mwdmcpLI/hqnNLOVqaHTpltnqgZ1t8J3kO7vYg9WqN87DR0vb5dS25KtEVf8ju06f2FnyXPPinJ1sgUgOEV/QwQYRYyR8gH3nlmKHcHPX/t1dLbYtRn+08WyqBiniFGVTXKu+S6nT0DEBS5/zQxJar8bkBhHVjLOr5YwEi5y4Qwm6Ina605jFbQISPBMLUQir3lXuB0NLKuOgTsu4FIhgQnhWCDZwCz85gvQkB4rkAkXgITcviuWI3RKRHgmlOnPgVXlNOFyyLa8cGYXQ8y1HhXEzFjcQ91gvVi5xaf5L67EotxAExsDzD2sVFXWVcrQE+MuQhgYd3+bzOqGk9FqfVsu0lYywVcffw7AxSmuBVwmlOlKfpi5bFWLEbMJ2pVwpytvAs4SeTcxQLg/pYCP4Y2BlJbAOiOJNw8r34sJiJwljcGLw/SX22xSfPR09QyMB0MpZHz2tXGV8rzQnE9oDFAWdjeZZYj5rWYxY8Vsu2l4yxVMTdw7MzxEkheM0JBWJwWhani7EbChBFc0I9S1ipI0e5rK8dy0E5HRhKMDTAgjix3gV5nuAh8y1/kvpsi+85rNcIZlxkdXFxrjLBao2FOBSD+VvEzqjJMRgjOE+ZG14yhaUiiXXPzoiVw0oNRHYmHdkxLsCUUK+UIGyKssRNOthgefSPtUNp0MGQjPkRxLGYK1uMyp+kPtsxQqYeENR65eKC0rPV6oEYpuyviGbUPBAapdZyxUumsFSKAoaxM/rfmqLoTpiWhWc3mEaFak6khKuJX09+otk6plyNMpTJAZGic34p/kfn1p+kPtsxQnIDhJyRpjItmqtMslqrK0LipNUJPBE/av5qKR/Xm14yTldE3E4cO0N/R3jNCb1k1bcEvARjN5wcKRILb5zhWYIF7KSuIXn7GDgNkVkOp6Jpgb9aty3WFBnzZSt/kvrsWi3EWL0nbg+CYMXFxVxlZqv1JMSckzi78Hm0d6UtovWc3FeKeNNLxlgq4f4tr/bUvxAfZUrUTI97WB+riG60+M4yEMnGIT92ZvhgL5m6wvdT3Lh3u9Xi+3bYFDzfAsSHesnsN/0+SVw7EDsQOxC/Q0d3IHYgdiB2IHYgdiB2ID55R//ngPjjA3swvscNcD6ZZc4OxOYZeXvnfbf2UnVPJm3WGTYtLdyZvs07+RTuEX9SuTevl7ozeHZtuNErR0K42qCWa/vz3yXfqJmBWFl0VKlzx1KYtoCIXeEFAKGPhaduJ5Bx8IGnK0CkYkRwGwhT4Hjmp6DNAsH3MCkHJa0R3AYidgKMXSCKr7wbK2+L0gARrgAR7gAiXAEi3AdEYTVcByLcDUR5NeXp2QFR97DQY24BoWyQzUvW64g4N5cVENi0FXcGpyuKSkXNPjCWAqt0FA6Hsh+QQHdMBnZbwSOwYEkI4+Fcsy0uUlrSMJNnNZzKI69e94KSMPxAbDCPFFXbeJ5b7QyuW/qin3gVogcvQ2tI7CgTg2l8nFgjZJDRADPDe58UjY1gCiHM7phMR8S5uVyQ+TY2B+uTsE4JjZM7I6dUXFVCqNkHxlJISbUs7BHvtGYyQKUi60Pf4Fp43gf4EjArYGcWFknwrAaRA0m17kUQDoV5pIAdoQ/Xt04r8ph7KkCw9IjyMqyG04x0MHoVmAcC4wOMBo9K8t4nRWOjPMYe4KYymY6Ic3ORK2IsCh9RVB5ARrAzOMlezDzY8cPYB8ZSwKSgHA5lP7TqHKJSkVWP4qRKGK7OofiowJmF/EeM1TCILwktybXuBTgUZY0QdgR94kwMxZxW4GwyBAUiTuoaU3M/EvM9okhLUMsJCdCLfSyHi/c+KRob0PrgeFNIlY6I1xNRLogyPTwQxoNZARFMPCF4lgIYGGafAfYDfY4qJsPIpIOsehQ51EBkaQE+KmBF5OBZDWB4EOOh1b1ogcj8+cwSW+u0AmcTdmSZRBRMXGMMiBwAhDAvOLMsV0TxSkE/vD6IEhlknZkh6pAqHRHn5qJrhMTrgaAxtDN42sFgMUujYh8YS0GAGFUNA+wHmlk9k4HWiHpqUuZCxbYQH5UlhBF0MM9qIIYHpBq87oUAISQcz45AbCunlTI1AQj9aBQgCi3IT015XOoRuygbDe8dYxobRYqFVECEHavlnJuLASEKHwJEYiCSncGcClkgy2Jd2AeFpYApUzkchf2QaiZDEC0KW6zFUeVcsS3ER0XdTzzbAgyPIK2b7gX4I3EG98PYEYPE1jqtwOVEfRuXsy7q6gJehtaQ+GvGSTU+zpnVObiuoB4v3jvGNDZE62NGeWaPFB0RK48tKleG25uIGcPsQePBVD/o2q9Yt7cViyK97ey+1sVGG92IH4/9kRrv+6Ucb/5OXkf5Trc4ViyK9Lazr2ldXGNq5Ctd/FEgesN1a/tlQLzXtt9r2oHYgXhLR3cgdiB2IHYgdiB2IHYgdiB2IHYgfvvuf2TbvySG+MDeHYj331RNY0n20XPIplYsx2Wvbbk+G8/DrLR885UmW12NtsZWSSNslV+5Ndy7aQRb6hxrGsD2BzJv9a6587bcqZ3+c+4cuflf+QYhiMwDDuQNA4zUBSKG1CT4szPvSFevslSZ06uhx48BsW4zVUfjxiDqeWsgho0epEbewdXeONBQBjY2/jU1ILkA4fhHrvr7gGg6nFf6GteACH0g1ooVdwIRwvU79FtAtLwSi+E5bE0oeat3DTMka96wOS+XYZ7sijB9ihygEgEghhl+I632hSaAAAS8U4ipoLoVuFRb5QvzEik18lTI/ivnnKGHUStWKKdCfVg41pKCys4NpVbIUK0LVfu4cGtRpt4cenyUYVrzMUR5QSIchMOxGg8kzSibvXKgMSDMv2YoIxMK34BmzCmJx0dKyOCO5SH1SX1KKu0LpETp4qWOirdKMt2KvvKF6Gpk44xEISKAt4F3tWJFED8UdZ2g/80kgPkWhf3hFTJCUpcK1POaRHGHP6anqcdHiR0+hk43GmFq9DaQNo2qMpLXDjQAglZc868JykWhCSQFuyKY3QDnjyL3ACBei0+JsjFOF81CE69BOppToYrUa4RXvoCuBr3TGvE4vbpNRF5EPbcDbAolEajvgvmXcM5dGBwVr0SYJyrQgH5Aq0OviB4fZc3HKFokojqSGr0N+MhEVRmZ1g40mWUDcvC2KYMqdeSis+FIKVMOfSC8vgTpTXg5hhoI1a3wQJjyhcgpkJPJrEAIbyOpuEqouB3aTgtE4YuMI+KbG15JoRQYEKpBJStZl4+y5mMULZIgV0TIoR2PVEQzYseBRqgUFRDlY0TKblNZUHBF4NNsjAgAAb+RVvtC2Q8OCBDWVHsj9JQvwOxIk9W4npo0FR+CsSkUCJ3mbGqiNQlqGq1ChmpsaAyv6RmiP0lFWdZ8lB4fQybJkVU1AMRqPFLR81FjqiqWUYEw/5rCdgEQ8juC1ghxO0lFJSLwrEbLiXp/eO0LJkMm4jWoogXxGUy3gufhlfIFNCmi44ycZk+y5OOhVqwAjwP/Ps+inVEWa3IPBOOjVcgYztC6ULWPiWhoUMWYAmSBVnyUHh/jxOWpjSD9rRkv4JOcRGVEF+sqljNob96/Rr/ssPnbR2yP63Ksv6I+8qt229ezV3PzNXMVb5+Psf5a+hts76HL8QgQA3tlPQbEtXiv3uL4nYB4j+1/9qbfDsQOxA7EDsQOxA7EDsQOxA7EDsQOxA7EPsy/e0Q7EDe2j+K7qOCwbRn/Fddn+jPLs8o40pzHOvQTPUTLnA8xm8ghbN6ZQVo+NFnyYR2E21RRI1eh64vaCyV3W4V6cn8UfEQ3HKYnuzeFRDCnmabOEKZ+j3IPAz+g0dltWDcHfv5Z0jMFmgrACdlBvrNOz5+Gkh4vPIxa88O6ndzQxatA6EPIuWDs74iW9FZo1USqT22T0k8bQKTU+xQVakSyGvvSEAqEGZGkijgQrwMRukA8k+x/iNkDEdZAIIudWiDaThQgkB58DIjqyH1AhPuByGaxnUIfiNjEdi8QoQFiVbFxITgtI+l+MCaSayxT7iU535Cg59kVsaRsRHYBvAkuex5MtQJqHrMAMSE96JU1YlBuB0o+u9cKBMcgNYJhAjZJDKqM0ah1BFXs4DJnYXKYiobUYBEhyQOOhtZS+jBhmM2BJfJNeFFuqLgZz5L+8rwNHRnVKNFywbgQE7MxhCCWoFhhQDzPLCoAJgWyvnKeAnGi1OdZ9S4ocTjKo+eqWgHND2hgOL2KolQRUgzK7QAvI5XXQlOY5UiWPDQNi7BJYoC2x5hqtY4o1yp6QlG/Cq8DSgNag2eNhHQqGgxIh3J9yV0RxSWFBR4mXKyq3oHejEkfjjfeho4Mj1FWBsoSv3EhXgsbIwpjwgNBp0WRYzhdpPlLrtaI5f1YHCiSpsfBYjCpCSVU6oRsyhqsciHcjoHfF40PKRGURJBRI39+Lyp/Mgj7pPYyicH0mhD1q3wwThfk0FGDRTQEbTkV1Q06b7gYfc4cWMQ7Y7JVbJDeoHWomhhdwGQkCAiMYk7BuBCvhY0RNK3vp6Z0Kt3MQZvPzRqhdIMWiOw1P2Rq0gm5eKKgfeF2BJF30CM51EBojebyAvOOXGlt4IoIswcih3JFOHcjVdzgD/asLaeiusG6H8mAMAeWNRCOXNHhbRgQNM2hXgAxKuMA00gQ1Q1VDBN6CDqfcjM1yeU8FaILf2sak9K2wPBQ+lWBStLyPLM4lQ4oVJSOjMl1yrpZpqakzApMTdD2aL1MtNXopiban4zdwTVYRNZyKoIurPsxplRoNcWBBUDo1KTcDB1XTE2OtyEjo0Ip0BPhqcmkqYiBAN8TYUwsSxP9AeuBpgNRp4BnCXQ3UELXCPkdwQuv8BSU8RCdKJYHoihrMDsiBvNMgSqGvJ499RJLegR3RBRCTqLt4bU2cvE4wecPUdOEzioawjVBDRbRMGvLUeW1mILK9U8h1A4srOZx0cVauRkwswEjxfM2wCgRjRKJEPFf3WJj3PeWEv3tx35Z53xnufqra3f/fRHlTi0/uuU3PVLwztuPAYHP5WNA3Np/HxDvudmdit8WiP+NiHYgdiD2iPZu70DsEe3d/uQR7UDsQOwR7d3+xRH9P9jK7Kp3ee5jAAAAAElFTkSuQmCC","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"iVBORw0KGgoAAAANSUhEUgAABiAAAAYaAgMAAAAgkEnVAAAACVBMVEX///8AAACAgIBJX6m2AAEbSklEQVR42uxdT27juNOlCQcgtHIDnT3hleBTeIA+QBbp+xBZ+RhaGr9Tfqp6VWSRkmwnne5Jz0f1TGxL/FPkE0lJ9fTKuZvbz5/uE7fPLe29NXy09t9v9R83ogPx0HZofu8P+8PPn/T3+byV5yknxsfupZR12Crt5aktZn/PqsNd66lk7bSfr9a2J/729Dof27v98+u9Gm8AcVjW2u7ZmaR6dLeRZ+eez8+vz6+782FveuLQ9MvuvDv//El/D4dS3K6qcKeppaD9fgsIU9p+1xhNufdmH0rdbQBhj2q+PZdPnUbNbC2lb88HAHHA/v1ucR7s7gNxRup91am7BRC7OcWB2+zqXuK/tFctPMxde5hLuQkEYfXzJ/09bJ4/CyDcFhC2tN3KiNgvd20AYY+WBlLJd4GYW31oC3D3hsc9INZGBICQ0dcAoXsZiD0BMf/du6eXby9PL/N0wZ/f5qlFf9lz+GneMw/xH3MaN///gwf8vG8nqWkP5d/zX5SD8vD76aUeEXJEy/qhdb5iH0qd/1IpVMLhoLbN3/JR2sO1z791RFAzd2Kbk88MxNmdDw61at1oPX+qNbkf6N/ry8+f3G7qEUn9LC2jutp8O3feP81WUQm7s+7T9j3zXs1TAXHYf9/TjEKfhz1NLQf5VWZ1Skjddzh8n//O6Q6CJp3bnPrwnb4c9s8vnI3KdFwul0r/7BpBub7vS1mH76hzv/+H9+3YBvpLpXDWs9p2OJSj33kkcel5jdhb26iEPYB4mmvf8azMtX7XurnVPKlma/aOS+bumS36+fPA5sNeai9s/Y6poMlHQNDUxLYddB/Xsd+jpoPmof3zKnHGiJh/PL3uGZfn1z0vbmhcGZZ05AkzGhcKICjn/PeVpoXDHtPDnv8+cRdKuZz3yQxymqWfXgVaacLzK3L/w/t2bMMOExxD9Oxg21wTmY+je631KU8WOiIyEHNNAGJHpwFPGXrU1u20xAPyUMlPVPKrAOEwpaJNsPUgc3Kdb8dt0KO6jyxETptHRwT67wXnBA8QJHyRcWKAmOuhAXYWY7DcvBwE64NDlxwcmkpHnJypyOteaiDyGOOyUDP9r0CQDRaIeUTIOrY/t0AcOLUC0YwILk2B4GnAAFHq1s7hPdr6F/pGQDydqZZ53xl9BVsPfAXW5muBwD70aQ3E07mZmr69HDAPySA98GTwzXbdQUx3cia50ljJ+x2TxF5GkwDxXX9V44vyfs9npZOpiWtXIOh3npr47Mm2nctRBgJ27zeA4ElIgZiXCAXie65bcgu0kodaz3Z/5xHh0E/ujL7ayylmzu6cb+d+zHP/3tRP+7h9PK/tTB69apLFeh4+P3a0IO6f+Myalzf6ZSaT5/OTLOi0ClC6skgiLy1WVNae/z7JiHj6QeZi0bNAUK7DvpRFi/WOl/oz76Oyd1wfLYZsY7HNlaNUyw6XAi/2CgeL9TMW3B/PWCIzEFzrD62b/lLuvcvWkHWy0D/9ONOIgA10RnJfwVYFos5HnTuvgi8YJbqP2zf3xvz3XPLgPuLgkH/jZunQXrph6as+37ut3aN+tKzV8l/fd2e9rPuwt63XfDrNbN9YHva4iWzvd9wiz2679seBeHqpPz8DiI+W9XgN28eWdT+9rAGBvbeAWMt3u3/f2fL+rKk/9OtAdCD+tNUdiC9idQfii1jdgfgiVncgvojVHYgvYnUH4otY3bd3QPA4HG3K2zk7zF8NiANzHp5efr7OTylfd+f50buj55X6RFEfY8/PG81DE8tRkG2/8q18X3mYtthul1DVuOCBuKfFPnqm+vSObj80pe0d0w20l5bdGVa+zd+blO1vFxsgvJv/JQdWg/oGZg/MPv8S5w7/Y29h6RsLhOUN7G8CsZfGbhAFNkoobndLRGmT7RYsDMq4q1LsW2JLA8SuKm3nmG7gQBf4+TPEpt9D/ad0fIglrf7WPUGACBmIwQ1uPipAKKNg//IdQHyHs8PBA80esttAHFa78eCqozUQO/dICR8GwrmbHb8Ewi2BOCPdbg2IuApEbICI94A4uqOLYQHEjx8A4psB4tv+WzYTPAYwGAoDZJ7GzspSIDcPsR+YC3FGGvJtId9TYU3MTpJHSsguqPwQHuyRzIx4nf1mBwXiSVgj5FXWdLz/B3ErhImCWl/JHuKHIB/xQ6rSiHXC+2eb5u+nq3PDdJyG6TT/P0wu0a/TNO+90O/rRL9dopQlrf6mPch75NyUj75haspAwPNFfIJ/eI2YYfkBIMixffjxPT89B48BHm12MsKVuQergn1P34mo8I/wHuAE3Qlb4nt2yO736hK9V8I+e791iOjUCTaFE0bETgeQuGbn7stTLDnXMDUREwU28ZTDk66UyvwOU9qZ6AYHsL8OGBExhODm/+gUp+4b5++nKY7z3BLeAv32TkcEpY35N+0Z54/Af+lzHhXzZwMEmfyNu52A+Aav8quOiNnvWuYP4WZgMO+FxcEsBad+5adXKgdd9WRc7ejSJ3HWPz1YQjmqC+l8UUEniHjJlWSjzMCn1ycHtz4xOJ6E0FWAQKlIsSfHphA0n4XAc9jbESFAyIiI85QyXOfOvw70zaFDYxjmM/5NOjiPCJ6G9DfnDqcrQBx4aqISVoCgdeCw58X6nBdrXa7PCoRwPpjBoF3BvA3mg0i3zNMauhEMi0PmiHCpLwCCmB2PlJB5GurReuGJNOflMWuAoBoOQnjZoVzq6DOAeDrLWBLyz0G5fy8YEaU0rBEAQtcI6ljq7PnrRP+PAgSd8G9y4uc1YqJ00YwI+gMg6BivE+3UBPpYBgKMm+/1VRM1huZ28DZkalIWx/7MPAcmgX0nVsY/eS91yFnYEsIVkQmHOuV+CcrTUAvM1LSvR4Qc/Z6JNuBqCNllxxyKg5noaF6jlkm+AzH5Smm4amIg9nrVxCCMgXs2zb+OEyERx/lfeJO9Zmpy5bcAEZCXPj2DAiDmozTMsUYc+GzA5SuYDk74E7iPcDKZEI8BQDC7ASyOlzPzHHZYaufF9sz5zkjzcha2BDgdxGVgtsdDJShPQy14yss72BTgizAREkd/YIGmkpEO+58z9wM20Z3GHhcGWKyp1aY03Ec8MU9F7yOG+UweLp6X2zT/is5jsZ6X6xTm39zxGGKcdkrmDmSekiY/0V/6HBIdnyeueeGe7yP4Euq1usV4XX4rF3iHB2+Jbm+7jfT3Sjh86Ci6t7oa3sh3aG621r+ba9WVfY/eWfubR+9unwPE7ytha3uMgfIngXjkaN8eBOJerg5EB6ID0YHoQHQgtnJ1IDoQHYgORAeiA7GV6zcD8edQLDX9O2fO765Vy/9gPR2I3wCEp8fi9I+fm/iJniaWLeZvQ5U9ROYeyIMr3U6JHCA+0TPf/7Gz8H/X+dnivGd+0OjoKSM9eaT9UR4GD9OqfdGngS3BQ+L5u3yby8r2ptDaqJayreFOHwS3/qhIdkZOgFpne+oa2nbN307Jtmut3Lk9c9H0OJxYGwP/2gQihJAL8ZWjG53u1bw0cIN93a5IjpAQCQg6Anci7SGD5VcQRwr9i9HUZraJykBNA/vBvHxzYR0I23I5PULb6ytAJJurBcLn+t3UtLNpF7c52nattYrcQI6IM8zaIOdQC0Tkf9KgihlSAVEMIecfcQ+OLRCBiwoZCAeDY5hGmD+y6wpp5u8pNmNKzI9ahnIc9Bufm2Ivlfl5QLgNILiVsWln066oJ1hul18t189FM4kAPu/oGyAS/3PHiWAKzD6g821wYBzESIyDeYJw14n2YUTo1DRMymy4BcTlAiCOBohjmA1yQ6KRNFDJV5qoyFAeUyMdk6kh6NTkUrH3cqGpgO0OwqvgNohVF7AkjjwVKnfiOmEvjntlWtA++v8C7kWM9D/Vof6zpp1Nu2ogqF1rrYqYlpKDa3R21VH7DBA6189IRszjgQc+TUPMOIg6Nb0FnZrKGiHngPhu51UiFSBobp9LTORGpLl0Tn0BEKfrnOYycgqe0mjKyRMV/R4ux7wyuKjf5jUs2/vGjVFuBLqC2iBWjXIkwGlJLkrHduAbHQcPI/I+Hq08vdAnndUFCCrTtrNtF9ps27XWKgZX1oh5X4ztiJAp16HLiaEQuJleHN1rQLgMYDSsBIwINkamB0/D0BHDwfFENp/nVz1z4niUOWM+d/hsp/NGgXBzswshMUMSir1vYbjS+QXuBPEq0AYs6liL0JbBoV0D24FvdBzdy2MQ36PYNtd/cg0Qpp1tu6LWV9q10qoxrx1U13xJ0y7WMuVmIKJ7PxBgJbRTExlM68DsySUzk6+GcKSzToBwE8rCIOZmcUqZGpwFQu1NNM8CiBG8CjMinIwPVDChXTiDg9o8F6EUPADBE4oAgXm+XKvZdrbtist2rbQKkCsQgfdVQMiUy40IQhehGdg7YRzwjgxE8DUQmZWgV01mjSCDYzY46pRhr5rKIA4uX0MxotQxckZmICid2psIAJ6ahkuQi0ZdI1APrR44QQImr1GAoBVozECEPCKc2BZ1ZJWpsWpn066ooym3a61VUa99eNWh5No+XwNBixUWa1yKnpRxcKErelo9Ujgx+4CAoN8069MiBnYC7iMicxX4ep/nUjoPxODA9xEXXG/jmnvOnUBZpKvxfFdB9xGJljpwHKg0fKMSCxDDBZ0/n6GyWM8L/AXpqJ4obSHrAi/giTudvtHxuSvkzob5evkeh8olCKmlVJqLdTsX7brS6li1a6VVuHPg+63J04QVqX0DtTR96MbRP3i/+b/r+q96v79RQnzwzrrcjcSVC9HHt/ZG7JGWrrfLP1afMyPiI9tXe8RR7s/jLwGxvM/vz5reBcSfqL8D8cfr7EB0IL6aeR2IL2JeB+KLmNeB+CLmdSC+iHkdiC9i3v9rIP4Wfs3vtfPXSq9zf/Bs/3NA+OazA8Hb/CQxwfEyu+4muEBd46CPrvyt+RzrW1zkqbZkPqNNiWdu89POcKuxZCU8ezXjZMMQ29YbObjKK0rHs+N7ObZsA+fjZIGI+jx6pTPKo396gM5P3hN9HzaACE4d/b41n5/ml/M93AOiAaWkzOWHtvzy6+dPshJ+jNCkW9iT6qPe5GhHKDgqKJ25Ik2OZUvXgODc4HxYfzT51qXMsAIEOjlEeT+eXZxBmBkVU0KBcJtA1OfW7wYCFq/JwzT2pKEFYgGA/eWl9MJj8lsluzVAkVs4HwYIYqVoma0F1MNStgIxDyr+hmIKUwJOb+ZB8INiy5E4TdCjGJizULgfypUgPoSwJvg7HjQPwq5QHgVYE6RbIfsuxLew5SMPGCTsRBUgjubI0h5yaF6FhcKv1EqThZGRKqZFks6j0pN3NsdaS7UNUN1AecgdooKqQLDrU8osfBI4xS4Xso+dWjErRrioQFRMCfbBEqOD2QfZzR6EPUFv33s4/DP3gxyYDHEa8yvekWdBjEJQAzKPglkTZKzs49fHR1O+UgQw4+r5dZrGUI4s7SEg3sTnjlaeplJ/w7RIjgUZuA/ALck51lpa2lDKQ244SklTIE9YdFJImYVPglEN++b/mCCpfuikQFimxOxGxIQTPC/UGQjme9CEBvPo3HjLXlrSp4hzTmVYDNxauM9nF+Y1KhCZNTG4DEQoyhYonzoJjAysZT55sakcWdpD9ioQA1MAgqvrN0wLGRFUOsZbybHW0lJG+Y7ccTEiwEpBmYVPgmkT9s0lqtIKZUNn+7JEMFNi3pv0GJMGhe8Avkd0MC+k6GogmAFAIwIMC6av6XoQ9WxKhTWhxEa48fOIEDIDaV8Ip0pqVCD0yIo9KV4UCEpVgND6C9NCO4/LxMKac6y1VMs4Xct35JbrI7NGgJWCMg2fZMJiLf0SnV414TuAqJgSTiciMzVJN4SiK0FnUuZ+MCdkpC4+TsHlqcnp1BRHrBHCo4jK6PBmamIOhmFOoJ75/GIORAFCU6zYk2J8ExYKTrcADh7XXzMt8mLt9JpMc/jVlpo2yHdaI5jNlxfr7I+Oenlh+STYk3TKJiYz7iMcfz9NNRDE7ODuF2IjzcxgTARmRxBrwQufo3A/wAlhRgNYE8yrCC4v1he5noqFNcHaFYnLZ2WLaMqXpZMby1ZezdQ0gVuxYk8k3gZYKFjSA0+uqL9mWuh9BJWOu5ScI6211LRBy3PIXe4jMkMjYuGv+SRYrGGf8Za/754yhodTvmu/v5Nim+9Qc0Pu27NOf14r3W/WFqty4+LO+h5DY/We5H1AbLzX8MtA3E/xWY84PqP02Hz/y541/drWH/p1IDoQHYgORAeiA9GB6EB0IFwHogPRgfi3zP8bIP8cG9dEVX5r6zsQnwOE+s2UURBUNwKvZ9Me+2zplB+vqdcC3r1hajUrNs0rrAbxQP3vGptUt7ki+Wg8JVa42EgXm1zD/RK5MNjop632JOMuP01rdcaqpY9xOmIDRFTdCLyejRShqkgRC1mbg5kK05LnUTZfgJjKC8Aowi8ewvlNvoQVpCCPA5xVt4AIm2UWCoJvgZjU/bQFRKh6b61Ow99oOB367LfOmhQIUbdIqhuRH3en6NaAUM82P4ePmb+wAUAZET7LpuigWgPCPQZE8PHjQLhNIIyNK5YV7oX2XlundxV/o+F0rAABvxt4COBs4B3iIEAc2aURXWEfRHdkLgI73FUoQRxNVLzVtVhhSwir4ThZtoh3YHggx3GC9gflUg4IvzwbldOhTBJ+dTzzP5zV1jBcEkwew+SdOnyOkm7gtqDVYJqgXNiI7io8FPlMhXuB3quPR9iWLH+j4XSIf7DSCAniuiav6xuzEcg1CgqNRgXBu9fKPjixH5p/yfnMhZOvLYL2VXQtVtgSwmoYg2WLcA5xaI7zvxM8yRH6F1lcKCqnQwkMXvZSsyHLMqqjlt2nJXqJY70AdYGOko7qhSMUQiXstB3VRnRX4aGMmVGl3Avuvak+Lq+/Ry2FiTQ1p+MyhpbTwToXxLgY2KFNbATqANWN4AggTPkq7AM40dlNmUcEulqAMLoWK2yJitWgq4wXgQQVUQBExJeg/Vhk5+9ROR0NEJnTp9oaqsJh9DNmWzIxAqJdrogxBBZ6GKQGtbF0F9kAjRFcqij3gnvP1ccFiKSl1H462udWOB10XQRtFYp9wKjSAqi6ERI9JAEIsA8yEGNZI6CfoZ5kq2sxLtQ2LKtBr7uyAEMFRADHgzkgTG1IyunQGsh2arp3BYjoigqH0c+wDBXUMSkQETbT9wQVD+U9yoiQCCrQGsFiDe4F956rj7P2QvK5pZA3spwOt8bpAK2MGRcEBDETIp+xEXyHAEkbyz4QLgIEIyCUIFcPdkS4dbYEnOnaSCUptNpmGYgQzdSE38EwSfiqSVgXGYhgVTiQg9sylqmp1KKTL/Q7uIaxZl4YHkqempR7wb3n6uNRQthoS4MAUTgdLi45HZgZAxgXrG5Bdw0XZkDIiJi/B8s+YJTpLBP8Bwy7fB9hdS2WbInBWVaDBUIXa3SRKn3gQgLH8BvKINiH+wjhYDhVwygqHDl6CR1jlY5SBxgpkXU8Akq84CKgZl4UHorYkgr3gnuvOc6tnKRf2DbfcjrSktOxccFXf9u4NMw8t7W7xRiU6eCb8j/N/X+DSRLv/L691cyLxy+sa9tic2f9TtWNx00ud9trhevRZXmfBcQtJkl0v7bdAuJx2/qzpj/cokdK6UB0IH5vIzsQHYgORAeiA9GB6EB0IDoQHYh/xdw/V+7n2bT9618g0XQgfgMQae0BWksqMEdCVs2/zXb4WIwVLXdLv2LL3nU+RZXiA/ZoW8vz2NPil76JaNkaxIRBz9QMkca2sGjYom1pE4ikDiGwN4LbYm98LMZKZoWEB1gUC3tvAJE+Yk92fk3qjWA3T/VL3821DqDAjh/vClfE3waiZiX4iiGBLGB32CM1e2Ob7fCRGCulXBfvsyiW9rZA1Ck+EvOlMDJK19e/orwMbIEgJkysgLg5IlpWAjkviFmQFuwOe6Rib4zKdlhhb3wgxkopF/oVt1kUtb1rfIo6xQfsSZmRMZb3sfnXqL+OMj4q33R+H9w7MElm3/tF1TuOaqWomriWleCFUUFm1+wOe8QYFxH7ZIO98YEYK6VcAHGbRVHZu8qnsCk+ZE9SIgD5GVVng39d9Fe2zLA1IPYir/Crp13jIpEvH+2SyC+uVoQAxRJaGC27wx6x7A3t9FX2xodirGi56mO+xaKo7F3nU1Qt+oA9GQgXjEJB5mJ4UcxpRwTEXpACnBKfvYoD920wkV9crQihDSMtjAW7wxyx7A2X1ZNW2BsfirGi5Wrso5ssCmvvKp+iadG77SnroTOrguFipAJEtUbAuw8AXAGCT6UpZqURWOdaVoJGSZnnzFSzO+wRjmwl7A3lJGywN94dY6WUG9x9FkVl7yqfokrxoZgv0tbCF5FfRXVDdeAsWwPslzw1hXpqkoA8zBlh8FtWAi1tHBcEwWUMu8MeYRqNsjeEk7DG3vhQjJVcbnD3WRSVvat8CpviI/bktuaWnvRXyqobU7mPULYGM2GQXtRCTrw04xIpBmiT0MWERKhsJdXWr93j4sj72Bu0PRZjpS03rly+3re32LOd4jF71my6dWf93hgp71T7jCv73sfeeHy7DcT7tsdVQ95jU3/W9K9tHYgORAeiA9GB6EB0IP5AuR2IL1JuB+KLlNuB+CLl/meB+Pu6889a9UV6oAPxGXWFEq8k6p7sQ+bnN41/GK5Aza0cD2IyyHNOOWTVMETpo2xxnZ1hnxfFxdFiSzTWLxuU5SryU9F1ZY6lVav14pDU1MRdicu6bls3lP1NgImA3gw1EBIYIwbN4CuT8q+gHA/W9AgNk6F0Ttu6tMXOCNtAJL8BxPIpLawK7JZpnPo3rUIv+A0gwjJSS1rW1VoXGuvcRqwQ6ZAWiGMDRGlkC4TEaCFNj7gFhIYuKCyLDXaGa4GwRx8bEd5lh43z8RYQS6vCWr0OvvDSTy3Lpa7rLhAx4JVXowohbIPgNI6Ik+D0WhWzJcDsgBi7uF7y67izL2xALAY6v5JlMqiKBvQBjuK4UZbFBjujUgJpmRmwdoUZUXFK8CaxKtorECUWC1q+btUJKh0Oe8m6nCJwH8kb5DXLJdc1+oV1dWQVzEESb8SoQohL7zRpHBEer6O+MC4KHSMULEbhSZwwBDkQE4gwtEawpkdqmAzi0FelD8uyWGdnWCWQJTNDHZA1M6LllAAIob7IvK02gfvxFrasYpcm+7PV7TpKingBvQ6tr1kuua6LX1hXR1ZhIHger1UhZKIJosTBoQGuscQfEj0CKFhotx45/sjAIQUgUkLXDApTzWRQIFA+SgTLYp2d0SqB1MwM5UbUzIgFpyTrhoziiS42wdOtQCyt4tZW9UQVsholuIdbZ7nwqSC6G9a6NrJKBqJShchAaByRLK2QLBDR8DacAyeOgs5EISQwKYb1OVomA3uzr1npwxWWxTo7o1UCqZkZmaRSMSNaTklZrJfsigLEulVlREg9YwYiZCDG2qZSV47PYqxrI6tgagqNKgQPdhtHhJEfMeFkhY4xK1g4X4IrjZialOMhcYxcxWTIlyUA1LIsttgktRJIzcygWoVVaJgRS06JsjGW7Ip6RCyt4l6IZWpC4BFNERCp5bLGcoGUUlYAMbyNOrIKL9aIN2JVITimiYkjwloa8z/iWyMiSWB1DeVt6GJN+bBYK8eDqYSuYTLk+CFQ+rAsi3V2Rq0EsmBmkLWuZUa0nBIRcHFr7ApwP1LYsgoWnSZdrF2wrfYcd8WFNZYL1yWBz6x1bWQV4bGvsRn8Ys/GdfwGT+KW5kZNFvYbFOZtdsOyxrjIrSnr2CePK4Hcsmpxgbth01Zd/s7vD25bPIntJv8qs+Lx/EtOyWc8dlirf21ff9b0RazqQHQgvqAZHYgORAeiA/EFzehAdCA6EB2IL2jGfxGIZcFr5JG/JUjOn+34T+2VDsRvBiKuqdmu7CFWAoTJeQsWCO9UlYPZG24ZNSWJjkF5LuPhrLzqs6DMZVAXTXJuQwYi3mtT2GjEzeSm2MFUNNSBHlbMGB4BgpU5Gr4H3D6muLRoblL3erY0cSh5F5QMUAExOFXlIF81fkknLoDIqgfst3Khcs1bYkIDhKE5pNs9GrIvselpqWc9m+2T/Pw2W+UX/VaA8NILoTjvlkBMK3wPvMd9HwjXAIGXY9dGxNGpKgexN/BLeFAiiQ9FDwvEkV4255bF0pwWiPgrQIyrQKxl85tA1GImHwcCyhzBbQCBOCIeAW1mZ38KR4kwQnsGYTgcWTKdgDgKTwfcCTs1sciLjAj8EjDFWwxFD/IiV6FmiC0lTBEwMob8EiybKHoc2TUjXJCi0DFUMVRyGm7eUVxDR5FnF1eR072DtJecNU64IjgCxyiYKp7bilya5orXiCNq9iJdQSJENNWKFki8ZttY1oKBEFbMdJT4LV61OOCPBRBjSOTDg6hD0kgncOyBlTBmGQREHilAkGfqmNcIfrM82agpb2AxpAaISK7QMahrhf3BYwFC1QcQvUTjo3ij0MHBOybEUDlNOQ0DQSwLMCiEqxJj9smDV5Gg2zFiRGhclzGUNMoTUU8+0rypMoED5wPyRjoixLYIjgb9ItGIIyt/jMIBCBK/hdlRIAGcnAJBntvgEFcF3UCRR4jXACDGUPQoYmiB8InMpn345Z2NmvIGFoOrgUBMEkgjeKevnxcg5KV3IiEkKG0oBUFsl3grGkMlpwEQIzgTgO/EHBPEL1JexVsoNQaN65K7NmY/tebSNAyEtCZCQMaMCFhBQODXwOodY1C+B/oU8VugeQLVioRzV4FAXJXc3VOU1Zad4ZtAcNEZCJBtbNSUBBZDMyI0lokuixBuKEDErPtEQEQn8VHSKEBEibeiMVQ0jS7W4EzIiHA+yZw/YS/zQnKNodANAkZBDQTl0jSSLykQsVojRBOERgROvEmjqwjxBn2K+C3QPMEQAu+AgLhciKFxnJQ2KDFJaLVnIDS2FrgT5T17DuVkgIAggw3WkcBiiDjL9Z/qZ9RTk0xnSVdl1BuCRlRxqVboKDFUNE2JngLuHsI51VPTnGe8XMCvGJLmVSCGi36rpiZJI1NTzEAEXwORpyZ0PqKrKBDSpwlkPP4L5Qo6OiSmidA30iziPcLSoDNsovsI1rFLwoC4DEZ54sS872ECEPjVAkEsBiyByHXiKCvBCVOEfk1YrOU4A4EOHST2CXgXVncDi7XGUNE0QYg/otBH+5Mu1icNp0YtjeCdMM3LKPrNLQj6DRoayKVpmH0xYdUANXVI5T5CbBOOBv2SWI8u9wCPWr4widD/WFywlcuyZSi/+ndcVZ545B7S39HQuHf8kZgpbXv8zTKW+R++Gcz1rbfcP9wrDzRzq4v/vUcc94G4f/R2nvdzTfqzpi+ydSA6EB2IDkQHogPRgehAdCA6EB2IDsSXBeLPmf3fsviLt7QDcX8L278j/8U7X/FOAXRclC0CMzL0bU36c0XclFqpwxyXzeYz3+6F7Gh3LWPCRHqSajkVsCHELf5HcPoEdVlXfmM51G1AaSXfsNFfjf5H8ZD7eu/Cda6v/64Uaf7iTXlkkHej834vcVNqpY4lEDYfvinFwOcaiyWm95qm+QwP/OhwaAYjxyA2R78BhJeoKn550iWNFLEEokRj2RadyL2kWNWUB7M3v6gXsh+tBcJvwCENytFZdL+XuCm1UscKEFU+vzoiHgGiaTmAqHRIaiDGVSCKssYSCLcBhM23BcTcyqvInscEfQgepBdmb+S9RfviJJFV8DKvpC7xU4iVQN5a4VCwqzOA5UCuc1a0gC4GdOVTq9QBboakwuvnJp9YoRobzIdI5FPn1EdheAzCQjlmpQtt0fyFWuuOEug+iANHXD6iElL4H0fNLWoezMQIKpWk8VtOUx1Ppm4DNDlUkSPHVsksDn3J3r2JDzomcYjSd3ph3OyF65K1JBxYDeS/Ck5jROT4KTAzQgKCvHSqbDE70/HaOL+8TvsHiZtSK3UINyOJQ5TXiLIHVjCzZHSiDQOtDPo2CsND7Rmz0oW2iE0MKANyMCfVAwhGnAKr0gWKG6WF7Cou6kskRxJUecAZu5o2JC/RWGw7C4sjjwjwDMC6C5moqEQo7FVzhmv+7urv8i0O7GmeS8wjAsoW3kHs5AT1CxkRiNhgVTFE5UN+ocx6T4mMhQnyzfiBpQxhoYCjgdblFkX1LzvhppCUEeLKNEBw5AzJLWoe1KE6IhBEDbm9qyN2VRYnjcZi21lYHHK6Xd2brqYXNy2BwF5085jPHbAb7HcZG9Eh3Ee1RvAASEJ/SWq4+LYbpQ6byihj5D23gZAyhIUCWYfo1oAAP0T0MJhlUVRCCv8jK24Iv0mZGHLRkMYgudMSCLW4RGOx7SwsjuXUFEEj47letCx0L6Ym4jyogE50+C4aGCV+inRLuWoSwCLObC4vXzVhTFpVDCHJxKJZZvPp1BRDYVVcLh6CO04ZHrAH3Ig3tVFbZIEIwuvKskDoeNUbkYCApoXKxBB7I2JtBS6t2FW3oURjse0sLI48NYFnwMsZuA+8tJ2EBYG9Io1Fwh+qkqHfnY2fQqlPHMek3EdA2YLuI4JQHkO+jyhAqCrGcSqp8hqR92Cx1qgnJ+ZQkNCWx7eAMmAPuBHcOtMimuM5X7SLNTM/kqqEBCGsiQiYK2oe5n6AGSFRclNpxa66DSUai21nYXHYa8MV/ka711w4VvyKWF/dv2t7//2mrRkaG8soKLX2RtuiGLfLbi+W2xZaixtxuJtb/OxnCPGDxz4PiF+3FUpP29s2cyO6NTrRv9/ST9j6s6YORAfiLzKvA9GB6EB0IDoQHYgORAeiA/H3AvGrJvxNnf6lbe1A/MEtMwxi43d1eKKuGhxR0y2YEeWrPJ67GomICUod8D7VMVLwxJaf6Sd+BXci7sYpIYDB6UrOxdMkr7Jf8YwUv8gr8L+rpEuNEXc3Pw0r7SfPIWpSa9WJgBRzPbFtQdnCeofc7vHGLP2SaiBEblzc6mEbiNQC4Z0NPC9OxVBoIb5IpItrh3w7rC4e4d3g5/pGJl6itqtjM0RXVMTxVvZap2j8+balIfiV9nPciyBKIgAi2Rgt5P4qLA+lPHhXP8muuSV+YVUd1MTX3aYdOjRAQPwhtnoVrQjzXSAilAZi6aIcyCB3tovRvGQLT5f5ld/axtvZ5T3o8QYQa2fNsoMKEKcrLFFrGyCCj0sglpFgVrklG0A4UY1ICCIP3sLcrFQxEZiHoBocxJgQFQy8hJsVLTwzQsBMEHOSWSMClDrwrm8AOwNEtVQBkTIQDMnRPQbEUQgLx6x+oXwJxEIheflB2R7Ct5AXbIsSyHSdvzPfKoiSSIL+Arm8+N3rScatsGhMzBmeLK/cW5ZbItMovwcOa07M37DRZcgiV9yEOK/4M1VMhATxBx0ROObEgVoULTxeApdXysGKMkCIUofKQqggAr+RPa8JzKkQp/8JbzfzW94jrxX4x6ILSSks8JFlJ+cFAIN5ofwKzhPBnRBbc6yVE5dSHKw0Eb3NKcBAdFFf+Xf6Gn7W5+BujCx8JDoNJ3mD+k0UGwq3xEksC1X5oO4ZEa5kUrpCwCSo0xCM4rgoNRMh4YzOU1NALBAbxwSebwABtYt2RMxLfULXy7oQs0M/lbP+xDFHjvgV6ZOOHdm+o0o+BOH5QQ1DFDzoDf5aTWMQAgIDofEkqlgruk+jaqEdrJyQoCQSyuQZVHEDeYkkCXWRKGQcsvxNRH8KpWG4OlN3DOqDt9FlZO2sgSCgETnHci5aIHTJdEbRwiX6G0Ttol0jQtasiA0QnqOxHHX6Qb34lWSN4MuHmNaAKAE1FAgqn8+/SS540hoQLPnQABFlRKAKIUk4mTwRgMM5gRoyLyVGChbnN1n+Lbck1zPWI8JVdrgSPcTnADY0bVjuBGKDyFWTkGo0+FNRtMhABAtECQSGTksSr0yACPKdiDsFiChXTVGumviCWq6imqumBgijzKFTDgMRQmhGhGuAYBmUt0BrhBOSjVo798hIxACZmqJe4M+tHrVGcEne5ALGcEsk8BSIBQwE9Vi00WUECF2sJewYxQC5WCaCxgbBfYSfJOLIRRdrVbTgeCgiIVXuI7JWxxVKHWga8yJoZp3AGtL7CEj24J7ghPgkIuLDsYUW9xFYVcp9hCplZH4FL8LgTpyydobGWglSClO+mFpKHFDwraAkIkBEWqxdADUT9xGgg2r8F7LH80WLEJIyt4S634M5Ag0OYr8wB6ZElwnCrn9A92JLZcK727cta1odG9s7lS/cHYvvpgjva+vj2hz+bo3xA6XmDv1Yo/+tRxzxbootvsY2EJ8XByZ+vNT+rOkvN64D0YHoQHQgOhAdiA5EB6ID0YHoQPz/BeLrG/l3QP1pVv6XgfDv2PtHrIz4sC/l529gYwyZjfFwaQ+miit5HinBp3uWxY3yzK+0WvRi7+wxvNLT3RQetm4qDJFBY9Lc67+wAoQ3QKivqhTUIrZa2opERFhxs9dABLel+LGoJ65ZhjL0xUpf1eHfA3MNXYRvJa3okdRRaJZsEcjcs2cnuNtBRsKdEZGbG7cSrgOxdsx9GhAcn2FhWQ3E3RHx6IwRtoHYamkFRGytZAtF10K4CCaqyAVMDTASjiyenoKwMYLyG2pWgvI+6PdxsvFHCnOi5lUcRf68RDKhnGCCgIVR8zI0RskgNajVUS0b1TItUYlprVaGKnkcs8z7sg6N7YJYKmCr1COiWC52s/rCMXMzWrYIW5lK/10nvHDs2Lsbs9qEM1FFRjA1TlMSlQmqGmyMIbMxah+sOFdZZWIMNv6IMifkjMq8ilGc5yWSCefEi+AIQ5BLsDFK4HLMQQwkLIBjxQvhiQQo3oPPFkWdg2obOWaMKnmMOfDBSh0S2wUuUkrJnZ9OzLV6C9ZytocjyeRW5RoLWwSxt+DeRZSZ/HK/sOqynkbhQShT4y17s98C2BgxszFqVgI4HEE80jb+SGFOLHgV0dQpUJ7w2v01VCXYGCWRFQk0UgqVCcuc4YlQuoGtmduhPvkor+BfVUACfA9oYtR10N4oayRegUfn64gAEFHScj8IEBVHJFrfODhTpf8IiMGp1EmyehomqojTkBUWiJqN0Y6IkEThgnZMpbTCnGh5FWAxSJ0CREwgNQZnS4gmRgmAcJMCEcUyZ3kizsg8CBAQdNDQGwoEKWO4RR20V0cE7BEgQg2EpKUJhIJcpZojUrNFyEoAASsJCIkRk7t9hOZSjioygqkBRgJ/D5dLy8aoWQngcCgQNv6IMiekIyyvwtWRTIS5wRaEqoTF1OSKSA8v1kyuMDwRKSOEQg+LTrQyRlXyiHZqauuQABuZWDb3QBkR9N1YXgFRTj7zjYFgOZlspUxNOItlYSI9DWeiiiCyyQWMBDKJFqQ1NkZhJYDDwZPBBaV4w5coyhaFV1GnYt4Idw0s8FUJJUYJ6g2iaAFwxbJUgNAyWEtkAn8Qi25gbQ6ldZbFelHHhFkD7A3mlETcR1AfMM1sisYez6vBqWmzZYuQleDDlCgzgyoLZoWUBy7F1u8MH70EXIuVF3+ZvfGeiCWP1ybaHh+0zjA2wv3+ky0rpDxQwa8BscZY+FVuxENN/HhtHwSi1FPX2J81/des7EB0IP5ayzoQHYgORAeiA9GB6EB8ccs6EN3KzzP1zwaL+T21/aWhfjoQvNkII/zupvkdJUWrIBG3CivO7FBSbsUUicXs/fyPt4M0Yndepn96pWP7w/7w/PJY23Yl3Z7Ktr+oHO2yn69cvtbD37i22S6pa69Zn19+vp73tpaDa8vOQOz1AH3M9Wk+qnzf9l2jmhAWsVRCE8givBMIfx8IMZHw+PmTOmjPDdjlHqC/zwdq4O48/3O23TvbBwUE+r3fBoLKcVLbLufA505r2zvtsL0ePczfztnau0CcCx4VEIclEM2PJdMiuM8Bwm8DcUZLqFPRNcXMFojn1+fXGoj1783vBRBUzl0gDk47LBdFQBwOvwjE4XD+vm+YC4GZFAnRRJh/gLgkha9g9StoW2dZXKHVMamDiTgOSOOFmoWYJseshKHaHz9/zg0/P70caMo5//xJHXTYP73M/9zTy/P8+e2FJgg7Ir7xXkozH6dfPxynxb7XF5pgvr3s5320H8fp2PMrfj296Iig2naOclH34zMDcXZnseSH1kJA7M+o7Zlr+/bSlv3zJ+x4+gFLyS6qh47RP3f+5r7tG+YC+ZRCYI0JZjCcphBqvoLVrzhN6yyL08S+4NG4XCPpVYjShxBRnOh4jMy2GMVNOp+Vc9PnWZNOvoOOCPpHe2k6/85zzNNB14id+z5/fOc0lHf+xnnmc5Smkv0/NPXMe59f+HymlN/d/JtPzf2c5vthr2sERgRq45G4BxBUG33jMuYcWsuzAEXzHpaaudym7LnUfclFKb/zWKBjqOm7O/xomAsQeHjLsVOCSn20fIxQhH7WWBZwyisbJHuhWekD5UE0hXQ8oFgRRceDGrxXIGREzGfXfE7PZ+p85OmV4XC7gw556rin+f/n14NTEB2n3VPOVwBBuahIys8Y759e55H3uufcOi1iRGQg5tToaKqNyvvGJeKkYLhl6qJy2Da3LJtyllzYi2mN0H/iUudTo2YuKBAaL6UGIrp1IKJrGRBvOR6JijqMwn+wQDgWahizCBBxQGgKOAgQZo14cTRZvDBEK0C4l/kMdQrEE83FLzgtD/saiMNegdAxxqULEM2I4KlHgaDyD7Btr7UoEDz+6Ihblk1AIJc7f99jLyZcHoYvXOq+YS4ACNaYgIpEAaLmYxQgVlkWy6mJVgKdmi4XWaCDRD+ZU42i40GL4hlA7PduMTVhItm3QBz2314YIpyr6Bpu8uF7AwRNCN+5zHmGwP96MbCcmmg6USDOdBVxoH20V2pRIL7vxTYBwpTNQHCueTV4oZSHampCqQ1zITALgzQmgsNiHXRxXdGvwH3FGsuCI5lcLBuERsdRQo1pOA1hbdC3iy7kdJl4dry0veh9BJZIWgx38/65k14sEDRvz4s7ltYX/KLlc8fL89MPurykvbJYz/mxzD79mCeMH3teNPMVm9PF+pnLmNPwtwzEnpfqH1pLnppma3e8NMvUZMomIJCLLrC4XQyELtZnt3If8fu2+OCR99xZ49ar3CSVi8qPWPjz9X131o/WYnMe3L++xd8ARNUt5vvTg/fat0u8d+zRWr4YEB/rjP7QrwPRgfj9XdOB6EB0IDoQHYgORAeiA/Fb7exAfHqpv7kD/2TVf6PsSgfiK2/z0z95dLs7z/7f8/y08SxPUfb7/GjkqcqzP7Cv+Mwum9nNSAWwc523Q+4UcEAQ6aTabr2HE1e/bmxhBQjWGL5qvbGp1/5O/Lbe/67v1B+4kXhYsFSG6jNuFRFmABQIcmeTGyBzCgwQcKUX2oHb4+jBwc24A/vCKfcCXiF59TYuMFgAYagEqW6xX0lT4sdXMvDR1Bq03nUgfAZC4tu/51xJ24o1fqE5orb6R4D4Ll4lYimI3+geEMJogOdJgDgjzS4DgepiuA+E2wTi1mhYB0L7fxkkvB0R0yhRXt4JhHsYCPc+IL4pEOcCxCuzFDB5EZdBXTNgIIDRQHwF4fqcyddcuBf0AmwBovA7jlDGuAjr46JKGIPwOMhwy+yAaodoc7ijaH0gNb90ayJUUJ251uQQs0WjwjjlpVglDn7LmrvomNU3jvL/Sbgp2cKs2eEdctfHkNs7jUsje+Q3KBclD1pUFEhmy8ineyA/lDi31ff0DzsUec4XNyWcjaBpkT+K0n7fC9fnLA5B4V7g3FTBkuJElXAIo7hYRU+jRHNxEntk5Leix6CCCvkIBYNiKSCUZIHQEcG1JlojTFQYzkH1WiWON14jCIisqMG8kiCRUpxEj7H/cNaDkxKq/VFi0wgjRaK0oGUh63lINJqk6h7SzhFAsA/3+ZXYBt+YRMJA7HWhJhIXOBIHpcsp621e4suIKNwLnJtjyEAIvwPRWCR4wJUDvFxpgfMSA6VEL1EdAgCRj8SBwwkMXNLg1kbEKNQEaBNIVBjhiSyVOBzWCKO+QTUM1yDRU0COgJX0F0BoSeWYcxrLgmqCusfAadHqkoc0Tk5Z3YO1SeiTgVB2A/09t0AQz2DnwJEgIHhqykDAJY81onAvZLZugIiuRCChzyGB++GEK+X4XLZAiKACBxzzWWIi0xem1TVCgTBRYZyBtyhxJAMET2hT5LNUo8aIXATiuEys2DFhsZaz2xwrQPCogrrHJFFZYPPoSjSapOoeTqPTHPY/flRA0DZPRRgnTBkpU9NeRwSWaP50YD7QVVPhXmiX4MwsU1PEcB6VhhNcmZpEEcMAMWYgNIbKDMQYJPgSwlSVCBUFCITMUd0ciZgyRp2ashJHuWoqUxPFhwkOkVIgF9FOTQpEfaxMTcRLqacm6I7QCqVTs/K8jBwRLbAKxDOTr+g+4ul8Zq7DkyzWoBPu5v/3ZQE/M9WQ7ydwH1FzL3ix4rm68Dtm02SxzkoaEqhMorlw/w0SKuwoqh3gg5wQgOzCPJILSqL7CI3Zkq/cp4Hl9uY1IkeFcVAHudRKHMw1merFOjowWDz0N5SdYhZlF0XHozqG3CzTdUE8lyLbRbaeWNcDkWpUqcQqkGxchmW2s3t0W7+zvhOp5ROiqKzFbPkVZcp445d78Fj8ba39MBC3t8/Q4fjsRxzxxi/3S0B8lurIbwDiM+vtD/06EB2IDkQHogPRgehAdCA6EB2IDkQH4g+D1rd3bf5Ty/o7gPC/vHctzWM9GdZ+8QfHu9ao0z5RVJD5d2JdYESTviLIgMaZtlGnZ3dk+t81R52WJ6X09Bfi6IX5IK+i1qEpZItu+SuuHNnYz6LcZl+wasB1CWJPWu2XtNJvcd4b1rp+fup6dewc8hIa25GUPJdpWSaxYBVWgQiuuL/xoi47N8hhGUV7XrwNMetnr8VhJzXsEocdQLDuvqsd7qx43wYTqTpLdSU+AoRPRrAiUE2+Km3trL1LdlBwVtIMUvJQwS7fomWZhJsVbgKBqCDGrcF+MPPLRRG4hn+q8l1lIMixGauGewdn5+8CgpympT8wGmsg3CcC4d1ReuEIL5Xahm/Jsky8sCnckaN6gFXB7z1HxFABU2IFiOJHY0iO7jEgjosRAaaDxEdJGBF1VJer2HTMLiSrAFIzNeBoKcogaBFaQzWepsy9uFBN4GOUuCiIgeLlkTX4F60dS/4GqygsbeXgDEccTxYIvMpcsUySvIROLrzTBAchKwFEZhYIUwJA0JpArANydlOnnliBntaIOQ+vFfjHDvWETvPsfypAxAuAYBUPWiM49AYZBcenUsQ0qotjJRDYBG6EKiIghTApMlMjihbOyAF45v3giaA1fDjzKEaqCWyOEhcFTBGfve2juFmtHS23QzzkVZogbtPjRHyPFojA7ahYJtGpx1gCcTh7nguXQYAoZ/2J9x/xK9InHTvyQDuqQ53janhZf0mDA1EoyojAwByuAoSrpgrR8RjmqerNTII06w5VipqpgUgsyDnXavzWJUIEeBQaKyVWcVG8BN7RiwjtF2vHkr/hF2mC7B+lF7zxz2GC9DXLJGVZhgJEHhE4/8oaMU8kx5BZGcLNwSevEYkZCWkNiOJGb4GIOiLMPG2iusyfFgiOPjLZFDVTA9yIbGNinsgCCL2UsEBw7RNioHi1ZZIRUdmx5G8wna1KAyu59AUQwhBJ1nZeIwQIxB/hGQxrBMVTCcJycIiS4uLFABHlqinKVVNEyJm1q6YbQIwZiFDituWoLiOGe1l/QhWpRbrEMDV4kpDgVGRVBmLUkoVHYcI4BUMIU+EWnc3Dwo4lf4OBMGm8hP/xuRcCXytJaB9Mr9GyTJiLMPC8zhFROEoJX3ARE+ECLgOgxL0C7iNE+ifgPgKsC9DGohAs6/sIrCr5PqKemi5gPgThfHD3lKguHHQNrA6UDHaFt9FRDFMDuZyEa8N+YU1ctGTswW/wMUpcFMRAOU26WEfnF3Ys+Rt0gts0Q0JrTpP2wnzOp8wIZwvoPqLYHh+89Guu5n5127qz/ty4oOZS8cN51nsh3ilh6x76Zr53AvEp2597xPER5oTNEz8ExF+z9Yd+HYi+dSA6EH3rQHQg+taB6ED0rQPRt//OthVG4H6Ovn3ulspn1H3to7JYvvKrZ20ZeLY/vQP+9OEnNmGZc72keHMffauVM+Lib/UtN3rYNO00/e+a7j4yvH3EVS70sAGWjAf9v2bchLDaa+t1xVoVw6YOt/MyEH7d7qqkdq83wUhQhjcMknU6wlqfbPOTOPL9Agj/OBB+UWmjKZGWQLglEO5xIGLY1r94BAj3ASBcA0RN5HkvEG4DCCsBE987IrwMrMJw4Fdm+VcKx0ljqLCCRAIwQ5Zcx+u64pqZ7itaKBBWFaMwNIQ3oVobUiJivByLjocodiC0SFHv0FQnKfOYjyVSAxDOBwcRidAFAG9jixdi+0QmJukNzadp1cMXElIdc91rKYtthXniEN0EXAZlOGQWUkjzX31pm717AoTP7+i7gHfquZQHFC0UCKuKEcRDTrngJRatDSkxceSVMehL63DLi9tU1Du4VElFGcf8a+SFzUtKUIxi0qmJFT42eCG2TzQKjBfCBfKJszNHmxEVmnnPKPFi1lLC+tG8LF9PTRXDAfFO3rjSAdyDKJyOBG/v4Ep6NH54QNGCob/WqhjQ5EAOddcDCJT4FqClIQ5Z7owS9aU4aiUyS+ZelDxv4klWIE55jYgCxBovpO0TrC3Ucs1HiiInsSQ6AwRYJ0I9WqQU64uOxwKIwnBwHBPoDQ7/ify0FPGkLNbgVERXNC4E2TuKFnmNMKoY0ORAjhoIlAi+0JgpbqrGUcQkiqpFAcLmaYEoI4J8zFu8kEWfCBDK92D/NaflWaECYjTKO4uUSmLIOh52KTMMB+HlTXEkTSPw+hiIYIFQZQ2V+3C5kluKFr5MTVkVI2hpPGadK3wNlEgxXo48v+rUFDkti0lYNYxRuR74JXkCaTP5HJCKj0YzItwWL6RlfdipKQPHxBiyBEBwp4fgYO1GygDGTDM1ybgwDAcs1sSxAOsDuhlelmQe2sw/gLKGWawvDyhaJJdFRVQVA8c9szmYNyGqHVoikScjM06wWHPtlyDRI4t6B/E1YBcPf8kDItw8g3NKsgprmjITt3khLesDTA9cWCjzQ3olT01McpO5guxZTcnWg5diFuu16yjvfsVJHm/88h8uM65eBr83j3t3KY/kMwSylVavp/xgd34WEB8vc1nOPZbGY0B8VCXD5luWER9O2be+9a1vfetb3/rWt771rW9961vf/vXNf0KKj+T7z3FJooNEAf8Ij6XHJs9MUnW4KWHQFGErxY0t1bFYYs7v6elq2rIt3is3v/EfV1q13Zq75eYUw4bjfcn/GEpN4sxogPA3K5NIKWHF4lDX7ZdNCksLt2qDI94vgBB/6qLXFIg7oyXHUNG2hK3nww8AERY8EFf6s+oYv2KXUUGJNuODIyIsDPs1IDbrMieGt0AscnvX8jAeHRFrQLh3AeEeBMLdBuKIqB/s9lAtjnXmwZBjgYBsEBvFikq/Q3gM+sYyK1UgfsqlvPiLVG4jQonGRGFbZkeVcCIu5FxB+0xEkmR5GJbjoQwQU9sFUVr4HfMIcSPNSTWa1lf6GjEqP0WE1YUR0vJA2AEkDjK8cEylnrif1Qaph11FEmNmFKl9brhocawzD7IOTShvXuO8lffzrX6Hc1KOpBnFRSnOTDhHkcptRChBTBTwPLzIKeBF9pOcCiUiCc4s5WFYjocyQDQQghP3LOkkjOK/1pyBVTtM6yt9DdZUyCWFqP7vlgfCztFRwxFE0XHIU1NWdhAxAY0xo2/Io0uDjecQa+bBACaG8BucSpAUxQqr3yF8ilIy4qfQXsQlkTKvqk+wxvVAjlJG4WjoKlWiMFgehuV4KANkMOmZlYLfMZicQdqgra/1NU4QNuKSWDjsGh2AaHkg3lAZeE+MGQjhvESwQ7wwXUR4oQZCz+SWeeCgMyP8hgJEVqww+h2ulDNKlznRxQBzQYAoQhHrXI/gLJiFo1EDMWYgULPleCgDxEQwofNwYuUQAwTWiwIEPOuVFgjpjTgpyXn+pUDUPBALRHQ1EEV0JggjJEBsRqOAcMWixYG5HYVWzIOgU1Up0ihWGP0O58Cg0NWHJyQGNE9NqraRgVgoXEChgycS1bbB/y0QRw7GUXgYluOhDBATwWTUc3gURkfIUiZmRLiFFkgUfko9NYWWB7I2NaHPvMt6ITGfZKPEmMGMSwtY0eKIbo15IIs1uBtJZaqsYkXR78AvKseVxZpZG7pY51RBFTEWCheIicI8D1qswYmwi3WOSMIUlsLDsBwPZYDoEuu4xEGJj8zo0JzBgQVyuqEFUkoS+a0FD4SX32qxRvyXk4hwwQawQ04T+stEVInmW2wuHpfMA+/WLuxiszdWOe8zLrY3f/PeYOvOYcmseHzbsja+K/UvbQrE48yDW0CUnPcZFx+3eH0rNb6/7C1r47tS961vfetb3/rWt771rW9961vf+ta3vv2RzT+w59/ZQs01+J/EeLAptn8NRZGj2Z9LjSUyi8RkYUH02dGS6OmkR+iC5ina0Hy2++UZ7xQby0r6+Zlxwrt1c/n817JQqnKTAUXs83lfNHvNE6bsB9f2S7iOBatluNWXZU8QjoM9P9aBWI/C4osih6ERWM0Ll2wcCuZpoJiIqOcSgGKyUhM+v4DsF2evtx0RWiBKvfAgmNgG0WK9NQrEXyTdqmoeuteUkJkh2v7jAohQ90PTl0sg4mNAuC0g3BIItw1EFs2oI7T4hRVbQLg7QOQzOWR/WVbi93eBUPti5p7Elb12ROgu8gC6RcyadSBka3QwksfrrBI9xLvMjZiEMXFZj8KCsPVwmlo+B3giXvgL3i2ASEsgQoTeh8Y58cIV8RqzQhgf0ERQxwpilahqCMdQIY965CkEQg4WiBGi8IN4JCEsf520T5htoVr4oah5lL1wPNXMkOC05zQmjHj2hcfhXaXEcWFLmcXS6GCoKIJ6gn3mRihjIo4bUVgCNPHnfRWfAxwM5ivwOaqRWfR/zKrwjHE8FkxZqZAKVBkk6J6oChlcbg5KEEXdIuRABQiJgDOY/K7w3qkPT1UChG1CKWbHqMMEo+oezk8KBNQ8yl6MipoZQiWHMr2MzgT/0KkJPSZiATGKHkmjgxEyECXUBbgJ7NS8RiNsUEdhGZiVsORzRAFiuGJwmnhEWfYEJR/LFCJADLgA4DBQ0QChChncsHS6FnpBUQ2JGkIjYrmlsG1jIdVklpqG6qFySHsEfagKI2U1UBGJspflvxpmSHCZbqSRXYTrYoCQ1mi8Mq8lRqODIaIfyQKhc6oyE9ajsJAGhwJh+Rw6IsTAZCKzGCDAmVIgCFKeuem8m7wEjQrCwFIOhEIcDc+jqIYoECHF3EGGQpCBiOLERzlvhRFyF4jglsyQAkSeaCEeMVkgonJWGiCMDkbKs7jqblj2EAreiMIisURaPsdxku4aBYgSmcUJeyi5HKFFw3Zl4RUTm4SYEMzMiKqQ4Z1InYzeTk2sGqJTU0hCTZBYeUGuzvSyBHZDkiGOGYgRqcAhoXNR1TzKXnOCjpbdJECM4IGQoAaEgwwQQTgrMjUJEFYHI0kUFEQGETmgyTAmAjgRK1FYeMmHIoflc0QHzQvmVfBcjfsIykX3EbRY8V0Fd5i9j1BaJuI9ggnhDAcC0ViwWINXgsUaqiGgQSLyl1LChnwH4UltRBZr1prCYn1JOjVdYA1pkgwclE3VPMpe3EvUzJDgcgwakDYvuDOjHj4JkwM9lnVPptMUb0m93biou7sn3kzt3k14by1bbh+5H/YPlusf3LtlcXyoD1e3x1gbvwDEh7ffxZf4fTyM9ZKj61vf+ta3vvWtb33rW9/61re+/X/eNDhND1LTgfhvbtH8rb8ZWYip7KmBoGfP9ROVsPakK7j6YZ98H/J31FAis7TMDhgWVyxua1t9zubc2sPG0L7xtB0xZVM0ISz3xtyGwvjw0x0OR9gOX2FIACH4DSCom2KwzyS54/CtbkAFhIlNgjjoosddurNmdjgOq7MGRKG7rP9ea7qvgLDBPdblGvK+4DeBWOpyWMaH9uCWYMajQLitEYEXwa3BtGf1TArLk8bnJKFJ0jI7toGwta39tqb41RFRYrD4TZ0QvwDTrbfM9KBlfAR3A4g6mgi9Clt+12yMgIgS7HBZGxHE1iA2w5EdKXClHIXtoSoaeO0V7huXBned8I40c0HIlzuJPgizKRbMDqYerEU+8alW8qDaUxgK80T/XVxWxAC/A6Lv0OTg9nMdG/ogCT02GF0SjfJCfIzjQpdDiAtJO92L+6zt2whnZVGxEHelqkpUbAzydQVhRxQguOvw5jdH2IFyBtYIyu/FOzvK6+rFoUmTwJvoaJBblToAfrkQ4DteMDuc34p8EmslD6qdNDhUmSAfyXFRmLmRNAwC+os+y9S0og/CQIhbNBSdEnGK8svvjS5HtIwP0IHiinJJBiIzNWL1u2JjsPNbGBPLqyZx7Udv326OSjI4mdgmA5Q5uLy3HE8icAcEmaAAxILZIR7qZeSTNSUP0uAACwT1RafMDuzzrK4hIy4KEcAAsaIPAiCYscFefdYpsWUsdDmiZXwEmbRXmC54Z7+oSMyDzf6u2BgAAg74LSDmSWkVCNWlQHwUjnLCfJE8IlaBaJkdskasRD5ZU/LgWDCTrU+ZHdjnoa5xE4i6VCUbMGMD8hguOFtGq8sB4oKuBQGMkxWmS0jC14xlajK/azYGd1UEY+LnTzASFiPCgd/QABFUlwJTE6KcJAYCawQxPBiI0ABRMTsYiNXIJ2tKHqTBoVoddmqCtczui2ZqCmZq2tAHkWgpeWqCTomZmhbxWUKyjI+Q2SiLvk2ujiYC7YisKlGxMYIodBBj4udPjrmSChDEXThppJGkFwJg83lwQvJizZFRmC+SQsTSeAm8QvgJQJACxpLZQT24HvlkTcmDLjyyVodZrGFtzK0pmhxH4YXQ9zV9kADC20V5LtAp0cUabah1OXzF+AjCkFkql/gleyE+wK9YGxE29z1+Q7xbw/u3+M5j8UOlPpJrTbfkgeiMLceg/r1V8dYjjse4EH8vEI9sj/JBPqXl/VnTF9k6EB2IvnUgOhB960B0IPrWgehA9K1vf/nmv3BptlT/m+rzn22XMieYb9AoXKx8L1sqkRKIccHvo105FMD8a3YjSeiAEz/t/N+V32q7DvM32ufT/6767hnXkMqTGnKg4Alnxa4w3mHEW6l4EWHV8uTCiuIGH1mnXpTg97KDbU9hJWZGuttDrqq5tquNR1MUNKKrZAqCb/QeVlDMQMSor6vmd6GjLy+5FpHyIqMefX5r1TWKElHcEKVTcuSXYBpimSOWX7EqU+EXhhceie1WvwCCLE6h1ly2RIAgveUKI6EyxD8AlmuaXwFhzA0bwzJnzfEljMAD3qln/5wFwvl83Ge1+wUQEEU3QLgNIJw5cVaBiNtA+JXzuwVCXDux1iVZABGX/fkOIMBnCPrKuIOPSsXNB3bbgGfBzhCH/5m7gOF+UYaDqMm7MiLI/OMDQFCaqzht1NIjlThS16rSB3mK1bmkQMA5Q2wQnlIvXtU7LuoU4pAfCfoX3hXHkR7h14JFV8Q7VfioWSLyrnXydbSWhElLlUyIlTJbOilPYxB1k6OUmEKx6zjpFIx4L05fxNY3fWePWNTIH3NXjpiamPcwqvCDd6o9wNCN2qUnXhFG/iS9jRNei7+I3w5CDnxkgAjJlF9lv7CHebRAzKMkzV0efZZswEvzxd0JsRJMSG9wbo55KhzVba8jOmhYEXWlig4CprZR0qnCR80SwdvaZH0VrSXBXvE8a9SXmHkaUNyQmqnLi11Bp2OXRPWDWRB+bURc4XHWhoAJ4VkZQ+du53T+5zEVjw4jgqeT+RcJOXC3ySjgM41nfx4HfNyLz/ctx0Ux00zwSQUeNK2hsiXM2aTIIUDQ6XAVxZCIuC4hz2dUAqltDK4c0ektKhCi8FGzRLxY7+toLQKEKJmIR9pFMFRULgMeeCrxLRS7Ql6+31StA7SWoJFPMhBwzFsg0JlOuDluskCA1cA8IVoj+FylXypuUICg4wACx3WxXgciZKWNqLo5rgUiBgtElC4YWIFDzk2ngd4491SOeGeBAI+wEHyUJdICgWgtukZE1tmAEglr3LgtIIpdwWn/vWXVKiGyQDTHADHK1KTXU9XUFBdTU6JVIcpVE6+0RWUiWiBivmqKBQhvpiYjoRKj0QIpU5NoWiSX5UxWpiZ84zknyRSSo7LkI+BlqK6IkHVGmZoMS0Stt9FaBAgVJhIVE5maCj8xKBCXi9pVZqDCNRH2BU1biCkEHoNEE7mompIuyif5n/kP1WKdaH0JuI/gqSRpbBLhTFxPk05xWDZV3wmqH/OihRBsE/ghzI9Ip3yfIYocF7BI6B90PE7MvEjMRAS/QhZridhC9gMIDTRmj2CKUF0R6h0ofLQskTyxmmgtcj0kvAywUpgZpjwN0Q8JTkmWWjtFW9LFmuO9VIzg8N67xM+8D/bVd383Tukyxf+u9Tdq3P+2qMjmSPygxZsRV2JbQ/07hrWeM6n/azFBfnd7tspXTte/ZVff+ta3vvWtb33rW9/61re+9a1v//+2P0pK8u/c/76S/R+3+7cCscGI4C2aI1sFxuZbzOXKq7FxyM3LhaSVUoNbY2TUMVomZ0tIdRui29LUiIsWFu2P1a2yW9/YW6qXFBuHjVpFUXrZMxaIhs1QMyKMPIL6bpZ8BV8JR9TV5WelyWcDFo780PAhlkSA6v2824rIsXo+G1Y0MkoNoQ3oEes81m7zxqxrpDKyje2TYZ/Df8Sw3jOLEWEETO4AUdW8aOR9II7NhFLixJTQJOtAuAeBaEpfsdG1EiVuvV3vB2K5k9KNt4GQ2B7stqCd4loxjAgwHErcFX6Z1fAVKFWJNlLiryDCCDgMPleb9TCSmqyRXGo+xAojI0dy4Ykp1GyIYSoMjBSOEoVFpeM1rgo4FceqhcENYm/TLleiyqjdSlVDzdpi5XvATaZRXJjpYXtGYgcse0bEloKoaOAMCeK6KG5HMBzye/ISDqDwFepoIyX+ClQDRiHNaINUyoEaxISWHCul5kMsGRns3FSmFOKfGDZEUfgga0ZxwjqmqYUcVwUaGWPVQtYYEc+xbVdhXBW7XVS7R04zFlUQsVGsF5WOKg7LZatnWNEEcVQoK9gLGQhhRAyukEMGF7KeReEr0HJjg1wUetnJaXSGAsScJzYjQkKItHyIlpHBoQquXjkdqWZDDMaT/iZ1gq0h7n9hT6grt9QQlL3h6nYVDqKxO+mIKCehnJ5io7BNWKWjCf8xbvUMjwh0AsJdtEDwTDaNGQiOu+IKEMJXmLaAaEeEY3IM3PVmjeAa4oIPscbIyHQEDUSW2RCsuNEAIUukts+pRkbdwqDL6VS3y5BBs93BXCzEHEMpiC6Hy0Q6UekYH+sZADFmIEYlYtVT0+xGN5wEnZoKX4FSlWgjVXU8EzZAhOjqKwvUkFo+xBojw+5t2BCquKF8CZ2a6DwMQeOqhKzVUWoo02LdrgYISLFku8fcduh1SNwWmZqg0kGryv2eEf0GYiQEnifBXoA2hGVERIk7pWwFCEMVvgKEnzTaSIm/MoiaxSBxRBxHZRyk/HJVghpSw4dYY2QEtlE4HdeaDaGKG+BLQOcpL585rgo0MmLF+QCLRe0q7TI89WL3tVBCB20xM0R8DnemUVxAHL7fM1bRJG9gE9SMiOhaToLwJGL9ez3/4gJv5cIzuq1tnZGh3/1qKWsp1+1cTx03zNnkbWxaH29yv8sl7IOPOLYqusdX2Mq95DHEO2W8X+HjV7atdm1ZET9od8nXX4D7IlsHogPRtw5EB6JvHYgORN86EB2IvnUgekd2ID5h24qHggD19JzT0fPHxG/EsQIGuWWwf/Y/8f75qMZskMDytJ0kgMB6teLSMS/tD2BBTFQ6PfGF88cn5U7wW3NVGW2h9ihsu9v8NWbJPQ5Jw+qYgci+7GEqkeOhO8Ke7sSurIl7j4TuWQWa3hBM3GJ+3KjxUMom0R+cREPn9y3lHVG8MZr3k1Q35BGiPsQyXAsNILDapuAbIFRrIwaR6nbs03GogVPFhiMRXBMaw1dAQHu9deY3DJQVZsldDklDJpiBiEZKIsctUScU9wJbI+/bsl8j8dEUMiWB46FUhiIeSpSuhDi6vNwr/jHdn4JEYtC3sFsgqpf3bgPhMhDy7rZDHeZV4rZbQ/vFdKXYdnMuqDq1Ypbc5pAsgUhLILwBQt1N9mVnvOiME9vr+R9Ze0Oih6QyIvi8T07c4RYI7KfGHjMQrGtx4ZdaHYZkAaJEDxH9igtUNlSbArFMwJRgoQbUnooNcKoUjoThk/BAh9i5OJKybUghDJAFU2OdWXKPQ9KyOqCUrkCoI4tP3gJEWgcipAwE1ggERcHZJ2sE1JYiz9b83jSA4BWB958mfm/+AiBOSV6UV9UL8nfJJ08rQR2b+bVyibNSAgbwS+80oUS80ezEBgWi5khkPgn1R3Z1xlg816QE4iUsQVxhaqwzS9SNyv66FQ5Jy+r4+TOIzgi3bcQb5uTXw8pKdAUtj/9PAII1rbCSVuszt/hUvYFMywxm4nZEYD/rS4w6IrSzlB9xlPKgOKDRQ1RIhFQ0zFkS1ctMK5ZPY547uS4BwnIkDJ9kLvFNOpnYFhpRhWxTcEKOtVIrZqwxS+5xSFpWRz0idGJ1KSv1BNh7rHQYiGBwlLGwAkRZLQCEzHmLNQL734Ke/xUQozAokpmakkYPKUCUsBsWiOAKUaVEW9E1x3Ak3BoQPEFOzhWVC0Q5CTnWSq2YscYsucchaVkd9RqhQCBiyzELh0F9pAYipgaIqECEhlQoemQrV00O7A3p0qhrBA3M4eKNZpOWhughbL6CNcaVqamULno+0eh4GI6E4ZPMJV4ueWqy2hd5X3CGv2EUM9aYJfc4JDWrg33OsaztqvYR0CsXA0S0V00MRPSuUn1AtBGJHpIKEHT9ywtPJJUlmu1OWFxkP7qa1ZmuoGjx0hYkqFfIPAhe3Jin4US/gjgUF8RZKbFMmCFy1dJ9CmJDYJDFhiqSCVgdflJKpUhjpXIanERRY5iWTA23yiy5xyGpWR3MwjD3EZojiu6IRA9DuCu5j4AyCdYOyH+tbB9/IyDm8bVyLNRp3rs9btXavcMibky8ffyRYzFfpt68s75/S/n7tjX2wp/ToHik9papccu6TdWNvL8/a/oiWweiA9G3DkQHom8diA5E3zoQHYi+fTEgeviaL4ZzB+J92wef1RG3gPgOGtsEcUro4dwSCH5ddqLndszNCEtOxW2OxVKngp5KJonB8sgW7yepWSF37FmxcJJY8nfr2LRNnlMv+RvzZ6Lv/7uixbx/9mImedofko1tkrSuAkT25uNV88k5/VXRAsKSY9Ha7JvWULHJCq/f6LGi9rFdphPvfZQjfnHkXr9m/9otaxYtCxaITf4Ggs4UB1sAoQBASDiaEtskafNWgIiMcZRHwKtAuHcBMZLB8TEg3AYQ7gYQ7gNAjJ8KRMPfCJWnUzoW3u8KiGMGwqUCBDsyaLjyhBQECNGdmJSfASaEql5g/5GPQD3jKLFJKHoKpWGFDwMEVDJU+wPqFiUCimpa0H4cRywSiXEiAu9HcRYdJYC9WsMMlcwjQZkUhaVYgtr4fW0GorUGta217CoKIqrysQAi3QOCmBzgFkQT2yQ58BEiKUTQPzMiOEMUIPByNw8647hU1QtHv4O8lc+/R2FAsTdZwwFcKIYLsRngQB1F6F/jVxSvtmpawKXrQM2SMscgjh5mhajiRrGGWshHgtEACG+1JTF31oXmc42bQi2YrePaQlhr2VtAuaM6X/nN7XX+BvyZXqLO4G3wyAwQ5haEZGKb3BgR7FhP1OiiO8GsCcOpUNULYmjQ/0FeEgdjowAxYDiPUdgMTnQ+IEnB2hrs21XuhUopKBCDxCLJJ0OuQxU3rDXoZuWRDBw35a22JA7SKrLJOU0NmCh1zt20DEeicDUAxDp/g0pDXBq0mL5nYh5xC4KNbXJjjfDSXS7TQsBmAD9DgciqFHI+SXwRHREIYxPdpIt1kBgslHLMQLgJQEThXrRAOOZU6SiDx01HTzB0ALEmZnUdlD2nUCDEkqiRVpB3DNoq2PmWc7cte8spY47BtcnfCNprEnWmAiKuAbF21YQgL66dmoSfoVNTVqWY/5HKhjRlxBoxXPKEEAwQEd1H+hQ80hdTk2paRCHGRuJtjLruFAYJT02Zr5GtmdcIsqeoYsTRTE2hmppkjWDehkZHGam2rDJTtYxiwSh9LQOxzt9Am1xmwzRASMCa+/cRiHHizGItrIlL0eg4FVWK+V9kxgYP7gtPNBOoy1DIMNNbQMyTyPwMPxU2hC7WqmmB2jQ8G8rkvaiD47IgXW1NZIt8WawvqbIkszXkPiKCtyGxVub8Eb+XLaMy/FSir2gZS/4Gc/wYCLQYQJyud24B1+6s17LcZEBU36N7VJ/i0c3Q2u6mfG/Jv1zeZ/E3Hn3EcYsBsQTiUX2K9wFxnyPy/pI/t7w/AETfOhAdiL51IDoQfetAdCDsZ986EB0I+9m3LwLE3wZIbe8t6z+/ZZ9Ym+9AfFrdDx0b2EdETwNZbDfgRfD5WWFaAsEavnVu8521iSd8ggfhJ+gaIGZJedzHTybjrYaUZ0fDrWSmRC8v02uxJ7J/P//j7SCt2J2XhTy90rH9YX94euE0L/Xx55efr+d9ve+waRKVo33285XL13r4G9c22/VMtdhSxYOaX6WV5/9pbWoK+Slnef/SsjY0ZojodyBIStBYKQWIqFyQqk99ZZVbqaF111vhC32ZPuNLQBzQVMLj50/qoD334C73Af19PlALd+fd+YDU+53TVNzp8+8lEHWacpjKcVJbnWbH37i22aBdzrbTTgEQY0Ao+TtANCdy9Vg8GiBi7qyobw6Wl/+icEHC2sntqu5c8D42gMhMkAqIM07d/dxYdM0h91gLxPPr8+uh6dQCxOFwe0SUPFTOXSAO7rBrq1IgjgzE8S4QxFUgRsUxszY0Gsmch/1jxwkKFeImmSc5BUJcL4lSsMhEdtMwJwKKHBOsIpaFMjGO+QXcY1YMOWVXlOhewFk1KfuCgJgbe356ORzmaeD88yd10GH/9DL/c08vz/PntxeaIMyIcLRvP08h315eX+jv0w8AsZ/P8qeX8/4b5+L/56PO4S+VQxDTER0RVNvOUW3U5fjMQJzdecelz9btpR6ixJCOBilqzKvFRXQkNoAYOEpDyswJz8oW4pic5316E3kU/Q4uh14OF5JJo4URnYmrgJgqo8QlgY/rLbs7x+yLJq4WMzQyb0SmpHQUMoyyLwgIGgnzlE19dNARQf9o77xv/53+uqeDrhHziTrve567bL//Z071nXa5Z+nCw/48H51z7ikVHaWupr9UDlVCR3SNwIhAbTwS9yiFatvx6JjL+k7ptR5dI8YiaRLdFhBYB95CERLxeK2bJwroc+inio44EeuhiCwqrgUuSDCiQxI4I2oKqiUzMaSGAG8weagzb0QDj4x5WqRjg8OI2CsQMiKeX2nJnM/U+cjTK8Phdgdt4YE7juDbu3/2ABHrATrzn/no8yuNGICJrnYMKwORF2YdERmIuTaUQrXtnZwic2nzCJJ61hbrh4BQ/QpOP8naHMpnAUJm7+D0misEp4FmWiCiK0BEV9dQgCi8kRIBxpblJqwRBwHCrBEvjiaLl710+m0gns4tELz+vxzyiDjoiPjOU/6L9lUzIhg6BeLAZfLodABirifeAELjfein6llcLla/wuoQidBVjr1TBFBcpYWBlIZToRFMYklbpqZMkwkCRCi8kVAINJmfweDwVdMZQOz3bjE1YWLYV0DsFyPCWSB+/MBxlEGrA4CY15X90w+Mvw0g5toKEPMSgWMyNXE9orBx1Q7AvcKJgRgS/TOfwlUgRkXhSeRFmBjQSaOd6GJd9D2ycBX0AKNGb0GpHiVdStq0qMGTosZFmRxC8pSoKIWDAbYF7iPOtEz+2L3ofQQWa1qod/P+uZNeLBB0jDr6eR4v5z0twHRnoUDQsk979vSNjvI9Bk9NdO8xn9V0xM4iWKzpgoCseOZvCgSX/kNK2j819y7VthwR9fH4DpaC37rkvZP2V7b33Fnj1mu5HfbbuQ7Vt8N+V8p7fe+d9a167j7i+HPqGr8fiK0jt87UGog65XtruzMi1j//lu0/8tCvA/F5dXcgflfXvLPuDsTmr1spP7/uX6ytA/FZdXcgflfXvLPuDsTv6pp31t2B+F1d8866OxC/q2veWfen1tZJNV/y/Orbb9jWWApl4+coNRALCgNc8oeVkulv/Rxll4to2RKLI/v7eR4reftYux+tfTL7SyOR3vYU2rdb6YvSUfu6W5b9XPa0LIXdAohdDcQZ+ferQOxN+XC86JPFXdP0/X6rs/Z72/T1PDupqfAo1kvernW3kgePvHdS+jYQe+m5valh3wCxr4DYm4+97f8aiMPh1oj4RSC2unvzrN27rRGx3zy59jdK3i6h3Y/W3gLi5fs6EO4xIA4LIA4VEGApyD/mMrAjZP/KnAVycPz8+fQD/AXmH7w8Cw/i2wt5Z7/NThPkeX6h73CHwOlxcLbcXU67Bw+CayB+xOII10cch/YIsykc1Q8exTdmarSpvkmts596pQQ+stg/Wy5AUOkHppxJOT9g/Y8f6lr6Jk6dnbHnm0x86CH0DVr4zKUQq0S4I5z6Vfbw9F1YCsWR+H2P3/8wb4H2zJ7Wg+Ev7PfsGNxzSsc8hO/iRp9THXCuPLNnmAAt5e5y2ucX5KDSzmtHyBXMe9sj5z2RwPbivv9ONrJfuk61l1rn3l0pwe0xNTV1ns3UtD+clZvhxLG6m12oTtyl+z3atzP26MSHHkLf8N7sYD0wdwQ5wRUBL8QZV2DmOBz4wJO6y/fEemAg5hQo7OmVgCBGw0EqOxyQhwE6HGRo8/G9LXeX0yLvE5f2z8oRai5xH5ZH/hG/cmFKAIg61Z7bQ9ym59dlCVTyMs+zWSOeXp+5Bi7nFfyNHed80rY7AFHs2edZ5qn0DZcFa8AAwdREqSnnk0xiGQhHXljmOACI+fd3BcLR1HSmDDPKZ2b3ODWEjP0uzSF2RAPEd54mc7m7nFYMffm+B+DtEZx3AOL7KhCY0Q/ViLBAUL3UtoNblnAwI8LUaUbEgUaEzPUH4W/Q2U9MEGk7t29n7FEgvsuoYVIbxgpbc+D27qVUAuIARokFwk5N6IinH9XUxAXOs+B5nh3n0vZ75fNgjZCpac9A8CCdc+yfftTl7nJa7W4qjfgRiyPf0czlkR8/uOF7ZRbR0f0ilU4G+9USqORlnsMhA0GTUYZo/v4d/I6zTFLoWrA3ij1MWDtzf3yXCQcgfMdEeWZr90pGm4E4Z1aUYSngHy9Te77+34NkSHvmEQH+AlnriBmxe3l6KcOPllbKs6NUP+gzsxuqcnc5rUxNL/T7cFg58oNKel7Jw5Mk1w8ehdjTpNoLhwKLdVsClbzMQ8sv/Zt7hFkeTy9Szg9qE4BA67jt3L6dsYeBkP7YvaBvuN9+7LgPztxe5o5w6jPzTPa3fdY1t2AeEXu92jqsXuTeL2M97eGRUjb2g31xuJt/+9jB3d62+B232/cRS25sFicaER8BYon1ZwJRH/0dQLynjx4r9Rdr7M+avsjWgehA9K0D0YHoWweiA9G3DkQHom9fDdy/Aej/7Ml4G4j1N4c+632iX7X3I9tt26uHVsXj+rRI+LSy7z3boSmLXjCstTJYEzg4ifHsEuKsNG8mJdVqbrfoWLlDXyhbvFhGLwhTNJMU1nJubJXYAYlz+6nYeDdHVT61qG4LfvkpA7FfAWK3QGnX8DtaJ31hRawfP5ijeObPYghOtTKggqCNhDQ6/jbn0yYQHvk3OkRjqbdAhAqIcBMICnVeA+Fv5ajKX7YlFsUSnKifA8S934emLAbijHS7AkR0sQGibdkmEO4hINqg4u8BAkEH1Ma7OZZALCwuGc5WSUImDmFuZMWKV/JjKRDZ0fOqjA3e+4N5DE65Hovj9M1VZZ1JG6BoZZCKh2ftjgIEB7+QF4MHJxFLkmpupKAxTmSPgxq/xk85yjGodygQHL/Eldwaf+WaNT6OOU8phUqkEcFAiI1cgjvlSCoa0QXqIhqjRcsHELYtkS2VV5N3lZKEnLvZsanMBPLsKhDMadjDB7ZXXzU7GdkZf1C9iub4nvzB321ZZxJDKFoZjhVyvAgDBZ7RA7/5nQPCQEwiqeZGCmPQaEcxIDe9BI/4KRrJRV6K58mE1PN1WtDcGn+FIqPEHCcFeUopVCKtEcep2Jj4Nf0cMyZHdMEL/BoIQcvnmAFVW+iN9UCKJXR8v6IkQXyOHTE25ChYGhkIRzwG8BvIP/38WgMBANvjT8yQgDvSjoiileFYjiKIchPOH9YQCIiqojIPPCJYc4O0CThcwdWJdgIitqALNJILgo8MHBwgugKE5tb4KxQZZXAaJwV5SilUop4sauMbC1dojhLRZeA9pCtiy8eCULWFg5IEyueYl9EqSRDbgLy5CgSRaCwQxGM4MB0EjA3u6DOAeDrLSGqOzzl4RJSysEYUrQyXdUEqIEYHJY7JAuFYJYdFIiaXFT1qIGJeHkORuGiAgMIH5CzwW+OkSIyUXIoCEY2NCoRGUlGpCtIcCYjlYsoHEFVbBAg+fm6UJFgGpExN+3pEyNHvMjUJY0OlQ/bs5j9kdkd1nEtgNp0F4lC0MuSaRtZiASIMFxOrKgPBsVcoygkUOEYNSKJTU8Sk4nAMkkIFCI6+EjS3xl95k4nIaIiMpZTIU1Nt4+XCQHAOG9FFIiWluvwCqLYlKhBzPhb5skoST7IcgyKozASi1jDJEkeZdkhMBDA2sJd4DGBqKC+hPq76FVoW7iOsVgYtqNRIuY+gbpvnT42qgogvjnVCOPbKhTocMU4kigmluWhkFY3kIkodl9OUgcBsLbk1/opGVzEaIpdSCpXY2kjdeJIcNqLLwHu8UwURsXJSPRJtC9XAU9OF7yhqJYnH3PTLvWWhv5Wrzv/II44txY/oPr59NO+tO+tfscd0zh1ezQNbufS9V9f7gNhS/PiEhn8qEH/11h/6dSB+2d4OxBextwPxReztQHwRezsQX8TeDsQXsbcD8UXs7UB8EXu7kX/UzrVT5be19HN4Ef9/gfCf1qObvAiwKLAFE6nYRjlRDgL5c5f8BskCB8xKrOOI0iTPGseiZBna2lmhf6iSxge6dqh4GMiRKOp0/hW1+Cp1AcK7qoy69+gpZVprpzN2N3vzntTuCYJsyPj65FP+bksN+TwJ20D4XEJrIJgX6qyxHAt1z5TzDH4xbxIEyYlIFUgdNs9Pz3bGYLkiCgS7bCog2tQmxI+LK22V72k5HsIqEOG9QMjouEpAAbce5WQ2M7b8hhoIW4I9XgMR4y0gXAuEuw9EOyK4EzNXBOVPIyreAIJTFyCOzpbRzi8fBEJioeS4JhqvBKbQ58Bp6HweJkRSkfD1U+EgDNNsZir8hqPhQmiOMiIk5knFvEAYDuVYHMWGwrCIkawZmOmhThUAofZ6SX3KtgmXQtqGlkjXip1a/uVCoUCoXvpN/IujBDNhzx6ntlOTM2VkxgcHsfJyWsu+isdRM0Nor+6RKQ3OR8Q1CcJlsFNTlMARUSKpeCW6ZA4C+3NT4TeMAcwG4lqM7KaMAWsE+8mEP1ExL4LlWIzZYakMC6oVUVdQFpySJ/H+BrF2lNBSUSKvBLEW1tA3zPqFK6Lle4oEM/9iMxOnZyDoJKpF6hmIWMpQZ6hnl50CgX5seBwVMyRIbQgIhBgnqYTAAKegBqIsk4jx4DnX4AoH4XSFpr7yG5T9QFwLGGmnJXAsaof/4GrXfvEcg2GhQKAsnzSeiucpT4FAPBXYBhso35CDigwy2RSuiJbvgsa8mPeAl5F05UNqC8Q8unMZwvhgvhNfsAAI6Zeax2GZIUzDkT06H1ZAKHPBLNaT9OCESCoeuabCQYgS3ED5DWPFfgDzoQYCM3IBgtkYq0BQymZEOJ80ngrqzGuEjuaJRwRzKcj+HN1FpqbCFUH5qQJiHhHSyQACqS0QwbRVZvsJ7dfpSvul5nFYZkjIcWFiaKYmGjLgFOBqxDsd0K4spiEqacVwEGjIk3HKbzhOylsFs8FXvmcTg6WamizHotC8zNQ0pz5OdmqSgDggeKUgsVZgG2wgIMrkRXZWXBEpPwWQ10YAERFqx8slCFLbeBrRtLVEjUEklwzEWCLBxBL3JTNDgtDZaI8s1qdU4pqAU+A5rgkxFHKAMSdRHAM4CJwjcxBoEVQgQFsB+yEGMBtKGShHeQ2GeTFZjgUdp7otw4Ks4SiSk12sT6BTUumRU1+82AYbvKQAc5DstDwMU75cXASxjlvG0WQ0dYksQzFoShkaz+WESDJyxmm/WB5HzQzxfKJgz43LvO27w3VeRX2/Gd3Ht/gLR++nfM+dtW/urNcjy/xr2zqv4vOA+BWYPheIP/ys6XO2/tCvA9GB+HgDOxAdiA5EB6ID0YHoQHQgOhAdiA7If9DO39rAR2L5+hu/foedfyqm8J0t6SNDs8Ub6fWYcjgKH4P1KipVjVjSa/nJcDWSc8u6XcuESK1N+m0wv/53jXfamY8H9bMVLoY4coOxTR//s5+hLcUnPLkWx/J632060PHEDrwVb4HwTbZ1YQTfHAsVNcSLXoUQZCog6vJTuC+dYGvUBLGyrnBMpGtWqAb1uWyBGNiZU7gYQvaJxrZ8/lRKEEq3ieVVYrW/7Z9tIPAMG7yVCgibzbvbChUGCLcE4nQ9OsvoWBkRxJp4EAj3QSDWC7RAHJ2veCfyeD8Z25L2h19pfXZuuQKEexwIp0BE1A+HyOwYcdCvIBcJnEJrChVgdODYEa+1GoYC8TFoRHi3ZHSgfB2Sl0uM+rqu1bAAfwJ7LTcCw3ho9C3A3WCXVkSHwZZaI2NwlV4HmBMXdnhGy8WIAW0othk9nAQ7LSeD3ZMZiOHC5bolPyNzSEQ/BL8i1DiEt8IOpREsBgf9igQ+woZChTA6hKUBLgczEXgPOT2xRvipZnSMUr6eI288rsnlifMma1hELa/mRsjJxfwKtcBl9yd5mQHEKI5Pq5HhndXrcOBpjOzwrLgY+rJ7sc3Mu7GMAe2ZyCc+PJb0snvgSW5c8DMiM1mkldy/QZQSxqC8lRic+oT1Xfi3MOjwWlGogCMzCEuDWRLMRABvAz5hpYxZRkfRDRiuBQhiY/CZJhoWc7qIMmFB4Ubg93A9GV7EcM1AuJNOTWKz1ciQF9yvquBB+2NQIAoXg2uevxfbFiNCBSW4FlhP3XwEjG64+pyi8DOiwyf9HebShd8iNAIAMbgVIMDZWFeo8IaVEES0BDOikgoKEMpywOD1eU7FmsRnnQMQmUzA+hWsv+EaboRaZPUtyohwOiLUZquRAZtFIyMoc4KA4FMnczGkJcnY1q4RU6YAOCPrxeMRs4YFQvkZsawmc0++Kb+lAsJNkuhy8cK4IIUKWiPWFSqkohB1upL/lZEBvQpfKVaMRX1idNLJ3NigU1PRsCD9iuDgTK+4EZiqxiy9k/kacgJFn6epWiNDbVaNjDmX06mpMDQyEIGumrJtMnUFI3pSekbgIIsiT01O2So1PwP9Kq3k/jVEI5enJiyYtDxBv4KXQ7elUHEyrITgyv86V0Kv4jRZpgSTGAPKd9LJidkWebHOGhYe7A+Zmgw3gi3idMWCi3JMmHeR0OVsi9HIUJtVr4OYJ1is55k9Wi4GOBV0H5FtC+BtxKzKZzkZMkEltILLvZz4e83PYEZJkFZy/wanrJk8NU03Lq0+tn3Go4MY3p/H/xY7fXPp+hdtnwHElv7GV7PzPw9Et7MD0YHodnYgOhDdzt7ADkS3szfwr7SzA9EN/P0Wfg4xpi3lk3uuA/FlgGhZGLe22HyLmymtgsZ2KUOl87FlITyArZ7G7frbEqiFWVXDbrV9V9Ej0ddWZTtVAPDL/Nxzw7T+kDAu23VdSWaZA2BhRPcIEKHpgrDREfAV+Kqh3shNRJPSmzw4UAs/BNbCCG6pp7EEIjRWFLkK8EyyqobNEWxtvrybKEBIKckCQb4LhjWRD86v9G409pv2Ze/gKhDEwljGDfkVIGzTXGi/rAOx1Z0BWhhuqadxDwirGwKeSVbVqIDwWfrFrwChX30FRGRYyQV7fAAIo1lQTRrC0TixfgVYGDa2iepaCBfhUpgWylaY91yUvaBS5Nf87cjxQ7wDH4NcJTXLwubi13uFezH/zQwS/CZljdOk7zlbPQ1rBW0aK2XgY7ZG2s/euSRvTDOhZRALyLXFziCyZv50Ca4odWdpKZ5f/VUgnAAxO1fx3riUd5R+pb9eYruwg4xLBg9l4NQ4i/BmOTn3Zn8f1ggb2ySK61DYB2NhWihbYd4Tx5rfgOgM8jK61/NMXlOvWRY2F85BuEDJ96guWfwe2ZtM3Xds9DSsFTSrj/kF88AMilIj7bdAUKeqA5brGnUk4J1v9rmBf0K+cHXdTnZEQIODeg7OVS1vFAkBF0bxZo/i2aaSwUNBalkgiX/AGgJxlYXBrschO8sL00I91tQFlt8wn0/XN/lGaT3zG8DHgB/csiw0V5RUp8zOcPHEuiCDK75pADE2ehrWCpxc1j5b4+AaDQGMCFEXUf94npLSsahlmFJEHiKvqlH0nHySESHMlqxuImokag+VDEaJ58grKGmCwToiFiwMEIumHNIiKtOidMHs1zX8Bkr7pt+ylIlodYSWZZFZEarMUTRxouhsTPhdRkSrp2GtuAVE5LKWQJCd2hMWiPkzZbUMW0qyQOAsp98QjtDyxqJuImokPCJGl08sLi9FNTowtyEErBEtCyMK4QpAIHwGmBaZrRCja/gNo5maWG3Di3oMmVGzLGwumZoKEEGnQY8oKvNP7T6rp2GtMEDkia3UKLO56M0oEHlqCvXU5DU6y0jkhFKKYykIhTO6IMJa0dmpSdRA5vw6NR0nihGjNRAPhYGQqYn4DlQF61csWRjQ3FA9C6exSmq2QmYvqEpH0m8T62VcTsLHQHgyy7KwuViZQ7gXfI5PYJDIb0f8CuJftHoa1goAAb7FIOd4qZEWYSoBQMjCe9HF2stizcs6LalXqH7M/5wt5cSCQ1mb4+rlKup0PZnyRA1koouI02RixGjJF1IX8Ws++nj//jA3tcr3Ad7F/a0N3Rd1cdxK/5AV/s69sL18beuue6oV1Fr2Zr3htYEYlhfp/7t+GIi1qJufvdF08B4gHrXiTz3iiG5tz4NW9mdNnwfEv9zMDkQHogPRgehAdCA6EJ9Z2m9oZgeiA9GB6ED8xwg6X61R/8lO7kB8aBv4ifogz5bKLwiqDyadyRQQm2Uwzlh1erRPWIzihURuATMCDlP6hPtle/O2xOvasy+bot5TR7Koj2lJag+828TQUG4LtQ49glY3PWf2S5pQcziitLqJH7P+Pb9oF+pf+pJ4SWez0/PSkksrFbdoVb5xxMsxidcgHoO4wiFpaATmZ9Eet3IOodIjt3ti3TH1Ma1X7IFblLxwym1x+Z3S1qyWr6JAWKpAMPIPjwCR1TXqX/ROcQVE1Tfq13JNHJ3ijdNtGpdA+BqIuAaEXyGm+EaFw0YJWt9zK3U+Jvb4qAwN5Y4gmtHWiPArI2ILCPOgH7wCFU4/Mf8C5pRoJ/bXkaNIKNcCzo8rczZIT4PcxkhXjhPXR8XVT5NyFi6XovGBl3+FGaHxfFh3vvArhPPA7po6louwLfQXNC6cTSF2mD1RYqccV49pbBTYo5wlF5TbgjAPRdcDE9oaX8VGlNEXjYuWB3Q4oHYivIIctsCx9gAbV6Kd2F9j5vsEHStzjjeJnHJKmLo0agi9hxxYaAFevZA5C1bj4zQZZoTG84m+5lcw50HjTdhYLuBE8FvL8mL9yO8zlxSyypk9J6J1aLiD5hhz/2KxJ0ZlaFhuyzAVXQ+ZvFf4KjaijHPBWS0P1eGQICLq1WWdCmYovOUOLgSr8usopoBrUYCAnkaMAkQoxxF9xAVwRWjEAIii8QEpCTAjKBJR4QhlPoQwTbDI1rFcZD4PxNAYVH0gLFJUezjEh06JzTHED1Kmhk8xKkNDuS1BpmHV9YBNa3yVOqJMqLQ89GV39EThFUzzX9bcuA1ETDon10Bo7BQFohwvQDDoEVofyWh8CCskgREBbzI4d8qHsKofronloqtSJIbGVANRUtR71oGQ1EFjo8AesGSVBRh1dQhF10NsWuGr1BFlChBokwJBPSG8Ag25EaBPk2y0k/qXXjWxw16OMBCsp5FHxJiPM8MB0wCxFmiyof3JaHzMacgGdsRrGJnY8itYiMdMTTlmSeZEFM2PUKcQO82eAsTKsRwbBfa4zNBQ7khyta5HjgGz4KvUEWVCreVRgBihwjLhutgFMCYSX/WWaCf1L3yCazHIIpVYv4IjpyRhYlz0OA1RMB+IzcAsCdb6sBofsAHMCArdhKuUml8RHBQysFjbWC7gRGiwsRJvxaRwbfyXExPqTBtsauGbqD0KBCxSIKyuh/bDCl+liijjay0PAQI9IRI//7vaO4K1i7u1bfvI7eNr+3GO3L+TXTIg7qtlxA8fe+zO+l4/PLRFp/eSf0IBY3tD7f0RxxfZOhAdiL/Cng5EB6Lb0xv+Be3pQHQguj294V/Qng5Ej6Ly37PxN9JwOhC/EQg8s+TNuuD5L2KJlP2sRjEVx/p6RBHsyxFIbm36VtIDyhzZ3o0yrI3KC4nVc7rQtLvYnhavgq1FXTnJvhx0omV1oLVBPRmFqaKWsdazzeUrFod1gtdub8QSKaoVrEZhGA5rgSyyy1wjkLgbUS4yI6NS5ljJ4V2r1ZH3N5oflhcSc4iOUJWBdtdA+JpcIY2wUVeEABAtEHVPIquepYUgkUdEiGGhdFHO320gbDAXbaRlONwEIpSXcB8HwrntHGtAuIXmh+WF3ATCtSOi4oKsRF3RffWIKFZJednfvQIEXlcdGrUKZTZ4V1gKp5VYIiWGCGPqCsNhGVHE5pUIJBPilrSRRkp0lVaZA0ocIvoey/6idTEIR+RKbIpK88NlXsiRGSDe8EJgMeKp1OyMRjPkuh51hepkDsqFjta1AghpE/cOmCENEEkdhosoJACC3/UPqjGwiCWSY4gQpmvqGBpRpM6rUarwGj1xJ2ykkRJdpVXmGGVGHINGXsF+2OwRySHmaBjjaSptwnlHQFApeP1ceSEERJC21uyMt6ZP1qOunBI+yeGJ6DSl1tMEXx63KRaGSr1G+IknNvAtan0MBQLRUFr+gcYS0RgixHUYDcOhjihSxyHRCCTzsSj8kSrSiEZXWSpzRAGCbYplfxD+BNcXEVHFNwocwg8Rfzec+ZkXkjRWBfM9DDuj7ZP1qCtcXvI5HImtFbHGTmiTzBdWu0PXCGKIyCpT62MUIKKrgYiuxBLRGCKqRqHXBXVEkTYOiUYgUSCaSCOhxCGplTnGDIRGXlEghD+BQCBuCQSuVaCTMWLUpMILET2PccRKbNgZdZ9kUZMm6grP+Pgc21q9Xi9lkkOt3aFAZL2KWh/Dy1WT8CHGBf8AXZpjiCjnQh3ri4giNm+OQBIlnlITaWQs/IdamQNKHMKXSGW/2sysilSmJtsmUcxwWCNGAsLwQhyzSKBBZdkZtWaIc+tRV3iapM8RATuMYke+tEGZTFBI2ltGy4P6UxUxrD4G0UMsH2LBP4AEUI4hopwLBWIRUcTkLRFIwMpYRBrJ0VUWyhwOMVTAxSv7vfInMrUxhVOj+QHFDIwblteKhRcCPRA6b4epZmfUmiEKRBt1RVpEVoda90PvI1Am5oshM1SKlgfd4axePf6e6CS/usVfyFsmgq1SzP3Dxy0Mj7TCLNKVlscnbF8fCHcXCHNH/eHtMR5Mf9b0xSzpQHQgOhAdiA5EB6ID0YHoQHQgOhAdiC8AxN/aSXW95dfvteeDAPiNff7BtO/vkF8r668FwnAvou7DF48Q90l2BvtOvgt2/1D24w2kXPo957SaFCLenStKFy6B62GfYaq+sNM6jOecuBQ+lTKHujH04u9U1xqyWj4HP5BfeC5K34TzIRaxrw1y8ZMtergTI0ZjqcQVICgSi0R0sdyL4lBHpvyG7hIIW3lYSkJ4C3NO7ZvMxb2Ot6qZY5F5IK5R79C3RvNpokyNUE4C+wDVm5MDv0PD69CIKhIDJeFXzJFWhPORtTe0ncG8a+hX+CK+9KArJIxCxqgjscRQgFDHWgNErAOk1EDEGgj3ABBuFQiYRO9uMsci80DuA+EWQKgSSDV1pqHqJnsKIKLKMQNxzEAcXeZ8qPZGAaJt62aMmBoItwJEFMeUs9wLZUwYrkbSSeEojpZB3Dt0pOz3zKbAJAKWgzIhpITM91D2hjIySFlg4CguEh9FbBHf9GRbCVekcj1Y9uGiShgKxDG/AKw8DQo8gAmHB73lZbDePfyATrXzRxfytwSARHsj6CQWnOGbiFoJ9wtPX4k/wTQB5yTzSxx6q+UzSS2Fe+EXEU4wGxJqwqxIelb6ar/4grnPRsZ41HADDEThe6ivOvuf6axDFBfmCnpz1p2mijwVVQ8fpehr9VDCUCDKK/HKQfFitZduMrwMBgJBBOZtwi/wLHj/pKI/CKCj4ur0DS7bolaCcE+A2ksPKudEXM7CIHEtnynK+C3cC58nA+VquKwyge4/8bdBuqfsV/UNwHa6qhbHINNdzHwPE40iAzFc2bTgKh6Ia4SFiPngMpnhdNU6o5mawLHQkQ5eBo8cTr/gqjAQGtwJ3e+CKIGYmF56JaNAqHSEFckAnwM1aAgdpTpAEOMkDJJFJJbkdX7P6hLikLd8C84xKR+Do4uMTq9c8n5V35Dxg/HBkVBUxkr5Hu2ICAnkAwWi8EBaIIKzQMRcZ6zWiBaISK26kKV+wVXJXZ/XCM8rcQ2EWnQbiKw4sglE5JYSG6COxKLhg1xx+WcgClcjmGVyBAMDTIVU7VctDE3tJZYINJgs30NDRykjg4EYWekiVDwQABE0WIyIjMS8YJupSSaPVAD3hoMytyqqvoZvuCrc4bECIsa8H8to1t5YApH1SfLUxBwOjdyCFuapKXo5SetILFjMUwVErCOcyLKcWQiyh/emav8JQb1kRWF5LOhcqNZF5nuAneEnZWR4Vq8gIIapVrqQ6CMShmxIrG6RUCc4JlD8kHBl6ZSUY4HFWjkoc6tEoovYKZaXERBRJZ0mvY/gX1eJtCJX+AUI1OlUe8rok+h35phE+gT7xU9OKKGDiIOBT2IjseBuJbl3bP+TNWB9//YNzdpW7719J+vvqG20x7dTt/v//J11zWf6paIeYyrEdx3///OsSejQ/2qrH+6Q/zIQtvQORAfisQ7pQHQgOhAdiA5EB2KrQzoQHYgORAfiP9/ZX6H2fw/0DsQHgABHIbVcA9ewD6LbYiy4m0dKhRSTRJ82aokNsyJqaX4KeYcqd4C3sRWhZStiChd5BWOiRKRQTkdpVW7t8qnm/YjR8Va8FjyxhQVo/Ul++cm3QMRYu7hDdlkWINYYC/gOF3rRzSjOnIIP/FHkL7JAFEYE50r5uwl9EW2dLHyxFqFlK2IKfI9gTKijqnA6SquKOMTi2W2qWpZWnu2mW/FavLEgZt9HZE+yBUI4Ci0QbgHEOmMhV2Z2rAMhjtVYAeGqXIZx4XMjKiBSlLRthJZY1RxXugGxUeqTpbTqESDcNhCbtfvKAguEuolyIezbiso1cBxd5DQN4ig/Fr2MhrGQHUFO5NSFpaDH4JzRGCrgLsWAhkARg0yHesXA3IqKcZHEOnY1kWUeKTYitGxGTEloOIcNiQjBoZwO2yptv/j65rKY8TFJFBVjI/26ToXJcYRVq7WTA4hfORYLjpPC4loghJmgEgZsxBiCUdWIG4wFdY2KY3JUloIegy9UY6ggJgkxQ6hcKGIQ0CPrgDhxZRrGReLZNco71uxqpFl0K0LLZsSU6PjdZnazRmWfgNNhW1W4Fk76ILFaxxgUCLWRfiGCDHgjNI1u1a4hFRiIiPocM0bo19GuEcJRiMo1gGfarBES/WPJWJDljaOFZC+xAMEOUmdjqCAmifJ4osiXBJFqGK5opGFc5BFxJJUNByD8ZoSWzYgpMiI4ahZGROZ02FYVrgWAgAAEKYNo16uN+gt9EYQtuR2vRYEordcoXqMRglCOQuYaLIGIboOxgClh9MKZUJZCPjbZGCqISaKmjA0QcO5bxoVZI2gGngAEzcbrEVq2IqZow4MQfZwrnI4lEBE0twwEs6iSxj0pi/VbEH4VTuR0K16LFwdpaT3BElyt2qSu8ahcAwFCGQuG0dEyFtDtRNDSqQmgqQ5FNDFUNCaJcj1FEUOAuFwod2gYF/mqKeapift9PULLVsSUfNXE2iRlahL1DmlVjsnCUkJQnCHGRxxplfHM/1AbPdRp6IjWkLZq16kpczYyEPpLPdgCxCkp1wBqGF4YC8roWGMsiOpfwKIJlgYkskRjw8ZQSYhJouxnUcTQ1SRSbl8xLspVDRZrSRG2IrRsRUzR+wiWNsqLNTgdJmpMaX/kIxd0LSloYE+xkX5xBBmHSC9BWIir8VoSSmbGSG79yfxSTof7lS26R+52nHMLzkJ0n7ndL+3eGxh+80jcPB5X+SttHX7VAntnfY+l8uC2FiP0flfc77rP3T7+iCN+4Mh9C/qzpi9iQQeiA9GB6ED867V3IL5I7R2IL1J7B+KL1N6B+CK1dyC+SO0PAfFvmPhn6/wztT1eywYk6wXcf/bh35W6TtmBWAWiUoTYUMJY2fittYdTV7mE0eF8WjI66BtrXkQcucXo0GeMDzA45MHaVoQWsDpy3aWOG7yOYmnQfnTSjw7az64mAfDHKSGl+EauBohoFSEaJYziuK/Pa94tQNxKHVZzwT81uBCXjI7oyluikmuT0aGO/PsMDuowFYYIqyM1BFO3aUXcpBMUSzWqzCAkDIlzIvom3nSEF+LA4IqvzgBhFCEaJYzYmFuFmElIcCv1SryWpIyOowmfE7OtUet5gNGRgahAj8YCBeLojveAsHU/CITLQGgtFRDaRytAHJ0qDtg1IuBtZbdQwmBhdMjzOOUkKJtBKrmTmlU3QuFAlBHhhHTQMjrIBy1+rbuMDh039xkchdaCMCJHkRxisfnM6tC6wdC4x+solmqcFGV3seoCROBZm+PI7ivqA+pfSZnUV1eA4NfYre6EKmGwir2MF2U8KJuhDLtbqYO4TetcYHQoEDWjIyafgbjH6FAg7jM4Kr0RV1zAmNgyqyOpazOE+7yOYqlLXmrBeR4kVgzZmaBMMiJmjABB9kxYswwQhV3QKmGwKP8V86sarmyGMhpupQ4NT0OBcA4xSJaMjlOWabnP6MgryV0GRwEC0VE43ktelJXVoXWbKC43eB3FUh3levkBTZDAdr5lDYS3wNokYk+hEWQgCrugVcIg0ZxIjIzJAhHNuX0vNRzqbS4wOuyIKEDoefYIoyPP3ncZHGZEaPAdZprwFVZhdeiIcAWIbV5HsVTjpKitPCI4aIdLFgimfCakXFusM7+hVcJgF3qebMB4yGwG0K7upKaxTIQDm0sZHV6ummpGR4wZiLuMDirrMQbHUgqpnZr4xIh5anL3eR3FUi9xUrwGyokhB8G5XBDrBTFjZLHOJ6FhcQifIFkgshJGUCU9UbZIhs3AKnr3UvM9gOVpEO9cGB2nyacVRgfzKk4PMTqYUeEeYXBQiaCQDmxFkDgvWGGyUofUDf7FXV5HtjRInJRTjv04oIyJ8GN2y0VjxpwSUsodxzWzOG7dEbZ3MrfZBlupY6jZDp/L6Lif+9F7Xv/gr7hyPK7Wss5pae+s84jYNu0x3Y17qZf7/yyj47MfccSHa1nvkXc9a/q7uuYr1PabHvp1IDoQHYgORAeiA9GB6EB0IDoQHYgOxNcC4t/qtD/V1g/V04H4MkBw7BC8iD3dSBjxYVUs7CavuKdfZVwQv0Oiq2xyLqwd9T4paYN3gbYOk98ocRCJCWwf09zAy/11PX66YZmWSq5sJ06URhnDOUsWSPlI0I5rBByc6gr8CuMC/kJ1m9RyEnZbRki57+5HWwtloc3tRcyhbrst5b7mBl7ur+sJ4YZlWipnErGOtmOrX01kktrgoQHi44wLDkQQc1SFW0AsbL0NhNe2Bt+27XEgNq0vjqdlPcHdAULms8CvkbuijKFRRxxHSkH0kzoyCfvVAlwn7CNLIsaQfpVx4RFIKlnOxVWik6TQ8i6qCCl3eBdgWtD5x06w0LRUSGilXR/T3JjdY1rPqPXA+1f4H2ulgn2AJFkZQ6OOOI4EgVgPdWQSRE7Rt+cVCNBFfo1xwW7bSYEA5+JNYjGkBe8i2Qgpd3gXwrQQ31hqWqqUm9Kuj2luICIL13PRevg0ViWOjVKFYzQGFXywUUdy02KjkyGdUbgQg1uOiI8xLjiIWag5FwrEW9BldTVCyl3ehSrkeCnLtnS4+uynlnalj2hulHpcrkdGhLO6Hm2pzD5Q42ogohQu0U+qyCQMxKhciPnXRYH4VcYFWJw158ICUfMubISU+7wL29a3pqUSpiOVdn1Mc0N4W24JhJti2FbyYI5DrIAoUUdQuEQ/MawKWhXG2SEeMlmT4wbF5qrpQ4wLSJWEinPxJtFJLhc7NS0jpNzmXYBpoRYTu8K2VKJvJduuj2huuFwPLjg4wopYE28oefB1yiDzphJaQtbTECLlpdbJGHixnhHPi7XENqnuIz7GuKBznn5bzgUpXyBaid6zrEVIucu7qNo6TxXJtpTspLWttOtjmht0dSn1JK0nuJNEDNtW8rh5FygxVJZ3IOaO4D1vR8S7Kd53T/o+3kXLtPCLlrqqXR/T3Lhdz41S7zd9q/vex/F4bPs9DyHiu0pv2xV/Syvin2n61wLiT7e1P/TrQHQgOhAdiA5EB+JLlt6B6EB0IDoQX7D031rPlpTHveo+x5R/t4YvtXUg/uB2Lw7I/Dg3qX6v28//eDtII3bnZYlPr3Rsf9gfnl503yEf3em+vVvu00NzXu2mn69cppbN37iG2Zbn17qk55efr+d9XbbmuwPEoT02W5BLOc//3GG3TLivji5q3TVFmrKrivn3vTggbhLNDPKfiXGEx8+flB1F7nIt9Pf5QA3cnXfnw75Up0bt99asXbVvL78pr5MadlWTdvyNa9hT5bx/r0cP87cChM25q3pD6zFAnLVdLRA77erzjndsAHHeVbXixxoQuwqIvQHCtUDUcUC8kRafazxoh6GbSle3QDy/Pr8eVk64vVuOiH3zSXnvAnFwh0NTEgHBO2+cm3bAPAIErD/cAEKP1rXuHxoRGYj7cUBUu4ZgmQs+P70cDs8v7vzzJ3XWYf/0Mv9zTy/P8+e3+W81ItwT7+X/3fz/D6qafs2TxY9vnJO+z+Yc6Nczl/TtRUcE1bDjMqhJ+MxAzCfiXD6XqmUTEPtzXfaTe6US59yvYmGp5+dPpKTjqB+tmFO92lL2fOrtHB098ITLR6jml3LU1kr1kDVo/Xnftpg21Ii63ANxQHRETPMZOlc3T980DA46Iugf7Z337b/zHPN0KGvEnHS/n79+n9N839PZSgZSsgMl5r28b/5JZVBJ3/e6RmBEoAYefXsAQTVwx5y51O+lbBy3Ze/cP1z7Yf8P27HnmRD1/Pw5tyVbR/v31JL9d5zpuZSdwz/Ke+AJl3LN379Taj1qa6Wv6Bcq98wl2uO0oUb59UAckGpE7BUIGRHPr7R8zsjTmf7KcLjdIQ/5eTl9eqXhB6Oos/YAjf8+ve54cqMRceC5AN2uEwdGRAaC0++kBu60fBRlH2TqsmUTEJTi+fWf2Roc0XoYCEd76Tftp197mXBNKWeaenbcxmdHVlE6ai+l1qO21oMA8cTl/rPXfXoc5T/lutz9OCCnq10jDgKEWSNeHE0cLwzRAogDTBIgns41ENi7DUQzIji9AnHgubkAQWVbILTsPCIIEDlSgHg6k3XzWXnGiNzz9chhj2k3l0JVneU0PB9kbufZgEaJHLW18oj4Tv1C5RYg9DhGBPZgarobB4RwKldNZwCx37vF1CQD1QLBR7/vy1nrDBDfYaoBgof5DSB4ElIgznzlsJO9KDsDYcomIGgNQHfATq2H4CR75ln7/O2F9u+lc1xdCv0jIKiNB7GYvn/HlIyjttZ55dg//dhx2YfvP34c9rVVdNqgRjMibscBqe8jzrRk/ti96H0EFmtaxHbz/rnDXiwQWNBoeZ5xf6G0WKx5JaH9P2gv7/tBR3dcUgEC1zuogUv78czfChBc6g8tW4GwZT/PS+WBl9nznsresf2oZx4RL2ydo4st2j8ffVEgTCk8VOayX2AnWjEv1T+4LXLU1nqgeyNerKlcgGePP2GqpZ5cXOFVl6+Lyzv3vjtr3Ia1241Kl2W8vu/O+lbZh81S5Ny/kWbdtnvHD9miw2qed9b42Y84nl7e0dp31nCr7G0gkOu9QNzfDtmiw7ut+gNAvG/rD/06EB2Ir1NDB+KL1NCB+CI1dCC+SA0diC9SQwfii9TQgfgiNfTtU7t3rasf6/7PPg3+H4L+9wOxX/lmHMp4NnJ4JH+zp/hol2yKpw2GxaPboSlrD7aJHFtpfljsW5NcMKmi+audSnohLg5TpRkSbgMRInKpXrJJH5v+qv88DkSbf78JxG6B2K51sTd47jLnYv34wRyFy5KJBU65JiE2/RRon+4NN4CA1Pk6EOw/mGKtGcJ/lu8UamQlvH3tJhcsEBK5o2raYRUI7cTDEojdRv59k/dXgLj3+9CUxUCckW63BkS8CYSmjneAIIWEuAbE9oig0UC5XFyMiCQsg2fmJBBDAzwMcngo6wF7n9iH9U1YDN+Ux3Bezw8uxGvJi4mDWRjqSCL2wuyjOigQT+BFzHuJ2XAQchoxNJhzIWyLxXH65qqyzuRbL1wTqHccRTwe3BSeIngvEengGBOFjIS0morSgd0CNQ1EY5mBSC6RU9NqhpDYPEWOoVSqGVIizPCIQC55G72k9+xHZS6BOjPJ1zr7kL67vbIexA3K7AOmV5zhEN1nF+kyP7gQ/+S8cu6qwxR+Y/bsFiCYFbGHv27OcxC3ygFTk7A8lsdne5Q3UYDYG64Jn9OsyaHaIToi6P1/egeaI18E6AHgJfuSitKNvIe0SDjkwfx97tTJk4JGtJohkYrjCCoyqIKNMEPlIZcXJaeSnoBwYDUQG2PPnAM49p9en1/Fx8sL4EF8zbPLk3kO7Cx0W/nBhfgn59Wl9PmV2BQHQyAwQMzlPAlDgnzYoFMWIGBLe3zO4ZQ3YUdE4ZrQuclhNa7Q2Aguj4gcLWyYf78F6HAMIilRRgRkUk45Gssga0QUJ7JqhnDuq89vag/ORpjhxZ9z6RpR0gsQ4Fk48ty+fNcO2jPrAQSjF9AYAASxGGQdeNnKD+D+yXlle2Fnf85DNBYLBDFCDgwhkwkA3hlAPJ1lJDXH5xw8IkpZWCMK1wSzfA6tMcU8Itw0ChD0800HAOsgRDMiRK8maTQWYng5DuQTKs0QpAMQqhlSIswoEPMRA4SkT+hEMLr2Z+YcgPxF/IR/8l7quLOQpujc3DtQS6h71vKDC/FPzgvWgpma9vWIkKPfZWrag9WhBBnmXOQprTnOJdCaVQFxKFyT3JUjNDaSmZqOE8efG4nH8iZH0T2pnpokEpKIqAQEjVPBMdUMYTmVsYyIOsIM64yEIqnlbfrEjAriEtAyeWbOwQ6L9Q9lPZyRZj76DYyHPf8+M49hIz+4EOecF/cPT3lZx3GeApmXIUf5EoFy73G5wHufM7dDy6qPMz/ElIX7iJprIsHIeOkswUuIZuqxWEt4MgIAsVBsiBPQUklbhBgu9J1Y8sSTxx2BaoaQTgdpeuSpqYowQ+Uhl9x9JJM+3rw9urXtNlJv5b9d7mFzb1nob+U6NBeK69+X++LNy821vR+PttrmuaNh8qtchs/lQhRuxm3+x58D4v6xz8zzl2/9oV8Hwn1ung5EB6ID0YHoQHQgOhAPlvKxEn49TweiA9GB+Nysf6ib/5/Y1YH4V+xCbGnnnITHlkjQV43m4MIiqERsPbVxUeqQOQ5Rczk/qf7HSsyUuNwxVAfUKuFChBVPcRG1KGq6K/7krPAaS12B99PT7xRqc261zdol/jdj16BRZZY2huE+ED5zFdy0AUTyVQX101x9TdgvgAgSZGA1QnZaAuGrz8ygcMWqsAVECWMTlnhklFJdF9wQNRC32+aNXcHZ3golXX1OCBArPbAKBLMOPEZEbE6KjwPRxEB5FIg8BYBB4YpVW0D4eBMItwXEGKIokT8GxJZdjwAh21X4CeAikDL+ETFJ8F41sw70PWsyGFFAmOlwgUg/OBCQdD9KdBLwI44T4pQU9sORxcoBBNgLg0Me+puCWjHk9A6pYCXtu6hVCD6ioUOO7I4ZpA60bXboRLWZ9oHJQS1EDfOkx9wNrVMju8yeOyJoJIm5wlbcbtvgCrMjOttbp5xOLRgMc4T6UHqAHYRRQxRQrIgRMUnYRQ7WQYxmGgkcY4X+HyVgBl6X5zN9RAiAAH5EEPF/OBJHjiNCqVmX3oG94CUP/U3FCtkzBoryUiKlkH1q1RiKVUGDD4hl0JsaOYQBbKZ9CG9AHk/UQGvBW2ZZIC+AiBd0Z5K23mubd4XZgRPEy9jSaBgUl4Ij1iBQyET94AKCJXDJAEKip4hn9uTyiGDWAYCQ8/jKAipXicMAswMiqzDX4YoIEIiNAGOV44C8JVqENgVMCeoUjeGie9D0EiklMhCWCxHEoY8rh8HBMl0DAMRw1ROFLOFYF1gkuU5a9JErwnvMnQYg3qSt99rmDbNjNHYNLkisFpRCfaCBseY+u5oeoKoCx30qQMRkF2uNChclKsSIGTRo2A7lPjhR9VgaqxwH/dcCEV0BgsNa5D0WCLIqaiilzIWIEv+L3PCOeRMWCLE8j1hmcyCMw6RARI57NQoQpUUKBMNzp23eMDuisYsZIZJORzntc2hRMD0gQJRQSRFRUFQ2qwQjQ/ggigVyRBinUYAoU1PuOjY2ZCOUtDLi/wUQkocjpqRYleJcnpoiTM4MilCsorpoDhaoRwAxSlQT1OskKBSA0Bre+ITiuDElPkwG4nJ5rG3eMjuMXVzfqFMTxyoblfHBfTHmHiDuAotk8YISA8chCXofAdaBBYJigURJieu/slh7ZjpgQZvLYO7DaZJIIw5ll8X6NGG1QB76y9cpcbB7AhmvkVKQVq2S6FlsFdXlgtZcFmu1EPvIHs8xUYqd1H6uU/LqfYSMpahW3G7baVq3C/UhHZ8o3Ft+yjFoLqUHbmzr94rxA6EtPm+LYesONob7kU9ulPvLln0ofMevFP3vAsGXiRtHfgngvw+If3vrD/06EB2Ir29VB6ID0YHoQHQgOhAdiC9uVQei2/WoYb9f2uT/Uad2IP7EFm7viTdzrByl55KzD8MAQU8k0yCO/PJM6JT4VUNz1Odj/IRywfIYHtS92N5qTkhsy4Ir4yo2O58qrY1shW3rjb6IGu+e2ofDG0/E4h0ggqsDfCylDiLvr4uA6LsBgh4vy3P6EJU6EBzU+KujpeZQO+cX7vqwNHjZEL/S4kJFCBUQ8Epmlxg9nHYhVhIPTskArkJOa18AkQYBjp0gE9KHNbvSKpBbQLgNINwdIHwsTpcWiPm5vz1a1fPrQLh3AOEqIMiqo2u0Nh4CwtiYjvxxhC8uot5gThNfAZHgplCWAhgPV+Z2kAte9xOLYsguDvA/juzMGZzyEoQd0QARTFeTj/ekXAYeEXJ0RHOZEzFxsyIM3eBNZHfUUXkRbNtROClsB1tMrp+jtMxyQgwzw2piUJck2OwzEKiB+gS0B/SBeNYjc0jmv+xcupDEPTgp2tH8JnUIAgTXynwUm1LwJU8UWArKeHiTF7xdYS9Er+/gsyvyjQqf/ds6NZUyKDoLzf8KRBSvWxkR8PtKg3H0gjeWR3m5/zQpEOu8ieKgHYO6YsFdAyeFyoDFKUgO4ZIoJ8QwM6wmRgJ/JfIakV9N5xreAooGiSGP/sjlwKccYDU4KRUQMcoaQS0BH8Wm1OknKndCGQ9UaeEbEPrcLBeEj0ApWN3CWSAy/6IaEbEZEaqJwTwRyaecDOZiXO2I2OKEKHdEQBXGBOVDOwAEaWwUpQzLCTHMDKuJISMiViMCJWwDEQ0QTvrsrQKCItPo4hdMCZpSZsthwZ2gSgvfgOe2BCCiUyB4fOQRUZVRASFkFbNGROY3AAghwBgghDWxAYThhDB3RIEAY2JOl6LO3WzxWyhKGZYTYpgZRhND1wiwMfKIcC0QQypTkx0RQ9JV8k2ZkDKiJM5fBqJOKetELOQVYTzI1BTK1MTNgmDOKFSTkeUoCIhQlSFAIDILB4yqr5qECQEgcNRFA8RYAbHOCRkt20OnHrIInBQugy0mJkYBonBCDDPDaGIUsinVFqLV2mAglO/izIggDggawewPr5wUil2GE5Wqi/QdLfGLlAKEsB+mwnhIQfkGulh70ZzAgoQU6LgTeAmljAlA0GLEPKTmPgKaGH6SdQRHk5mawLi4nGQRXuNNlMWaV5/JZYYGOClUBiym2T7I7Gw5IYaZYTQx7H3E3K5ktTaoxfQpAlzSGtoXWeljnirpSKK5gS8H5gV84Pw+sQKgtJ6ZfqFOaS7C3rOt6Ui0ZZgRsbltHSt8jHVmxvJ4nY5+LffU+9Zz3rZrq7XR7K+pyXHj+zKl4Op+eWvL+G8/4qhbGzd74fH++u2N6c+avkhjOhAdiF+2vQPxRWzvQHwR2zsQX8T2DsQXsb0D8UVs/09sHYi/1Jivfhb+aydWB+KPAqEvYBaWwey85L2ZyeDWmAycdrE3Nj/xPIX/No8E+blqZYXWNxjBCZ/06ev/rvwklVyN19NU1yW1rFhh9s0OmBQk74r8QETq9uFnYTNU/hV6KqtxUVpWCo6Kjv70KAD6/C8ZloH4AZTJMDjv2uetaxIIpVXZ+IRnlOq6tKXEYH/Z+mIwGhtR/RHiReAXx2IrV5LLDg0Qtr7IoRsWtqRiuV8Bwq8AMbuJ6JH2BitFjk6FleKbPr8DhLAMxDNcmAzHTwTC5XwtEC1zAq/qRXE4jb8MBNQ14sKWjwFBdoora8FKwVEfvVsXpNkAAoZ4l1kG/M6vY4cOvGreKZOBnC0bnIqiZZHZFOx8H1EmNDgseyHKa8Xi3jH1SeMTNAAAxHEBBFQyxEET4LhSNgf0M2ABu3PmvUcDhI184t1RXiLmEpifomod3OKk018B4ojYQmmdlYKjIRpWCsdkAQMELBkmTMTCIKFUCkRmGQAIn5kMPkvzx7jJqShaFuIBxnFpGDsx6W9hL5wm8RIH1TDQ+ly0Whbigr0gB68RiRqUpEY+w6OWBpcoVD9G8ZInuEMvp9l9j7w28oln4gccwvnEiYih4vilfqxnJQrXz59I6zdYKVJSLKwUqmkUfze0QiAroAwS9JsAoSwDHRHKvlgAscqpMFoWiEjCznId6hKR5GrZC0cHDQzSppjdiLa+VLQssvrF7FE+Oh0RRNtCtAfOiyW9eLOz6geW0zcwNcbItIKjqyOfEPHBJ5WLkEkvwn4qJY8ISyENZeQuWSnOABGc1XQwPRWJFVOitcSQ1whlGega4TKToQDBzu91TkXWsnCZTREJZANErNkLKapChUhgaX3BWeUPrdFTDp2akgLBMUtQyWSBkH+qpyGLNQERUx35hFoOIKKrgdB6dR1aB2LJSsFRnAQFiDwiIIURo0xBsGVkV3viZbWwDHjqgNO/GRGK7YrORtGywLQUoPSiZTpocBj2gvCGwMaw9YlAj2hZmKummIGIRSUDUClNJmj6sRB4KKUBIjaRT+hiPeoJGEe2NyqRDnwVBmIsPnj00RYrBUddMyLA80A9tEaYqYmPcuginmULy4DvIwanTIbTpEwG5h2scyqylgWl8BOikhAMKDM4aHDU7AXQNwekLvVdayCkRmZrIPwSN4BVMkSw65olqiZnFEWweGNRjFkOqIl8EpVLxSVc2F4m4EArQy9qSLBLWSnUR6dpi5WCoxYIrG5ggPAaHESJYxJbpthcBlYXosu7xBjWo4D871ryrl/QtqWssbS/xp113DzyK6wULVcYI8H23O1aVwu9xTuIDzd1q5T/9iOOaH+Em0f/7Y7pD/06EB2If7O+DsQXqa8D8UXq60B8kfo6EF+kvg7EF6mvA/FF6vtrgPj3uvXvYRX9BZZ2ID66bby8ZTUxouzD57CeO7+FVnMdyjZ7ya6n6X/X9dfF6Emq5rWxSbafc8W7LaInq/x4bS2qirtdbrzZI3UrhxWT3i8ZEtazFU2M0ADhF7lrNQuwGWKVTt7HLm6Z1ZI0r41NsoTNS/JoGqykhtAAEcsr5ZWshL8RVTq4VX0Nfl3d5+gultGR6QKptdLa13I6/BrFYF0f5VEgXAMEczKWQAQFYrUT4iIv+Byb3VUD4VcaM1ZAtG3b2vwWEM5VQBRGx2NALLuMv1n9DThXrkaVw77vfJpKjJNhUml3VcZYqlmAzYCwFhLpBI6aDAS8YZSPXozFtAQ+B/La2CRR2BkxXqcSxaToaSiTgsos2iCi1iFAWPYGMTXo5dvC89CXfvOru8mqdRTeRWD/OFpfMzqUR+IlPtERRAxu16nEUjGcDuai8DFn9TegZvGWFTXGShMjOI0uMopSQEm1pmaB83AMRU9D3arEpdB5FS5TF3XqoXVB81axSZQpEt9CiWJS9DSUSeHzYDevxF8kHIFhbzBTQ8oWngczSoQ+QVBGq9ZRnJsnYbbEUBgdCoSKAAEI5oekPHKDHi2cjsTMFvbjsWtQ9DegZvEWEG8kB7/IQGh0EUQD8a6kWlOzsC52OOKHMjUZRz2flel0tWuEMyAGF0wJBESJYlL0NDKTogBR1DpGjQtR2BvwS3tXeB4D9wN9ykmRrFpH4V0gF2pXS9Wvr5a9ydzoUwFC7bOcjje2AcSNqehvgH9BI0L3tkBElyUeqGmTBaJVs1gCYWKJGCDcBDaITE0GCBubREvgEZGjmBQ9jcykqICI2UYFQi8TaiB4JZhEd2eyzEFR6zC8CwXCTUsgRI0pFSC8tKvYZzkdCkQUnoTob4AI9hbAfVsAEapYJ6N3JdWamoXlOuSgNkGumlw+As0LV8LR0Bph8yqNQP5hanJ5aqoinoQg2hehVeuQqSmzN6A04l3F8xg1Kku+apISLO9CgaAy1dLgrGaHSxcWyQjC93Ou2FdzOi4sWcFkNqu/ATULouqKokawmhhQrlDGA+0tqdbULCzXQXU6WMkiET9UF2taUk8TFmtcPND6YvOmSi2E7CtRTIqehjIpRPtiWqh1uJq9QW3208nwPJhgaRbr6ErplndBXBSJkZIZHcFZzQ5CWafNfJEq9tWcDkL2xCwQt6ZJEd1Su2JdHWOpcKHbI/ec65eOv8bniO5Xtk1NkrBu/4NkAdf2WHRb/faJjXkciN+R99e2zfv38LmWxj/XpP6s6T9gYgeiA9GB+Mua14HoQHQgOhAdiA5EB6ID8UDhj1T/ERM/s1l1We8p2aZdp9D8oROlA/FvARELryLcqiaHUHgMiBIJe6lJIXyHWMWnuNJbypRryfywLJH2kC2rJEb94IGc5BfKPaUqe/PgTdOy8rMFImqk+5NDHBh6Yrxmpz6vYhZKqjgvRvXAWqFNSoVXgddsQ5a+rvQSDBAhljQuB2bxFRCs7iHOzhDq52b6UrGVSWARiFRe911G6hAPttsuq+xB/WJTEmvEA2Id+ouaJC18jVVUmKyJIHFgXBt8BD/y649O3ldVWOruTMWGAoThVfhtIDR9XADh2s5BR/joo1+W03SeN0Dwm9ZxCwj3MBBe6o8GCC/jM1Zn1joQPrZAeH4vWEvjODB3gDiKWEUsVt4C4jh5w6sAh4IGIfgZ5DaBssaV9DLAaZAUlAaaFU4ZFK6wPMiJSGB5OTdAQ5D0ouGh/m3HvAo50+ZcR3mhuKSvWCK3yuIWoRVUUnTFcUuOfAAxiNOIpR1SW5rkDPYNUjr1qNwc+CbRb4mIUdlpRzhrIIg+Cfm44VwqQByld9kxFRgA5VVAEwNnOzQ3TtARCG/5VXZNAQULdvNFZVCA00H7+PX1GIXvQ03mZMIQ8aI4IzETmFcxyNxL/ArxF+f0FUvEbZdF9Yyi7EElCXATfikQ8vK8hP+oS9O0PP9PNhjJqPEgJsSBGTVGTGWnBQIiANAnYefqqNFoAMSI8RNIU0Gje6g7P4FGCFYDTA3MTSA3N6tszP+XESEKA8Rw0JLgXsV8n4EIarADQwSvnpcRAW4JNZs1QQoQThklhiVyqyxMPdwKKmkM5Rwu8Sg8R10ZMhmsLk0sb0YErMrji0gPOUCVtdMCUWQxaJn3mQWjQEhfXU80BIKuEW6xWE9jUNphANNhYoWKekQgfo6rgeCzxul1Rg2Em3TYljUCZzY1kKklVQPBwygsEXejLOdcaYUCBNUY6QwcH1LMnIq6NE3brhGwSktjZkYDBOyspqYMRA7a4ZZAULyjCLkny6uAbhKmpuMUZKIhIJJ0RzJAHCcBIoATAb5EYAkRvmCwI0Kj8QSNJFKumgoQ0bkck66kr1kifrMslzsa9RcguEFOr5qiK5wKW1NJq0BUUWEMEGjz0s7C8uCpPCqPpExNQr5RIEQ0iPm9hVchmhi4/KLLyAn8g7mdMwBQ2EjmToPGARYb1qIQNY7AMlOk7uFEtYjPvQkRWIj9gJTlPoK/XOVMixqlsaS3LBHSw9gqy+k0hnNYSjrJL+FaJNEVAePiamsqafU+wkaFkfxO48Cs2cksEM7DnI6kNM4TlD5ENmw+KkBQbmGK1Fusunl9a1PEKqFv0iw3f7fcW+lrBvdjTJC25FsRZOo76zX9jXt2+pss89tHb3Tz/RRQYHqsUx+r+de32113S1WkP2vqD/06EB2IX87TgfiENJ+RpwPxCWk+I08H4hPSfEaeDsQnpPmMPB2IT0jzGXk6EJ+Q5jPy/IeB+HPbr0B+O+/jwiWf3al/JUgdiLWtCr0wOD+VHYhgokwE4R+EzEeYTH59iiMObfxSDsepejmMnrayTHvmQgyW7RHQLI6wIo+4syJIDHpk7fEZH11tndMy1VMC5gj7JuNK3Jjm0WCxL670XJBX4Evpw80O19LiqqnoZH7iX9z++WV0F3J4A32TNR9tfgXzS9/oi1Vl9ExzcM4IoXsbSUaAGFRr2+nTzejgCh3YK7YCRLLO3DUgOISN8d3x76kScVgForBRVoBI+go8t8m1pfnF21/a2g0ghpytNL4CIuXXwRdA+FJs8Kbrs6fAGIFOOBbnTWusAHE0mvU1EEdELvkAEEfjsiLvA2tpxIeAcLeByG1yy9JWgKh7RWOHgAsRExweGlkCniR+uZaGMKWIEE0nhwarWgSJPSJAiLJHQiXkwA8SOSUK64P4EE78XzOwo74ADNOsPZTCAqF2IG9RGadaEGMFfA7LMymlKb+E/WOpcJQ8q0BYNY06Fzgtal/mXTQ9R6fmIG1S8QqUVhQ3lqUNWeHEaewQcXYmNPw0CSsiypvBHAmFg1akCOrGmNU6EHsEUiQTYoZoDC/yMM77+QQ9kfQEXJQRrA0yerjoK/HC9jD2YCUoQLAdSYEoawTRBKhwJ3yOMiJsacovYe0MvHntsEa4pDQHIUxUuVxtn/Iump6jIA1e2qSkBZRWFDeWpSH+S2ZpkAoFIp6QJBQtNXZEHFlHIsob9sxwSD4LRxw19oiQqBAzRHIHuGPn4w5MD3kJPI8IFYnItVb2tCOC+vbk1kYESVdwPaEeEba0zC9x3rD2JPhUtGoadS7X2Ce8i6bnNA5NKV1LK4oby9J4pbuK1IHEIMEKcAHlsVojmPMAIBxGTOLPkR3jSXQyBIgqEokAoSE4FIi8UJYukVpre9aAiBIBaQ0IPr+qNaJqnfBLbFcpEMo3iQsb5GrP2gfeRdNzIATUpSN/UdxYlsYKIEGB4NghOFNjVCaGuWqKeWoKTBsLTAQbh4uqWlD+Mujod3CIpSJAjGowgAgZiOCUCKm1WnvQ3SGHf4IdLRAYpaRrEUfw8SogcmnKL/ESmKcAEbL0lUxhtk/MZCLKGaOqctie43Gd6tJRWlHcWJamvYWhxrFDWOEiKhkxmDVdl2PWuJhneo0KovpGyK/3ERLRBAJEzHAISnHkwEjDFPJ9BNUzJF2s8cvaQyn0PmIQ9gUYGziCaC/EngDkVG99H2FLy/wSvnzQ+whhmUSrplH1SW6XWJt5F3XPBb7mtKVraUVxY1kaeis8oAdxY4s3lQPj3fxrd6F+M8VjR+Jmyev8kvfeCZe61nruvToi3n3KdosL8cj2Ox5xxLsl92dNfwSI+0c7EB2IDkQHogPRgehAdCA6EB2IDsRvKq8D8VWA+F3IPt4Nn2vB55V2r6RbdJwPWNGB+PNAtI/uIrQs+Bm96BhHftNuI3/cKGeRLIiHwapa3GqQvgfKGYOtJT7YaVBGRmmhUs+ILrNLFjofNYelUe+4wulUHtcJGyNYu+VZrtO09u3Uu9FXsus+4fXYgekX4itgTwVSLba0BoRfyDREeRutcCisS3OJI73dOawCEVaBCFWsFPnMxAR+e9oED4kus0sanY+Ww6LvCKpzit86DVZwIgRbCh+fytussXpfu/BHNOjHCr2gAFGzEQQIE4rG3wViZaC5Um5RtbgNxLHEVIn3gVh+L+CwzkcLRJtxlcMyVI/7BQjDgmoD6dBxHzeBiG30lUwII5eO5Rl4iWrCLnUFIkLjHXoY/IIqsw8on284Dy1rAcLrpA9PXl3LofBGw4AcJSjnOinb4qjkgAR/sEZ+yXFOJo3UchQtEJVb17oDmB8XjIij6NMcWT2DjrbRYFY5LKm03EmsGBGTSIaNkUuh4yGTdY7cowaI1EZfgWi8gzqE5Rl4p65IpYWAOcFuP9HDENWNkJg10XIeWtYCgnLE1AIBVQt9vT5IOW+ZbTFmV6iuFYj8kuOchBypJWAahQszqvea6T/kN8caQW/NUzkUvQFH22gwaxwW2lcisSBWDNZOk9qUwlFWorZv5B6yQPipjr4CMSEHdQjLM7BOcA1Mc9LX4a8agAPsgzfxvdach5a1MECHwikQJfCFGRGg6lypTGVbqOiCLhEa+SXHOREoqJ7hCiC0LE3pKVeU65Xgk45wLafW+VjjsAx1y1OZ501qk4aDe8SiiuMqIFqGh0fcFSeaQhXPQBZr70wYDmZtBBu2bKIz8Y2JMzXnoWUtOF66MKq8s0BUawR7ySNHdFG2hQKhS4QSeHKcE41dlLkhgdkViAwzChCIUwEgMPePCyBU52OVw3KxLfdmwS2pbSl0XK6q0i0gYl4jAAQP9opnkHzDRgBzghUnRuVOcFSQkWORJMtmoEbb0sDl8LLOKHMjLICg4Y9y3oqahwCRNKqKyOwEG9mF478EnhoYiCCRYYKuEXMu1fkQTiqtEWMBwuh8bHBYYonEAkmuEg0mszFyKayAVhZrEVZSBYOW4cHzeBySLGUVzyCKlkVmI4A5cUqqh4HFOkrAF/CgM1PC1aWBy3ES9k6QcitVC7lo8KBmUsQUYVsoS0OB0MgvGudEiZxcz8Vzauz3vB/RT5hBwkQy4pEILfQiod2aaDBrHJb5WCqRWBArRiO0GDZGLkXUBJ3qAw68WKumR8vwIN1BSAy5NT5CvKOlYejGi5y3fz9yZ31fzeNWDcv9MdRqGtH9ynaPe7J+Z72m6fGQRfe7IX5CMz7jEcd9IPDewq9a/qhd/VnTH2pPB+K3dl8HogPxpwzvQHQgOhAdiA5EB6ID0YH4LwPxFZr8+0v9+2z4TwDhH9jz51tWbWsEjJi/ybPBkLKznZ5txpWHNuB/EF+i0bZYqaBlP9jSykNFiQBhuRP8aDQzK+rQFMLxGKoCc8FpYURypqZ7ncS8kqnY4OJWzrDa6qH5XOlrfxMIORpTfjYsb4Ym23UqeY4oEI22RVjW0rIf6tJqIOCOiYXTUdSXV4Hw60C41l57/BEg1LUkQKStnH4FCJ99HH4B28NAQFZcGp9UdLw+w4LTWCY8HuISiNWuaGKl3ABC46NEEahZBcI/CIT7GBCVDbeA8CutvgMEebjYVX6FC0gZEmBHHNmxwcLoTsTY4aJnAoCyKAZmbWhEFnodvta2KByHhRaF4VBcs8oFoqQQ1yJmX5dSGeAlYkWQeJw0SolYciGP4HFS1Q4qSzU6eFJo7XXO1FTaDXUPTR3jlVkqZEMsvBKn/BWNqKLfB47pom3LjBNpFeuZBK2JheFVi4OCvsi5IawI1YgY+RdGRMhxSNhk5udETuNysAJ92bzVtigch1JPy37wFKNCvMlgiZBDMybVzCgKF9C8jzgu7lOxfIRPWlU7+OVnbsMofJLGXudMTTFIdwSoe2hvxMiWjeVkUPIDyooJkWXwOn7IQRSobaU/lfsCP7rUKz74EePaJwVicOrL5/g4V3Ak1JEeNLxBHhGRmRXDNToFwsm7/LW2Rc0AcavsBwWCRoxEeIjgjzhVsgnelXUSxzVKyeBEA8RlH/nsqqRl8S2USCYLe6WlqIndulcOJYLyuTeILfXGJZANo7HBo4/meqgVpFcywE18lYsAeOHZETqI0EQ0QLCNV3XZMhB8jkEjYrQMCSTWuFbEkYgiEyGrhuKKho1YrN1C28LyIGotCsuhoC4DD6MAEUuApbwCRBlTIk2BZkyqCFKAiIminLyVNK611wKB1BHSOQYIUiF54+8qO6E2QJeE6hmRa6K0oaxRyq+iiYfJclHYJjqGozAOBYgQSzQSFlNQvKCgIVMTBVOJCO0U9aqJ0hwZPLoGIsEfuOlbbQvDlDBTU81+yECELAgUo2EIKf9BGB06IszUFNupiae5y0VpOOCsWHudhTRmTkfI+iMy3ejUVNtAkyXXHxFZRqYmiemiZfEa7Ersl8BaV9lilRtyKtyDKYOVlbC4KChMYKTFJ2AJJBUOvY+gJSwymwE6HRqRpdW2MEwJp4tmy36gy+NBeBje1J+194T/gE4gRRCwOspizb94sVbVDiy79KkLc20v9pWWiqWTMEakDGaW8GJd2+DkYgGMrkGIp1Ab4emIv0enNFDEfiG2idyLBSzWfloR08CultdxKwZIrPK+T43i8TvbR0u9X2Js2uo+wYZ4q5a19Pdrfr++RmzyfvVnTfHDbX0PELeP/aqKyR/ust9f6t9nQwfii9jQgfgiNnQgvogNHYgvYkMH4ovY0IH4IjZ0IL6IDR2IL2LDHzPl36LbPF72r9f7CZZ3IL4MEDWXwmzveYBm0salkfv5H28HMXl3Xhbx9ErH9of94fnl0Wqfmk+txX6jErWrfr7WuZ603tlCqXW/XtNBch0W+ysg9pqgSedXvi26SXU0ggu3gl48BkRYA+KA5hEeP39St+zZzl1uN/19PlBzduf5n8m9d7umPNtTu+pT8T5UvUIlOql31+Taab17wguZdovG7blMpF4CsauBOFd4vGtQqY4GOyR+CxBn2EWdig457OtuLUA8vz6/7ja6/WNAUIl3gTi4w2G/UWEGwrkHRkQBwkQGES0L1qcQ54jhIPDfoiUB38hJmB5Im/jd5pr7oHFMtOQU9FVg5S/AAkpNx3/+nJtwfno50JRz/vmTuuWwf3qZ/7mnl+f589sLTQt2RHzjvZRmPk6/fjje94o9c+rDgf7uHJfzQp1ER57517cXpH7mMnREUL1Ij9T0mYE4u7PY9MPWh3SwcUdTGZf99HLeq4Xf2L6fP2Ht049drtmpqw6+MHaCiqsSvq2ieuHA7MmiHnCiU/oxnNj9mJgrUXMfqLRRnJH8Nj1csLHwFyJSzKlHDpZEI2GeqOmUO+iIoH+0d963/05/3dNB14id+z5/fOc0lHf+RtPZ/Osf3sMFcXFc7l6mkz2PCM7p5jz0m8rQNQIjAvXymNwDCKqXvj2/0L7Dd1sfLIWNO879nfed2bo910Xp59L3JTdqzpFBIrv7iUMBfgJeXAdLQqVyhqu+L6xrdRAHObr/DaBV3AcQBObyo0YXYbYFiVg5o6ohsGFE7BUIGRHPr7RQ0jm2nxdLbqrbHXSAU4Of5v+fXw9OQaTh/vwKIHhMHZ4k5byM8mT29LpHaQBqT7+pHJ0gMSIyEJxrJ/VSzm9k1StOD9SHiU1t3LG9KJvseJ7Lp9+UnkoouVGzsggcdIWIQyHUSZpkUhZTEgd4AaLwD9aBUO6DKODkksGHGDhGS1HVsCNiHvYChFkjXhxNES97PecaINzLfM79X3vXkuO2znTZghsgNHKAZE54JHgV/QN3AT1I9iP0yMvQ0Lir/FV1qsgiRUl2P3KdfFSQtiXxUeQRSVl1dMopEM8v6BgZEXT5v7hXBUKv8QMA5876rmPnVYEoRoQ7HhIQVNMRVh5sfbwGiI2c+jUBwXcgr0cdEQ653UusOYqMgFcxqnhN6tI0NYHH4F1yn0NvA+copklSyIhTE4DwWjJzKgalvqiqxolhE2Wj4wuAOBzcYmqahzOfKIA4zrMwQ4QrFAsgpiaeMg4vaWqiCWEuc57hUBqdmXN/51npsAIE51IgXuh+4kjH6GiqT6am+bhOTTj2zz9H6W4qjdYI/j7ndi9as4uRQSQ+CaJ+nOMSnVQvaEEugYDeBhQtaEEGldByHwBEH0tm2ualnxJ/gZkWjlIHkHHmy5cXslf9HYGFkRbBp/n43JhXCwTN1vPijgX1FXvUMbRIcg5Zpikllkha7Kl8Ku0ZVy3vI2e6r3mSxZcX4X9+8LcIxIGX6n9sfbKoz8e/ya0AlU03HpSGUsxTFLfq4JB7xlNq3rzLXHINPuvXbFlykL/dXb+s8YPLLW4Ud5pV2fLbzF8/7/tlfV99x6yEm35BLLkGnwVEWXKIf9/7iMM26Pnm39pr3XEfEPfV9w4gbjHis7f2rOl3FdGAaEA0IBoQDYgGRAOiAfHZVjQgGhD/ReMbEL91q0mG/Mnbx+VO/qNeaECs9sghf9xxWPnMUuY8CPKSPb3M3uOX+MSS3SrzkVc6Rg5IOkpPe9n1zhs9c5GYJuW2zw8JtWzyBEsUQ3jPhGgIbreuRU2s2p1USLQs71TZ+1oA4Qvz/LrdneuyEmL3HszHYQeIpxyr2f9Fng86/4xn/gCLnC/sTyD3Av19AkvDKUfD+2rP+G4DiDIOiWmliZDi+M1XlDSuqVBkh30lCbxiUYXEAOGTNEUEwke9i1UgxhKIzq0BId0UHwsebwHiGz0KZu/mtwjEN/JQRneJAeIFeZ/4ld06EO4mICxceIvZpIdHkL9Pg3OrFCGfNDJUnd+UKGWhhFiWX3ajAiEaDbH0legzSyBoEgHDAG4TYlAwT+IFR8mH9PyqLAWelpjRkPgK6t114gVz7C/jby8AwsFVTt9eyCedOBoS0yRGZ5FoKBeoduSRWpQJwq8MO4jFzyNbVTBGcr+KHgdHMQlOS7rgFWDuBNX7oInrKqWzbgbqhfw8IqhQiR5aJJeL5kJZpLbB5Y0JiFTCeYraHJcy+ozVG4kqI7qx8zG68o7MkxDH5LwvzsboRtRUcP6xAzkC8YM8y8fn1++AhByBxImjscKEFur8g+FoSEyTGJ1F3tYfoNqRR2oBE2QQpQJ9Tf9NFC06eTd7kPR42xklQVoCl+lbVCyQ74OGZkDqLrJQOnldn86kXJrHBQT5SECkEoiboraX0Wes3khUGYlAMPPgoBwEcaw/s0P8IG7xg6EeEqkLnAk48gEEg0E0LBoZ+MZOecdH+KiMiMTREDdrjM6i0VA68aXbSC1ggrB2R4pDwg3r46R0vmqElY7DdqCkN+aUuDEB0TN75c0rS0XrxbdImWAlEfIbplwxz3JExBK8anNcl9FnrN5INiKYbCJdShwE+v9dvjPXIAKhDAdK86RnhK5igWA3ugAB97mUzseYpRE5GmJ6is4i0VAUCBupBUwQXoKnGIeER4SbQlxbY/oxATEnnnrQ4qQjkOMthfySevFN9UiiMsc4xlxj1CdZrBFTDkTSBrHRZ6zeSLFYg1qiLAZyi397PfAndd4Ld6N0ryPiiJmaDmlEYBlwgOhAFBPk4bsmd5BlG3dNiaOhVJwYnQWTwdAldolEauEzI7QsOIqL16npNAWflC4wcXjcNUlJo4BDa8HG1BQnEpWe6JQsFMaYa4w1JLGWTuKsaAnKbSHby+gzVm/EliDL74G5BLQAE4NivuL5k5kQzE94fhGWgku0QjAkAMS8Nswp6DfDj3nxBkHyB+fnYz/pVwa4DvQ7InE0ZGpK0VkQDUVUO2ykFj7joGXBEUtksWbuhyytUY+D0vtU0ugl8oqLlM8Jklp2se7i0go9EizWrMzhUy79FuLvCI2VkkroJuRj24voM1ZvpDMlVO/olg7tp9Uza9tayvova73dsxoguR7IMuqLywTjuh0VDvC06iXmt6z6e6S7SSmkjJXSLZQL16PP2BLu6sTP2LaBeM92W9571C/uSXvbI45wY4/8xq09a9rqkQZEA6IB0YBoQPzHJjQgGhB7PdKAaEA0IBoQjwbE53RHvRRrpv63+4/fwX9clX8OEN1Nx5N13U2531d3V1YZnMb5iFE/6Ik9Hd6WFNBH8NfqcyPDcTjH/z7u8zvCPnIlTN712CP2SFj8jS2pxauY5Mvoas3y0Z/spVe8huvI0tFeOE+z22fCu4ajp71Rfd+kfi5nZwfSBA3mGDZktM+xepTmMyA6E1xeX8mFCcuwE7n51r3hXR4AxIyAUf93aV+ayl7FrDs78+mzY+mIXwWiy4zUsnyFkqClZTlKILI60Z0q+w4PBfnHRy8XCY6KYAAi20MrX+wzjJIUd6WzI+J2IMxj23cB0el+0OgotwDh3glE9QJyGY0mP61ABBXFd1tAoLsZiJMYEM9yYIcw0pkue2hfAiGdxDFHXJJH5wAbrFNPjAMTbUVZFLMjQ1kKEuBm7tYrvyCcGBcu8+h28b9EXXGQk0CgnLPIqiMvrNCA9ijzylFX0hGr42G1PiQCjMi2Q2EEZ7xLcVWI66GRXDjiixPl/KAskGQdnT1Jy84TOrAOhEZK0rMnpvic0OXi/AkSVUXbInFXFIjBK4snxkDhyA08sEYdVMqiUHcgWArKZnjzTuMlxAFpPbr6H1FX6HtUMfA6Q0lkEXF7auAOckK+IR5LPOKMjoeX6CnenyfwL0wgAy8sCibg+BRDZUDKQV/wRyyJoCyQZB0ip3iJhoL9+cIZcfkAiPPsAj3H8FZ6dmAgBsdnRh0RKMdrHACOuyJT05t4aMEtiPoZiAASgUgsCownsBTSiHjz/RUuc6RdHxG6r5Mw1UhxVYIofug11187qYkILYhfYoFQHQ/+luyWyC29xDjpVOUghmcA10NBSkoLSX3EG+v04kxt7+arXK/52WXKE9CbT9NmOmtHhItA4MKLQGgkrwQE5tega8TIbLkYbSUkFoXM0CFG0+mkFAWC0u4v1iFGR/GiyoHhnga/dLtTIILPgdDcPCLi2tbFGCsU42QJRDDcpRoQVFKyToFIbZ/7Za7txKtACQRbFM/qGpFW1VUgqLOIX5BNTTq50HAf03qvLAqhFTJLIQNi8FG7Q4GIHl0DRC/fdRJmQpjEb3E8SQgjg+OjKMWLj8UjLup4YNpAlBeN1VJOTQUQXiKreG1tmpqUo5Gsc5FoplMbB/dx4YL0AOJycSKNwUflrN41pWkuSDkGiIGBoE7hUEeTLoESA4VjnGiQMixxkUUxLzzKUki/I0YKbSP3zbIQGY6D/R3Ry/cERAcGBPJeYEXPHAzV+6C7dWJo6JGk40FhZaD+obof58ku1nov70UNJMZQ4dsSqpvru2DhVBaIBeIs3BGk5Z72+ksB9E9MxWTbKNNj/jsC0WNQN8rxclOEuCvzt+4dv3BrrIO1X9aJ45B+TYeHeMQR4rcaQ+QG6/x9Z3d/k/+vPmsKv6FXPqEL/34gfkevNCAaEA2IBkQDogHRgGhANCAaEA2IBkQz86+z8JPs+53NfB+LosbC+BgDY9uue2vY7MFCGSN/bBXsseRaD/USfJ6tUl5l66P7PcswZnoPY2lSWX5fWOjGf6+Sdqx5uJeNXLGLE/SprvE82hqdZYIstpB3gbfkhGVLNngOiYTSbQARS5C83tIBNoBQz1eXWavX1+i7ao46EF0JRPYWWzB7Pjp+Vrort4tNsQ5/RJ93lkoRv3TZoUVNBoj1llSKdQUbyDAwNrK4PGLKu4EIwb0HiOhiy4FwCyDce4HwbhUItw9ESFFpgoimT07E2k+T7pU8h06OIdpK4jicmD8BxXsWXQhw6fDLs8y0YHdL6GN8Fi3fOZV/T6wGZm9cVJ0j+nkpN79YCxbG6JUv0k8mIozkg6fRsjA6p/oXWhb2lAfSO40Ac4pcjaVdpwmQcG5wPnwvNfbRAlhzZu6IOtq0piu3CP2Y7GPnUXD68jcYasnVueQ5OD4Gx2HiOAzyIjjynifx+g6YUDxraszHAjsjRTdD1DeUjGVYDXCHSv0+AaGvrXuVphFWRQp+QIySM2txKBCWheGY5cFlgZsyWh5I5zQCzOCVq7G0y3tVrJmbA8eqMES4fGk14sf4jLKgNb2hRdyPNiQKWec0tknPkVWIq0BEgRrPob8GMbs3HAdoVKhyBo8Izu3Yy+w1RE4AB4Mc8xJBRfgauTO9k7xR7wLjeOxYBQM8DSpZ+SIal0V1L9RiV7Aw+uvZQXPjzPSIs7PxXISqwPEulKuxtMs7A4RXIHr85Yg0elSAkGg1iXECINCPyT7P1jEzg2KbILlXXsWC5+BGDepB0VYsA4M1KgZlW0CxIgeiH8WNzgwJyeWdCRaSGpyojCEteMKTkLf9XeKLBI3L4lLAERABShZGGGV1UGbFlHggFgjlaizt8rJG5ECQzobkdyUQweWMkzcRmwAQwaSY7QEzA9wJIbX4Gs9hBmIIZmrSCRhTE82OIgUSp6bLRWhoTthwA3gaNkJLyWpIUxO6FA53BoLjtQCI+dugAaqsZocCQcILBQtjiJ0XUlf6eGkFMzVFFkthVwbEkMriVWlIU5MyXcy0KTUJENyPyT7mrwQhN4I74SW2So3nMN9BX9DhtLikYkBRdJElIdFSmExynjrhdBD7gjgfrHEh5btIjEyshnNcdMHK6KUjz0KqVJU45YugtMgo4XzzUuv7gg6DmtFN5xgnRnkg5xgB5hS5Gku7gCMsxLJ8HrksCHTFxZqZHUIhzRknI4OJfkz2EX8lbP/GCqtnyt+F2wWFe4IF3mjBdn3hN/72f5+Nd9l3OxDb6hX3aFt8xkb1/VVAfHkxX7i1h34NiAZEA6IB0YBoQDQgGhANiD+mmQ2IBsTf0oO3GPhfNeJvEHFpQNy/4Ykn3u+yW6h829pWU/mtFDm7YbVDOIU+w+q5LKuPwU9sx60UVkFjPaVV0ziP+uLZgu9i2xzyNpZtvmmzwgTB1/k0S5d7Hk7Dr6RyldAlHwXCZ7IVVh9DBUfWU2QKGuspjZqGM0D4Bc8itmhMja0pfVhlEb+G0fuAqHf3B4DINDDcepqsUbksA+tijFspMgWN9ZRGTSMDwt0ChPniESvCVUIaySWgjppSIQKvyZI2xUkEzU+ilRG5D4XWBbtbZGKwqU7Ct4g6HZc+Km/0klpLsBoYvcQlOcW6WCljigwNflH4yiwKVwiV6Lv+6ykyBY3VlLl2wK9fJ7YBfBcwWDQKjLaoW1H6IF9jGPvprHogRt+D/ZDJdZkrRLByBWv/g+kwCsMhcR9KrYvAgQRSDIUh8UG8F94FOyERVsAnFoZTd+x8JFPoQDyTVFeA41FlzT2H2CCf8dzcYPUxoH6xlSJT0FhNmatp/PpFjmHoekjElwA2StIS6VaUPrhFrL7vZdpK+h7c4+BBLIUJTsLJ8MzegcMePAXlPpRaF+drN3ZO3ZtJKQPaFsy78KqWocobqN3JGeI3lAodUU+APd8oHWm088Ti0epjpOt8LUUm3LCRMh8R0dutlACEWjBaIt2G0kfPcVvq+h5k9FRViBjByeAqJwl6kWlglFoXs/+bgXCTc1YpY2D2BRQztFN1OQySOsk3GH6IqwHhJmVo2KuYG2b0MfLuraWoA7FMmdQ0dEQsgci0RMYNpY8LRV2xQBh9D8PaMJwFUZnwAIJSXC7B0LnKqSmkqckjwEZKdcIkN0BTQ6Ymr4wOpE5nMoUOCwSmJrYPabTzMK97Z/Ux8u6tpcgUNDZSJjUNXSOGJRCD0RIZ60ofqU8hBmH1PYrFOnEW0DWIEMJT04RC5I5auA+l1gUYFB0vUjZVEHZHJ0wIWqxVeaOX1HrGAtFPPupnKKWR+RqjAkF1U0SVs4zppI8BzaStFFZBYytlUtM4s54OLcMJCPAvqG1RSyTUlT44agu3HNOr1fdYiafy8fcIwsbeXur9X6+lekio3iPfl+K2lATEcqspeSyVPrqNHvnc9zZu7Oi97f5HHOETLQg79X5WSX/A1p41NSAett4GxIPU24B4kHobEA9SbwPiQeptQDxIvQ2IB6m3AfEg9T6MmY8vofL1ffDgQNSf0vymZzfv7oOqfTaaiWVuJK+yHhWdjKBqwiktpbIxVYoz5e5NDAd+j01O5/FXQiwovRJfVCIqGcGUZTejrbFQ5ehGcDi6uYykAWJKyBQ/Us6gQKxRH2xXjFm7xXVtoplYZkEX0yShA+6FMUmLO5MqfyZqA3N0VQNKhkPJavA+hRbJ468EkZMvEcyCgUQgxJNZXJFJKUPTaD+R84FdV/Nz/wSE96XMg5Vd3wKiy9teuwB97NB1ILoSiDHFQfBZZetArF4Ji1gmvvhbAGHUP+4Ewt0OhHoO4Ygq+8wVih8VINY4KPtAWAaCiRwyana4b9jZzqoU2kjmafDrvRpBosZqAF/htMlwwFDPo5Z4EYWn6aeIv0Jv+we81ltnUmiz6dx5SrFU1J2VtDXOwi9xUYXEAkGK/yjRs33X+PIx9BdghY3oYjkoOccF8WrQVjp7TZoj3B9ZNJMscsjIq0OALwmBbjxGyoiiPA/YgccyoqfUWA3gKwzxJfC1WCZl1BJ6W1qcsGMRfwU+Za/TTq1O2Dh4vOUeY6lA7cBoa7BWCJ+iT6XagMMRok7AwFwPiuoiL7WrKkcoI7pYDkrOcemEsKbj7i1pjnB/ZNFMMmZBHBEnZmsAiDQi0HAT/GYkbsWS1aDv9VM9WwyHktXgOZ4KMUV0RET1DypHdC7qTIo4NQkQPrJEeDxdk7YGO4IlEkvPjJLZAmZ5nK8CRJR0oBgW4LEg5kvHOiZ5RJdkZclxQbyaCASTMazmScZAsMwCs0awWgeASGtEAqKPoTyqrAYVWODc6wyHktUgfJIpxDUisTs6YWpYIPI660Co9kfS1vDSzUKi0KlJ7FwCYWO+dNAxySK6GAWQguPiOK16pxWI1B9ZNJMFswBXYIhTk8ddk/AuVAwn3jVVWQ0KBI+yOsOhZDUwC4SZD7CqiL8SJ4UMiKxOxLjiqcnEUhG1jiFpa3CNg1DYhsQCDBJ1LPJbFAhMKEMEoojoYjkoOceF2t65EAmtb0lzhPsji2ZimQXpDkS4FhdiaOB3BHgXlAYqFnwFXAmICqtB1hhmY6wxHFzBasDUdMHi64v4K+frOTJEVpgU7jz2Djoh3ZRiqYj2xyVpazDXhNMgkgrob/KfRzr4LZ5jooxeFmtWJDlPWIBtRBfLQck5LtT2fuwnjasy+qg5wv3h3rPld2chycvxlrMaahyHUJwNm+Xf8ohjWUtw/8W2bWUX6Xu51Z9kq6J5+yOOeyv+e5411a/8e/vjg0Z8rOT20K8B8RB90IB4kD5oQDxIHzQgHqQPGhAP0gcNiAfpgwbEg/RBA+JB+uAvNe3hYe1uOHJfKX87EN3O/nu3kQubdJcclfnzETgyg+yFlVI01sgmr2E3noQ8183rClEhYcJzWrX032s3/nuNisd7QIhVUN0gSQh9putW2ma5H8bMsbBkXKszlK326USw7AlTfhYYJQ9XIS+qrhibcxa6ChBdwRUo6++yukLh5peGq5XecClIEj7Ep/8ZvaBb1G04IeKfHpPnwLQt1Z6CICyJBLklqQ2lhEaXtTqeHv0SJN3PgMiL88nIbv26G821V/Aa/PqXoik2WwUI5zMuRSc+xuBKIGzdZbwV1bmfBva/L6/gWM4GEJkllZZujwgAoWwKZUOAFqC6GQDC8hGCV22N2c0xpggpOWeB2RYhj72CmCi9Q7ySBZsjL2EUIAKinSRtj05oZXj5t3N6Pgeiz2OjSMQUVgrhvIYTIkCQtv8JcipTnzRAhPlBEVIABFw9iL0i/JPCEvQpe62nxPLgcsF54VgzELcXmSKqS9kUkQ0xfyVvlLIc4EVKfAREThkkzojhPmScBQDRcSydOKj9yAwKTHclm6MoYZQ1YuxE0yNFP0FsK9CAOqfn+0mBgJ9tkBf42fMHl6jQH1xmFTE2qIUUUiBcCAjkzLgXPk1N8L1KgAMJU5BbAkaKhLqKLI/zBJaMxpoZ2DU7SrrBJ++t0wgL4EMoy4GBMXwEkW/wiE+i3IeSs6BA9FfDa7hSJJROdDZKNkdRwiiXBOgJzkY/6a8AwsvMLbawCgZ3HGswqaQFxoqXGBZyE2CsYuUOie0wd81VtUKU+IDWJSB65ookZZKKJQKf6pKAnaKBcDTESZCIEhGwJRDCh0gcOShmTDkQQZdT5j4sOQsafsPGXoHoSIpXkkVsyUpQpghGhI1+Aid83nw+7nVEcAXTFhCZVSOASIu1aIc4jR5DpZnFmmcLHhFuCURwORCUm8kFbh0IXiO8oVpFIAbIBfl412T5CABiMKGZarocEQgTOSQMFAmFuSCViC15CZGkFYh9aBVAKLLLAgivzexybRGpC9cceCiIaWKsCtld08CrpfcuMVDAWLHiRWF4Y4URnZpySwwQHtHIdIrSGmg9Qs+RFsgMBBPMhE2hbAjiKHgXdTOmko8QhMCIxVq5DyVn4TxKHBQTOYSDoIELUonYkpcA4RBhljgb/YQiu/i4RLL2kjA6AISJCEPXsSy13ikPBZoYlvZCvyOg8YeIMaIdIgwUKZsJnWf5TrFXgssX62QJGCn0t5PbCC9sj1hDwDcOezbvIu4Mb3U9ie0t3Tlv5a79hl2yF5YldNXbyI9v3apV99V3u1Wfa/+7t9/Z5Me06ivs/+QmN6saEA2IZlVr8sNb1YBoQDSrGhANiGZVa/KDW/Xopv1xNu/ZVZ7P9hsQDwAEuTCtkz84t2QsxC1sVBHjkvAr3B0/aUV6UfKIntrE86iVbWuP7IkAR6m3DfBRqABxv/3qA8ibY5qE/SN+u1zqWDwxdjbshnwgMkvii9C+8GZGFkfPgViyK3xmVp0fEdOww8gb8kGnqhvRZe5iLQumBt7ZNN/VzD6+hdbxy7RLIFZ5RZnexDoqNgBJUtsoGBlGJ6QmYDH7/0LUABlzzgY0xpMCise+t0BEtkUNCFcFojxrgehCMLoZXTEiEs/DVzrB1m6BOEUReajWRKKHXkKhcyskr3cAYUeEr3VF9r0ChG3nNhAYIuTUt2wL0pXoXIqBwtFUopNklR8xZVZ5aEOoCkaHuCKTqlGwpoFwOJTdAKbGdYKv+Cxsii56vMgnTdQBdu7M5zWyCoLfQ0pegThJvJb4Dy/TBqoDCh02zYnbbNkifE6sT61X/knkd3CbriZiDJX86xdHyIA0fcbZYAbKiPNncYQJEPChupCpSIxRhYB5GyO7Cr2zEVKW/Ijgs4BaAWFxNNoKd+eg7kUA4YXDIYoewtR40/f64UT1CQgikKlQEaYm5YfMJQ8anAlsRFHISP8Gft87+KjQYdN4j9f6E1tkEO0QL07OKFTlwcRQm6hecDqSkgiuePYuhi7jbDAtIo4IlQGYgZmYXzA7s8dMRYKvPeYyiLMbb94jjsqaW96GMurGbiR6jrrYz1dlOQQzNSl1QWM0gKmRgEDklXxEdKOuLj5GVumZE8L1jzoixO8s3BDUqxEoFAjwK3qJHYPoLikSTCJWwOuNWwbUpfyOTixGn6iSCK54BELpMqoAR/iSEYG649RE89+bL1QkxiBqFTkQcnaNHzFZCSKvlBlRwOhMLKsaECL74DXKSSdMEh1LWCM6oyTlY2QVkRWZLBAizICIMVMM8lSOCD7PsVtYcSSxRYYFEBr7RYFwk8bniiPCBTsiHHvex4yzMdk1IjAnxExNl9mNn6lIBKvLQed95CSs8yOCTxxRsBmj8ko2NXVR40Y5JEpE8Ybr0wmTBOn7iwIRZAKl8xrQgycmnpq8SrYId8JOTQIE8S8C9D5SGgYiixdzmhIQg0+RYnR6zacmZn5EFROdmrirR8vZ4Hs+BULSm8WahqVVkUDoM+YrTJh5feQkbPAjsHyP9O98nYd3kKBlPspa8WItKRgIcDiU/wCmxuhF6WLCcsfsCbkPJu42GB50PkZWuehibQXevFsu1qS7ERDdJUvDlMnJskWCw3IKca4UKYa4LrixkBiPvAgHp73VccQX/JJi9kawnA2ms8p5LpH4IQDC/gj5jK1z3c47AvUUYTvSic9L2Lc57JwJbvunaXhHmWpdsqveqod+xLGtz1I/+xEgttPcy70IK3bdZnd71vQf2dWAaEA0IBoQDYgGRAOiAdGAaEA0IBoQnwrEn915f9plBHv/U6sbELtAhM2socrr6G84ogXw869l3JXKY7GRjyGi9PJ5TdqPdV3J1TI/+Zyf6QZ2X2kIA7wfNz8LHvHc+F9WUQbTI88x8tvS//Ix6ok55fUsb9bhSFFmemNuUc68N5FzjOphpQ9X1k7azvR30VvR7VPGWzFA5FoTvviWq2jkZaN3l+E+ulUgxAHqE4K+QNTW1cFFxP5O6yqKATlYpd6+s4oW2RxjfCsV8TvYpyLvmuJIXqZV/cjL4T2OoBBF3l1ZO3vb2dW76IB7gHBVINwKEG4ViNqIoEfko3gyVdfe1tcVdBc8VCcgBu2m1Gku/vM1ILIcIb0enIAQhQ6/LDMpd+TljJkzbAuILgcCLI0eehAO/Iac7dCbeChwI4F7Aa6HE7bFVRgdFQYFxNKdRplQvkTBbhCXEzmNcGZO7/kVYqeOqiteKdbXi2ERcTo4qMGJaQz3AJHlqAIhCh1VIE4RCFvOrUBQzAH+K0oorBoxetGDcPDm5r5d5mH4Ib1Mzo5Rcow7D0TJ4ffmocwh/mLjpARHA0BQ3BXlSxTsBqep4W4f6O3rgCA0QTgSbzJCwCTRV9w7LnWeoy+kcB+YQ4i3nuk7z+djAgLcE57bsxwauicBcZ5nelHo8LpGpDJVuaMs541XFepc1MOxWVxZO8WIwYhQZ6xTSQIhyxgglO1Ai1HP8UXAc9AgN/OVfQUQ5L6EixxHcgZFp7EY0ohQ7ohlNzA/BIvXm0g5SGg2ZmhQLW9xTqYjsAjTHq8Rw2xpJLWdlDpAsi12RDizRpgc5O0+KdNERsQJlgw4kpepyh1lObP/2iH9+VoZES6NCPw9SzyXAgiWSylHRLxWg1OmhBfJCBkRzsduXTIohKMR14jZ4im4QpFiLnc+OoApoUAoOY+Oewd+h9rjJrHILNZmxucoRLtAmBwsWDEWU9M4mskqLzNfrFM5bz7EMbAFhC7W0DyREUEzobAwRu9ztoPqZqSpSYCgWZFVMcC2kG4dsEZYBoVwNAwQyg2x7AaE0sBUI2e8knDouPdxaspKTot1sDO+xoFZ3jVBKGKMHWhGRAgFEKEAIhR3TcOynLktYQ8IpheMWCNAYTtNMiERS0Ei50w520F1M05yFw2NDVbruBDDwoluh2pZ8OyYMSjkjjv+jmAux5QrUkh8FFms5QzdsRPjg493rJeBfVnWYREt1vI7gmZq5o+4IHFg8t8RPLML/VOlijSHzt+s/4H9qyr9CeUsLxO2L8pRsOIacaUcZe30O4L/Mo9j8RsOP3RypYyS5r63T0eCq/+a2/g1yeVoEKX8Zjb/5bBUlewWpdao+fmx4O5vVc3q9XLyvixrJ3uDZXu4jW3zpLsvZ/o1vPUwYo3JsR+B509/xBH+KxP+jFJ/HxD/mQl/RqkNiAcptQHxIKU2IB6k1AbEg5TagHiQUhsQD1JqA+JBSv3DgPh4ccsSvrZjP1b6A1zNX9Es2hoQi+19jIS7dTvMYb9oFL1LnR/5HL6FkBtGvPW5odqxYjLURHzAG29Lm9ct3ueUdFKIqe49jARLIfDGuFVCwpjqLZ+dehFJsPk+h28hwhFBgehKq4qtK4DoIhB1m9ct3ueUdEvc72UkdKtAuB0gtLndJhC9+yy+RQrNQ0BAo2ALCLcBRLcBRMXiXSpDB50JKE7A+XEvI8FyEcjJc5aStpghnVW6YE9VHzUxEBID7iN2Mo2fxbfoJIoDrmYAkfNVauyTgsVygZuzZvOGxbcAkRQneG67m5FguQghBJ8C56wzQzqrdMH8Hhc1Mby6PqUsatbn8C3siNBScu98jX1SsFiGxEkqbV63eJ9T0iUZhji3vYuRIFwEA8QWM6SzShe8ePaimKGRGBIQvfssvkUOBDQKcr5KjX2Ss1gkd9XmdYv3OSUZEOqXvZ+RoFwEiiehJW0yQ8akdIESWEkDmhiyRmhZ4fJZfIt8arJAmBGxZJ9kLBbkrtu8bvE+p6Q2It7BSIhRUcwkt8kMGZPSBUpghYuo8SE8JZQZPodvERfWAIkGA0Tkq1TZJzmLZUgsvdLmdYv3OCWy4qvihN7/voORIFyEXj4Z0i1mSEhKF55LALMDGh/4HaFlse7FJ/At0u+InkeEBjOzfJUq+yRnsfBiXbd53eI9TslOYM17GAkuzr/1UsoYKmGlBN144O9YVd/fjw6zrK9m1XqLSgbGVto1e94TwWZ321Oe2G9yDYj9sj7D4j0galasPeL4Sovf1azPKKE99PvtzWpANCAaEA2IBkQDogHRgGhANCAaEA2IzzbyMzuNPmvf92v6ym7/tLIbEJ/WQrxGK+9v223lMVZiNLyPg7HPDMkZF3UrerFldVswLujNteL7Yf7H21HOPL0sC3r+SecOx8Px+VWPHePZp9c89dE9Fyl0r0xJJWb9J7HqV4FYkYPw4Hbcy8G4hRmSMy6cW9f4WEQ7KYDo8hExFt+PAILw+PWLuuXA/ffEx5z8/XGkXE8vTy/HQ+rYJ+3Og+laOfMUgVCkjyblQfJSiU7qRWP8+4G4l4Nxm0RCzrio93MXR8Q2EN0WEC/oL+oadEjq6hKIHz9//Dy65Yg4uCUQrg5EkYNKjECo+5C2/SgkJaPhfg7GrUBYt6aNbtJPVonDRjtZxEcxjAvtfAsKfZ+77OX59Xj88epefv2ibjkenl/nf+759cf8+W3+m40I98xH+b+b//9D3Up7js8gz5N7jsd+8DHsHdzx+A2p+LyOCKrXjIhaFJJNXQ6O6XAvB+M2rYrc0W+jm1C6pMRho50s4qNYxoWOgin/TiNhnqhpGBx1RNA/OjofO3zn2eT5mNaIOenhMH/9Pqf5PuecYZqPUTLKhzxPXOr3A2ajA2elvTnl8TunwnldI4qp6ZYoJCWj4X4Oxm3MkIL6YoNqFEocKdpJGR/FMi7WR8RBgZAR8ePnfLU6uqbnK/0nd617Osbp9zCf/znncQCLgDgANM6JPE+cnwqltHyM92hEUN8L/Ae1hEeEnZr2o5CUjIb7ORi3MUPyqWlYAJGUOFK0kzI+imVcrK8RRwHCrBGvbp5c3OtBr/EMiCN3uwLx/JKAOMY8vFi/8jDjEXE8bAMRRwQxG7BGbEchWTIa7udg3MIMccYurBE2zIxlWaRoJ5X4KIPVRZJFu/juji8A4nBwi6npcPyOSScBwWe/a3fzzJKAOCCPTk0MxDz/oBwDxHxkBQhiNuAOZDsKyZLRcD8H4xZmSMm4sNFNciWOFO2kEh/FMi44igt+O9jv7vBCi+w/T6/6OwKLNS2wT/PxubtfLRC8kP9Dy/PxQGvD8ysW64OcozzfZIHmqWkumY5hb075D+V5iuedK36HBfO3/hNu/cz7OBhbzJCy9PXa92xUxkUcBcX3Mu3a9uvXr5+148fDlkX1PGWaSr3vAeKrORgfB+IrH3E8v35OCz+tq76oaNce+jUgvqxFDYgGRAOiAdGAaEA0IBoQDYgGxN8KxOdV/KfJlTzY1oD4/V3OD6aeZe+ZvzFr4eAOx528Nb5CyVQwQCyKOxzz1Gv8h6cin7oWs/SHxL/Ichy0fc+ZGctS8zptbmnZLenhmz661X23dF0fTPFPsTL6xj5aA8ShYsSSr4BjJVPBAPGC4wcDxOGwXl5yuz8VVh9r6cnxmOWQPIcnad/TJhCHok57+Mm051Dik6V/Kjr+kO0fzMfhZiBmp9JxeVksgHAlEEWeHSA2yrsNiDQi4A1bAuE+DIS7GQh3CxDCG4mZjtL17NBw4thIQLw44SyAdQDGAnEQanyFn5HxUDIVfv0iVwnS4jhyzql/Uupb+A9PvPdtdr0/cx1HV6Z/ZieOzYG6yDVDKZ5iS78h9VwW0sz5js+aNuNccLlsO7X+hy2TraF0P3kPLY7MDplukRrthI0om3gj3FcvOOpe+O+TuP3YyQFvK7MW2N0Hhx6zDsBYOMABuOAr/B/zFNiFXjAVfv0iR6SyHpz4cA/gOMypv9/AfwCnYi7lKG76lzL9EW5Nk+O7zHvH709xejryObJi/vbyxK2Z8x3VN51zLrjc79+5d75rH4hT9CAz4f/Bem4xXKDf4zBHa9FOdrkekov1u/TPgfe5LAPEvEgDCHINcoexV5aYCIax4Gp8hf87/BDGQ+kgZyDcj5/ibGdWww/iQci8QZ97/AdwKrhkmZLK9FT+jywH9h1y/0xA0D9K88M9Odh1PKL+knPB5wy5JpUJa4jPQUA8c4ufnbY53kpwa9HOI5d70JYKqeCZ+xM9nQEh/AMG4uiOAoQaIYwFV+Mr/J+kqwHx/EK55ivj5bvM40cGAFPT8bDPfwCnwgJRpj/KqEo5YIOT73ZEcOc6HhHUQhoRXF7JuTjG9AkIYWi+6mVKQMz107X2GtvsdEQw34PbeRSKAdVE/7/Ldy5lAcSBhrEC8cJsRGEtZIyFGl/h/2RoLpkKBCnl+sZzobAaDvmI2OM/YBCj5IN0UZ5epqYsh6Sd28TTk6T89vodaY7gNNEakdJazgVKnW2W+k2ZsGY+W0xNTqcmumjR2qNOTQ57x8ML98aBP6mUF/TCk0tL1fM/P/ibAjEPpFdwHCxjocZXeDnQ36fXJVNhHhGvzHpwdBcmrIbXHIg9/gPZ+ESl/EOfPJUV6Z+FcZFywAYq5we3i9YjXqT5tgNpuDUH2tO0lnPB5f6DGwMuO5UJa+aUL7AeS/p8DIs4frOgtWgn98E/T5zqhXvjiT+plBfp1frPvMrv4W3GglstSWfJyi+69Vw/3adsqZxfP8sbzfIGNa9zNf1KuuXt7FZrb+6JGhDvYyzQiLgfiK/b1oH4YJfdlfNDQLzzmmzPmh6j+xoQDYgGxNeU1IB4kJIaEA9SUgPiQUpqQDxISQ2IBympbV8Kc/r+J0P2h9regFjdkpf1eXHuuXLsnu1YlHWABoacoybg/TnSGMC3xbYa3CHgY0PONmyX4atp70p1ZiDO1268xfIuakXD8tGvAGFd5Hqk5HDkz8meFiyPQwHEU1YWvUirDvSDvEdNLzb++tWNqjxQtMQv2+dNp1RyFCn0cFcW5su0Xa0HfbVETTvyiBBlkl3LRT3dSwmI7OI/A4i9/WNRFgPxgnRPGRB4/3S7OZ10pnZKtwg2vgXErV18ayoAwa9Ghvss92L5NMRkiXcAHkfiVPz4+Y3ZDk/xLJ/7qYwKPvrPs2PGhnA1Fufpm8vKeiFqQlLAIDt0RMzfBtLC7yfIo59ipJZ+ukKFg17OnTR8EvZJfZ9e50U+aIakFKIgcjlJlBQ6HzU9Lou0rOkB5ZH1VKL6wS8n401uN/Kb37uWe3kJ+SyWXy7RpkPkHRgnI3gHzN1QII5gP7A7FIwKPnrE1CTcjeV55kt8t2W9EDUhKWA4VprHGjF/u3TyLjaUP/gl94GgemMVjg5hYBzrDMl+z6+8hwH5cCWmFCiHXtNHlBS5rqHpMZRpKYXqLKyn8vLKPUcpYds7aCHsWB68RFcR1ZKe42yITQdDuPzx84nYC/Zt+gSEe/757ODyJ/8x2AoJCABYnn9mvoT4gM2ISAoY9GI7RgRLZeG6YgmIPqoN9Nfz9U1UbebjrPrRX4OKV4wer9UH0uLwixSY8lStAFl6OVqmlSgUYTsVVD9I7gKKIhCG2bcceiWkRSJz1viWbJq7Q31v5EtVXsFBySYJCHAVQFs5Cg/w+QVAPL/ISCrOzzl4RKSysEYkBQyOziVAUBupOY5VzeYJNMk+uNQcFnmAaogswZ6lV0hmwk1+kSJETRsTtGQSVY8ibYfxE7ZTQfWDr/G4WIdbLOc1IriQLdZs05M68F+yqemQjwg5q5oSwqjgo1TCQcloKCs7fwCnLpWFu6akgOFEgUNebQ92gJ8mVvQYSO3jzWvAtUCKQB4KIFjyqIv6i0xNnCOlUAWR0yRAIGBNUhvJ0nZcKymPbKXycoPBAkUsKBH4rmnXcoWThY+6BIR3xDpS3sFzpAWCacEkKWZ2yFnmOxDjG4wKHP0RGRtgJZTnWbXClIXfEVYBA0uefBs7WeJY+QPRWC50ZPSswkFL3oXuxukY9t2VFkDnkQ85UgpVEAkOUVLm4y5qelzKtIiPRce2UumCy0oiUPuYulssj4t1h8X6KhElp4Kau7YdV4+mhX4rV57/9l/WNMxznY7yZtXsx1vFPEcZzSW/Aa2lDTelslZW4qvcY/lnbOnW93Yg7wPi5m0nuOLtZd1V64rt95fxH2ztWVMDogHRgGhANCAaEA2IBkQDogHRgGhAfLHtv7trvkJM5c+Ad8fKBsR/0HLDJuAozpGb4Pa5CSHfXXAqSCW/3EOec0yr9d3H4ohnfGmHFWNHHOlOHMaqSQsBeTxBdRL6oHOINb2eoheHKWsvzz1FCs+IDvPvtRs5xvZOCWeJa43o2/307/U8ZkBENoFPOvgzKG6fm4AO0HTLjgzBHsdeyCD0mXtlr74tIPwCiN45dWzC26mC185Z5xH8cGzHVoqkTe6i5jmiw8zeCXgj9ktIpUCY2wJh2AQCBLgJJ7fPTQjZXgUIv9xbAnEPFyJ+WwHChic4sWcZHmZK4s0F09lOmvdPAsR6CulErksjxUDnPKbcK4Fzkx9w4G8CBFgJpGIPNgGzIfwpchO4gh1ugsQycZZTQVPddcIQDUHPpz0q80SOyKCRWFCfu4ELcWUr4SJSOXkItpNznvezKCpO+VIjOqmfLTthChzzTurcXgoLxGkDiI0SBIjT/M8AEYTwpGwCZkMQU0G4CQBim5tAXkTH+cFMoCuFgHhTl/mo53UvcBQJT3FWgpQq9eWu0np97HoMcJqqMxMhDCQUic8DdnQSCkeZRVTCwPFbXJhn7FG4FY4uB51Y1lLwf1Hrp0gxHAdgSkDMK8ZOCbxHaS/hQt8ECOIVKBCISaJ8B8zZMiI2uQl0tL92kVMRuNuoTAmHEOx5RGg5I/KJxD7xLtY37tdH5cpK489XXKMI/KFRVfLFWtcIkLu8QImr9SQhSU7Ojoj1FIE+PYqSy8AhOgxHmXFpjVgrgfdwwQ92RDgfF2u+noTxoIsngNhnVWhgDSWkAAg3Lc/LVTsG5jMg9klarP0N9VG5CD2iwzwO+gH8ijoQQVYRvVrZ1pGCUTn+zIGopwj0qUBsTE0bJYwhizKTpiZHjVI2AYb5PJsLNyGfmurcBJ7SiKEnzAQ6frlgamIuRNDzcc8jClFA7BOa2qS+cEN9/M3Vpqb+AuqXjaLSxXA8vERIJ2H+pmhEAQF4goTqGbdTwPLyrikC4S0QKyWE6l0T8Qp6h4iJzHAAU8EpN4GX0R1uAgWV6S/nyKmgOZTsoxQ9VgU9r3s8z3oe0j3XG+u77tfH5U61xdqhBVMWRUXu+xUIatNIs/mEpRf3+MHLPf64nULjueS/IxSI9DtivQTspd8R9NtFxm9Y3nC693ET7J5lRNj8a9t99aHpdvvXhK3Ko6jUa6v8Grkzxd52ewmrv//DyungvnK7p756zJZ0dO8Rx35LPt7W20toz5oeZGtANCAaEA2IBsQHzWlAPIg5DYgHMacB8SDmNCAexJz/XSD+1K11fmvi+60kt7eyHDKVCIotLZ/5UTlQPsTafSzWjamM81RmpEL1KNfrz9eKHfKSbHkGR3qkuSpzQ9QxhLtheRN4ihrAXQl7LAt6oEjPjv+9xndSN7ZQ7nuUUfZStBIPLzmouHqwMpmEIE86td9jh42pSGV+dBWJhaQxIX+DFW5wRgoh1ug1pwSHX9qRkQTSmeCcvF0K4gDaJOoY1omjHgBxJjEQ4y7LQnwNncupDSXNwa8AYXJlQMBK4cmcr4nl4F0JhNsBwnzhyvpiaNjOxquta0B0JRBjqNmxAMJFIPQReZczN+R0wZvA29RAe9xlWWRArL2CuAbEsAoErDzpiBCWw0DNh3IFSFTM5HCqZpGUKLqKtkVPzpuR3ICUEmwNZYXw39GJ/IGWkc4JsCMGMbmUOnbjB1fa0bMqhqucoSO9+NfcKMyNQX3GXHbOm+A98g+u011s6gQEeRKhxcGtnlylh5hPEq7SDycBgtMaPopaKdAyNYqbSM0fvLrgXVSPYO5Fpn6x1Lbo2BXK/lhKKdMWWCGDz0aElJHOUTcR10HecWZ+BgsljKUdePXcVc6cR3jXqZRemRsXnVZk+jK8Ca7pop28x7KgywJMDeJo8GiOL8c7tsMVWh2kXfCG4y6IHaAbJD6KWhnHWCfTt7xH76Bn0V+DkGNKJYquom2hxICeBEoiyUtZIdg/Z2WYc3FEnLhuAMEjorCD0usinZ8JzBLJRoRX1qCXaSzjTWBPgNhlWSCNjAj0KVYlfin+utTqCAyEWCsjs7/mfBS1chWIwHoW6qh300KJYlxqW0SGxsVNORAhql/YMuw5s0bQujABCF4jCjtCYglWznRmjbBABHH8ZrwJWayFALbHsgATwyzWCgTxRWpaHdmIUDt8zkepAMFFhQgEsypo6lFmQqFEMZbaFmZqIm6G8EIiK4R1J9hUo6CRn5PFN8SpiagPrrSDau1lvi/OOMy1PgIBqkinS8TKXZMAsceyEGZJWqy9S1MTGCS5VgetEcTiUD0PrnHI+ShqZQRCSYoKhChFXE6y/PAxq0QRSm0LLoMXa0rRCS9EWSH8lw1KZdhz6V5D+BGXM+lpgOGQ2cFsDSfaHdmZwCyR86S/I85o0ZiAyHkT+B0BiR+6c9lmWThhaHSRfMTaH8oPvyx6iCbPC7E4emkV13jJ+Shq5Xm6WQri3+v2Dxec76rnanfcH9nCDWne9cv6Zp5Gd7OVYdPaT/v9H9yjbv/9I45g/j6ulf8DQPw9VjYgGhDNygZEA6JZ2YBoQDQrWxP/ICsbEM2gNfvut/MPu+Zr5u4/z/lUzdSHAAIch7gb4jfLtOAz87PIcf5fHs/SLP7Wztitz9Uy1I8zbtoc4E5KpY2e3gFcbWH+uWm83wCiY11/PLPts9J7kzWYnPwgnmIBlE9YY64+B6KrALEgEzDrISrUF+3DG8xrQKQziTagDqFMLSP54LfGwdgVNRAQ3RoGtwIxmhpMlkzVY1IvRlIu0b0cCLw3zhyZKbm/3CKX7flwGxBgPXwUiLJH6pyLvS5bAjENXw/EyXVhCYSrAoEcxJFJeUqbciDYb8Iv2bJGxmlSdQ6WL3BXVs2gbycDRM8xR5JsDrgc5wnsBmhzQGVDpzk908ccfQzyBDaDM5wLiYIiNsHjxaVSCRfxTTu1l4C40FGJdCJphY2CeCg5gyJxL0QThGcFLmsEE0Pz51MTET9O4irqVSfEcVQVroeIbkmZhEYE8igQpynPhXLYFvXzksbNIJ5o79OIeJNX08kfTcoZpDzhODULOUzgUyCLZw6PY20OVdvAWpPOiLeaXaTwlCubwYXEuXDK2gjgS3i2DuwPcqUCCNgaWOPgjXUv4H6FvzaoMsfgFgwK5/QvnLsAwkdH8IDavS/WCLoOo56A6IR0iHkhDBNnlEmwRlAekUcRtZCUSx3Vg2gZEC5X9hjzkuxdDgSilBCngQQbTo6ZC0Ecq8xdUJUL1cRIahtxavL6YnjPe8LTuCZXPxybukQQY6NnlQLK58WfzWZHIGArnQcQGukkhBS35Cwci1LRA3/ns4E1QQAE02XUY61xT3IgEPOF54srVs6OW6EME6tMwjFiOM/g9fLKc7mkV6B0RZE24CW5BEKjlIhyxogrCeaDT5GACC5X20hABDFAc3QxgAeA8IY+CYIAMTq88PoSECREpM0Ksvq8eRX+Cg65NG6J0ltKRQ/5yy3y7Ngn/mUQq5TBUQLB3ahXstMuFZ0Pj6kpKY8ACABtLU65zIjAlNw5DZWkgYgwVOPU5A0QgUeHTBrgU4BU5qWRIjcUEmXXx+Yrf8PDhCFxLjCPyhKhnSFwZyPCGSA8zl8uEQjJpXFLNB5KzqAwJBgmqzGDcNQOTP98oeoBbkmcmjwmGeiMmKlJjgCIPE+eS4+R9gk3nFkYF2hJsDrHSMc6XgbBzMCw0iFIqTtZrGl+98wk7ybwIFibQ0qSe6MpMiTAESEdDmFsRDbD1QKhrA3wJTxzJbAmzfsxJhbbSuUG/I5AtBLKFeOWYLEuGBQo38ky6yMHGuWBwSH5M1UPF5TXcY6xW87cCqWDSutZeYRjxJg8/ciMDpNLOTRh5379t2z7vz9zFkl4ly7G9hY2gnDuqXrc3rJu0Z5ltJf/cLu3gXUVjo9tW2X+jkccwT3A1h76PVhDGxANiAZEA6IB0YBoQDQgGhANiAbEnwjEI3bJZ9hUlvG+rn2vJe/I14D4D4EI+nd+7pXzFJZJ1ce6roXhnI2SMueYUvaOnz33Zb2WB8HRWM4iFoHnqmqTcDzCkjURt9zFbnzreRnngpfRTfpWXTLNvNdnOhScjLl3xj1OxrJXV2LTpF6NWcYu8hRclREBL4CqYtS1MFxmuhutugf51UQdowAiOdHhtUZ8EryzGbkT6tFesCYizD7/lgQlyjJyXoZ36fVbWO2sPki0SzkZzqdWrHEylr26jE1T9GqeBZwDF24HwrlcC6PLgXC3AOFc0tHIgUg2JSBcFYjliBgMEHkZBS9DgXAlEJ2rcTLyqC91TsayV5exaYpeVd5DJ2/ssy9Php5lPJxYzgaDeF0LgxwoQXgMdNbz2esEsXfLWtB6DQ9iJLNkChr5n/jHmAchV13OmtCyoAbCE8Sk305eOB6pjEFfiE9Tkw9B3xF34HhI/RKSo8LJGBWIdU5GpVdHnTFWenXgN/TDqFnILgXizTvhOnhmXaifdV0Lg8NZjOBQeA8FC0+xJNg1an20Wm9iNIA/FxBzAkCELnIn4vvOGWsilsVnMCHFbxcwQVK7mIgzTxM5HUAIOuyzS8E39JKucDIiEOucjEqvRiBWepW4DRSkIGXpTBboTEBFAzFD5NpZ08KQ9VTpAfikcnIg2BXJ9VpGA3WA3CiM8i9yJ5LX17Im1NFv1UCiHMUgkVbGkn+RAzHomshkm1i//K1wMgwVYI2TUelVt9OrwlcYMZuBc5CyCPsHJBcKOqPz5ooWRg4E3PGey/EiWdJFHQ+p11mNi070oMTsMXEn9ErNWRNLINyUIreozEPOv3BjzssIGRAhrhFy0S04Gd0NnIxlr3Z7vTpAPyJoFhpFyuJ4Y8WNKPAzdnFZW9PC6IRmsgACakJjYi3EeiOjQRtigHBYIxIPAvpGyppI1x+mpl5hHVQDQwM/oQy5CSl5GXJzEoR+lt81LTkZ3vmwx8lY9mqaEVZ69RQVNuiOF5wDL50wsuKGY26D52bInfiqFgYt1iIHNPnIi6BymA8dEmsh1pt4EDQdzCWfHWhmZGTvcu4EzfOJNdGPyrgQHooTCuhFNTDA84hljPo7wvIyoEOChX9kjoj9HbHkZHiIjW1yMpa96hHTa6NXV38D6kRTV+Kob92N/I9QzVtLt/W7dD16S9hol9vlZdRb/F5Oxjt6tWZYcF+x3V7qn/iII3y0V9uzpq+wpD30Wy2jAdGA+H2NbkA0IBoQDYgGRAOiAdGA+HuB+C8BqNW93lF/nqLLfhvMfgPiMYC4Ks/APAXM/MHnxQOqIB83sTRMiUuuhtaNp/9nfm5bV784u011i7qFeUtC9Wu1zeXxFA9md/P2a60NllOiKVxiOTifJMiNi75itAo63MjSkAR1robWrU4bffIPIKz6hbiJKuoWtv2LcBo28suYDgVnW5jHhLFn9LgP5Vm/hp6pccGMGUteSuyBxHJI7ATvkoZ7zsuIEuUKhLsdiJQ/BwJ1q9MmByJTsmAnS03dYh8I+TL2BRC3j4jl647bQLgIRMaMGV3BKYk94AzPQMLTwLVCrhOHgPaJl8FHp/NdLA2JPrLC1Yh1K61ktqJgWST1i1GZFAoEoqyEoLoZeVyTMvILBR+4soqIZWfwlDkZFgj+qVqHHBePeCVajIt2LGvU9gWfyEh6JJF7Ooy3XpkSo7oMfXR8whNteRnQoBjuYGl4vB+9wtWIHAdAQy9+h9y5308ioMACKmBS4DKQF+nlVX3MIDauSRn5hYB4416x7Ax0c2KBpBAlOTsEf4dFtBgX7VjWqO0jUo+0L7ZKL0bpAWd5BnABSvgCE54jeaFdVNq43srS8C6qcFS4Glp3XEPG3LnPMR18upo0lsMQtTfIN4woMGVckzLyCzFOFIjEzkALEwuEyupj5IxERVCYtGdstBjV/FjEmhm1DWlS1iN2RARdrMEzkAVwugUI5i/cxNLwEhuiztXQutNivcqyGBUINFdXKx4RTvVCbFyTReSXMVwsEBKsJgPCkQrABC5IDYjBAJGixcCOZY1dbENnbzhMCzRF/MpyGBqUyedTk2UqmVgoN7I0JE7LClcj1p2AcCXLQs/1Y1S3SECQKz4kPoWNa7KI/DJy5B82PbEz1rgfOTtkvjHns4iSYiPCILAH2bGsMbVBLvIx8lIyIGIkQ+EZQK0OkUGIjzH1BS8Di3WnOhs3sTS8yFbVuRpatzK1MZwLloWc650yKdJdeE/03ksflTFsXJNF5JdAcU4ojWVn5NwPXaw1lowe59tmKnEZLSbasaxR20e9lNqQtwD7MgUs78Hs7evy6N4WVs+U+eu/rOssixVOR7xdzJkRt+pchA2rLTtE/4a1/L5W434bvvQRR7g55Sc84vCfZcsHW+ZradqzpgfZGhANiAZEA6IB0YBoQDQgGhANiAZEA+JLgXhEw9fM+xqz/wfi/TQgbtyip6jcPkV5wxRXr76ivaFdY7kOwXSZ8jhWFENcrc5QN0H4GYlrsaK58Ml9UbUvPu0vt09R3qg4820fVLU3tMMt1yHE1InH4UohhsXTvxA7gr+NZQgR5WcYrsX0tX2xYd99QDh3n/LGFhBr2hsKhOU62BGhPA4XbgXCVYFwkZ+RuBZd+Nq+2B4Rw9cpb+Ssipxzsaa9kaYmit6B636piDGfGQtFDGGXkKKGixFieo3DMnVlisTPUK7FoHSgr+qLdftYXOTrlDdyVkXOuVjT3khAzJBJnJWlIob4BJ1RxKAkvNy8SawUiYqCOCziRU4pDD9DuRaXEL6uL7btc2mu/QrljZxVUXIuXFV7wwKhcVaWihiRGxgVMXqHOCr99Y0jvRDgvYOOAkIu5CkMLUCZFD4B8QV9sWlfBsTnK2/krIqSc1HX3khAzHchGmdloYhhgRBZs0niqPAVR3FVOCaLNJTryVIkICKTQiVRvqQvtu1zuuh9jfJGzqrIORfr2huJwaFxVpaKGEoQNYoYHHKKJr9i6DtMTW7MUyR+RqrtS/ti0z4XlSi+RHkjZ1XknIs17Y2MwSFT11IRox8XUUqmDvFQLiPzKYT6yLFbmPYZ8hSJnxG5FsJ1/KK+2LTP3lW7r1LeWN4yphrLG7o9nYx9RYzlXm3L1Tt+nwrJjVtwX77tcC4e5xHHb+iLR97aQ78GRNsaEA2ILy21AfHuWtvWgGhAfF2pDYh319q2BsTnmLae+v2BYR61rQ2I/wkgViOZ6Ht1gRUzkDhxGByeHV01dVdwLjIywESKG/LE30dndrb1rqJZ4Nz661KLzZxZ1wUp0pPXD9aJ47a0bj1iSrH5xZe1FvYmKoxhf2xEMhHHTKCXY13JYcCBxILwLudcWCBYVFtf410AUYbOWAJRVzexmhxJ2oHTjt0uEF6AIDV+56MCubFuJ2JKxf51IKLTNPpfhL6QitiIZLIChCuAyFJXRwS5oIIKpYfKW4E3ANFVgHCfAAS/zZ2k4I11OxFTKvZvAZFauACin7YimZCfSoAgl/2CwyAB5mNqVchIOhdmjZj9JCeVQhAPtbAYmMmgZDLkhsyDvm5Le8TU0KBMyAOORB+PnCeNEIO0legtypuYYBVcRjQiYF0M4zEimspexJQU00W0O0bV49Az4Kw4DhhynhBnaD6m1oFHQj2FqtcimfSXQQIhUXSQJYcBChR9TA3YrM6FASJ00xDFQZTZ4LiUARoAXnNTfeoADbrHHYerCFog6hEOPnE51P0oujpl9BavygAoC6oitEbAOu3uThy0exFT0AIwPfBKveP+0Wgvc0uCvHwfhsTnGHXmAY/kPA5eF4qVSCbqog/+zD7ZksMABYrEglDYks6FIQOMKK0zk3Dg0uRNf9HqEK0Pp9wMmvB0D8wQZW68iSJIcDa6CXEjLBBWF6QXkQZdHqEqwn5wtk71DMS63YgpaAGYHpyWRsQVXd8jAAlzVhxzNhSIs9JvripAEXxcWEM1kkkCgqKDhCqHwehJKC/P6FwkINhp7RNVMbgUEUVjpKTc8xgQbgY0PUKaYb0yN968cCCcjW5CY1vXiFIXhFgTfCStER6voMO6SBEoylyNmCJ6HF6oAxoQJICnQUVyr+VAxBHhLBB+K5KJciUQHaTCYeDBZTgXPudcWFYGOh3GyrDjwWm0L4aY28PwODUFTFRstg57kFUE8MTl0G8KRBa9xeNIDBbI+wSEXhKFdXsRU9hqAULvGYcUiIqBkKlJZZZcjDikLQ7gGmxFMolcCY4OsuQwyHJzzVNbnQvDyriCs2CbyhRMjnyCGClYbr3TWChxsRamBt9eXBAthTkS89EzFkNJwTYhbahEb5EjulhDVYSZIVecy4HYjZjCLWDi2CSMD8ctksXay/KOox33G095SkW9UO/LFHV/JJPltpZ6n5Xx9VvYPbJt217ElI9tu9yPv+cRR/ggELeU+IVbe9b0RxrXgGhANCAaEA2IBkQDogHRgGhANCAaEJ9l5lbejzU/z/119XxeGQ2IPxAIXzdik+GQbX01bzd14j/Wp57klYrn4A3mM8Glv/kznYDSVbacnDmH+R9vR+moXz/p77PkeOZvzz+R8scrHzysWX7Mvj8XxyIQBz1IH4djVsRh5TNL+ZxnOT65Hz+fXp9ecgyyOCr6ZFb5F8HvA7HUOWZXosREB11Hyh71HPy43q8BoRISHdQzRlXUnvvigPYe5j1q6hOnfJLcT/ztxxFAHNEth6fCQkXzaHrvKHnTMQPES8JDu/dgPg47QDzlWB1nYA4HC2mijWRHXOJfLBkONwIR81aAmM+dTKCYLSBcCcQLeuowN24HiKM7KhDuRiDc/UCINTHb8RYgvs3pZvMOzHAhJ0dyU/TCkWD+AqtfCMNhyPkX4FnQTJE4FXAijV7VN5BXgSAf7llZFqOeUy821DosgyMxMsDz6BzLVYyOYZyb9PL8ejzOk87Lr18/flIjn1+fX6mp+IxAvLiX44GOPf/z7L7Nfx39faV0P+Z/dObbK3L94O9PNLHNnz9578frr1/fXpH/iXP+oKM/Had5lhLIGkqLT9pHHfjrolWpFtg82wYgzkQLkVexcbWBI+Hlu/IvoNmR+Bdw/gXDqVD/9ihcCeVuKE0FI0JDHug5cagTw6NgcCRGRlDmyAAgyPNHI+F44Ev9qCNi3ufJZz54ABDPR4Jsnolf6djx+5P7Pueh+YDHyIFHxPz9OxUzf9Led85Lx/6Pz/DUd0j5Kdd3vqjn9LyHepH2iY/SPuo46LQYUx05x+Eg43aGREbEfAVefeQUdMJe8PJd+RcuYzjMqQOzFHzyIIPp0DO7AqUh7+DTiGA1CpmacC4GkPIlgyMxMlRogcuJI+KgQMQREYGYF2kA8UQgzSm/zYvi80/piKNOLs8/6dx8nKCjFNhjIJ7nY/93+PHzCCBMfsqFzj1y+oPmpO7lzwOfkQnM3D7MC/P8/8iWH3SqmuGbS/Mj3t73kVPQCXshAeFdAiLxLzwzK3JXPiKyELsiCKsJ2jTZiIASx6jnlkAoZyMxMhIQdo04ChBpjUhAHA8JiCPtIzU68kWBQMfNx7lj5yOvenXPeV8JiKNTIJDfvXyXK5qg1y6lnPT/u3zn1BEIJ1MTpXnSMweCD0AcFYjAmhlpahL+gVcgwHAAWTI51j3Dp3FEoDuBqelyAdfBcDfSXZOyLEY9Z4GoMTiUm4Gpyd41vQCIw2EJBE8iCsQLretHOkZHjwd0D3f93HMHOs7TB08a855OTQwEH2MgOD/N/JTrqFOTw97xQNZ8ez3wJ6V+4TqepCYC30xNhzQieP8oJEZSjIicAmFQ0J091C+E4TDm/IsOqhPecCouWFhBMwwucTfS74gOXInpHM+dpwREzuBIjAzwPIhLYX9HvDhePl/1d4QuhLws/vODv0UgDrxU/0NrBS2e84RB6ebcT3T8lTuLFtN575ss4XNnHujv0ysBgfx0j8N1HrD8HniPcpI18xXPn5T6het4fpGaXLoZ4FpeAYTsv+DWcFMfY/tXZVj9TRF2uBt7v1aX7In13PfVczy4OzdbxnEj3fLc0w254pZ+797fYet5g3vURxx6O/k7gPjErT1rag/9GhANiM8vowHRgGhANCA+rZ7PK6MB0YBoQDQgPq2ezyvj3RXeXvmfQJl5VPtWbGpA/Ic2MY/APb3Qk112tPN2zMzejCRyzV5q99sSu/WtFm8EJ7TetS3njazVGKr2pZNhx75Q2/PLY2FP1rYGhNr0zPQReqT+BG6EU2ZEXs1aJJHOLYFQZQq/2kkp9EbnfASis5FWUsyTlY7K3wDski5HyPvJV4GI50e/Eu8lb39XlFYH4raLLymIGJu+zX3/jZwZAOIFQDytAuGcjSRSAJFxPLqN68Nnf9OI8HlT7gCizFsFolTZuAMI96lAmBGhNhEv4QnP6F/IE5yYEdCSoPgf65FEHGgAWqhyPKBM4fC6LulQSFyTi0woUhqYIizsAIeQQ6QVMEl65peoYoayOpIWBhQ3lDGCcvixfNTugO7GWfQ+kn16FK/ra/lRF0SsQ2QVOKyS/TH1BdFRLOOE3kantuRqIulsslx7KNr045V4CQmIg2FG8Dv2gUT21yOJgFGhQHRxupIYKo5fKg8D5xz4FXG8acwBVrw34wYOVY09JDFQBqjsQz3De9XSsIobyhjpxM/t+O1ynAVxwQvPJNnnfYxvwvlQPjQF4CWGi/aN1QjALlH7Y+qBzmmkiajOwUAM9iX9ZHlAxBVVEIFXP0oGPTH/LY2IxIxA/BOOdrAVSWS0Mg9KOugldAcP5RC1BYIusV40LBIQ5ytHD5JJjUYH+CSqmCERWcSTTd8SECECwYt3IP0X3rui2eClJPu8MEo0H8pHubAMQGpkFe06AKGp++v5+ia1R3WOEReVslygHQLuSghe2iCledtnBgisEYkZwY0WINYjidg1IsRl3E01IFjlgLbJa5CMNCJcDgSFIkP4Np8+k3qHs0CkEcEaOxS0ZTL0A+GlJPu8Bt0YZI3g8qHS4YVLAiBUzgFdx4E/Ymr6TEBAncNEeYlcFFlXpiAslhyIuGw9gWtwFCCOiRmB4QQg1iKJqBIH1AWUWqDVeF+MCF3auDRligjvFVMTN7YT0GVq8ikii/2nPBNijGg5ZmryoosRp4Nkn5f4JlCb8rEzT6zCBk6LADFQmsHHADnRmrnsAVOTkOwGqFqR/VzfqCJD0fJEsPPawmQT8wh+MI8AvyMSM4KXT0QfWY0k4vh3hOptaKFnWeSIgeFZh4JYGMTK0MW6Yw0LMEW6uFjT994pk4RjnkyqmIGILFDmSPwOVcfQciBSRfmgogE9DfBCLBCIb4JLVsuHSod34LSwrgqpagi7RO2PqS9Uy+h1OYc6h4dqn48sFwZbLL94+daplonps/LnRfX73o+SW/U2wg2/d4K7f8vzhLBm5549Ycfmz7a/+68ecWzzRd6/5Y2PK9EuEKU9Ycfmz7e/PWt6bJsaEA2IBkQDogHRgGhANCAaEA2IBkQD4qtt+r0BaD6/oXT2D5FEaUD8ti0+EXXMzpjoSSueajrZw/NO+kYvUf17peeQ/84OE34Pbkwlpe+GRjBuVC1P58LaefFvORN9JGZdpsyOBjlI7q100D4rkrPG1vO4rIGf4RZckZB977fbULXTlhTSuRQNBE4PdXQ42YuuGCNUjldtZ59FWPRrBoS6IKqPLcd1IKI2SIxmoof8DUB0kXdBjhdTn0+EA5wVBQTEDw9LrgZ8Hp2r1b2ILnM3ED4HwkXDOguEEyCceImFo1AAUX1InLgJwX8ACL2sXReTrQARXGVEVIAwdhggClszINw+EGtt6HaB6HIgenedThJRpQ7Eaf63BkTiMtD30WfcBKfS6GfmMIApcRLmQ1eJnmIZGswHidFMoAAi4uaTloVvqg5y0ogpzJtA+IXgtMaOp97IxxjhSbG20rkQLHPEi7b9su6S1yIS8Be0Bi9Eq42wrGR+wO6TRnihqemNWRakeD9XNuKdZl4vaI86/hIuZxYwobeiz6ytMc+9IvWjXAYCaCy4CUkigrgccJL66OZcRk/JGRp5NBPMFNArGNjjN0R/NdRBJC8YFx5dfR61RvjMIh+Dz7rJ2souzGCZI3QBddW6S16L+K0Hyj9EtQX5HnyF+UH9rfojEQiNrnMSKYgTD5mTuuvJPxtHxMllIyJyGYJUYrkJtFRqbBFumLAn1PGfaW8UDI08mgkYER5skggwND8k9kog7oe65ulCwYjQGjvpTOFjxBGRIqjwuZAzR+KIyOqGoojltaCdVDL/FVIqfQd/Y8n8wIhAH2Qjgrtmvu5P1Eiev+mv5U3ENAaIxGWwQKSoJE5ii8iIcAkILJ5Ge6NgaARno5kE6Sp7QzHoN8e1UHunyJGIa0SscUxAQEcBa0Sylc55iS4WmSMhjhdb94LXgnbyiJAp3UmbwN+Qqd4wP5xLEWQiECcVYqBiLk6uXym0vGsK9q4pcRnoOOlwWJd4inaCNUKbAyCq0VMiQ2N0NpoJ9niaGSzrImjKNDVFglpXAAHuhsJEdjpraxAgcuaIAlHUXfJaRE3nAs5KmpqUv7FkfmAkDwoELTojDaUJ+OF3BJgK2Eu/I3Sx7plzhN8Ricswf58wPeRAgLcwm8i1gcmAu6Jl9BTL0KD8KZoJ9jouSaOZBMR6idFZsEh6WWq9qIHEGgO4G6o20svviMg9GT2vgQVzRNgqZd0lrwXHnEeKtFgj7gziXebMD4xk9EF5/7dxr1nb6r9Kw+rxz972SlxnlmB6+YJf1t7W/gVNugcI9zBArFuIa/BrgfiNW3vW9ODNbEA0IBoQtzf/ESxsQDyIhQ2IB7GwAfEgFjYgHsTCBsSDWPiHAPFnXCV/gpUftLEBccvW3ZDiZhtD9diKaoXZIq8hBDyJ5Kf4GnN6ZfNrh0L8WuNEhKrJuZU4yJyRtTe3ljVu9IZ+O1/LzKaN425/jsueRApijGRlh6qpy+zd2tUwqvo3leR9t9bji8Od+ZvCiNj8K0SBUd6NtlwRdfXTB7/a6Ct2+7LGVamHrhpMxMnL6/WmZQFH7KjNelItDaGoPqwNqM3IKQsgoj/L3QzEAg6/hGcNCKdAiJWxeXTFLtpYA6LbBMJtAeH2gHA5EFlPWktzglTBoIgDKkZOkX/Oak6wY4jZDp3LgehF54KnGWUzaL6LKl6wygUcKaN2ruFrbDI2Onrpll0/yUrlXLBPLUAFROoWBQ2jvjHaGpDqOlVZIA4qJMrX6Ce8qnuSHKozsrDVJaZK3pPG0uCcqoLwmZJBAW6BZTao20/UNdJ+gICCBYL8W/qyuLzWDjrANIDhIJ5mepVcHIyjshkSXwP764yN0UM/w2hrRDUNYl1ABYRjTCUFjaS+kWrUI/7NV1kgmMtFu8CLCsF58uyb5+vbozZrK9gpourh8p407JAR74VrD7mSQaEjQie2nvkKfdISgKqEV7aDAkHMC/i/vevjMPWsqdFHQFXxAkDwmXh9Jr6GXK8rjI2O83oJMaLaGiibyxc6DPImBY2ovpFGhJMIL9c3X2WBYERcVRZCGSO096YCRlxbbmsXVT0sn0XVSaQXgk500BdxJYNC14g4sdHSN4kuRlKV8Mp2KNcIVsSYLBCUO44sZ4FAuTpjJ76Gq7EmImPDjQkIo60hZbusu4JV0BD1DbNGTKKNgW5dsECc9ovqc7g1IHJbVSQDQJie9LmlepsBIAoGhd41Jed/mpqsHoayHfSuyQJBgxKKFD7CG8zUFMzUlBbNxNfQ/Tpjw42XCyZAI+kQa0FG1iBiJQ4rrAL1DVMDHyG7iqkpskAQbQbTElKuAJHZ2kUxFlf0pLE0RLkUHCkZFLgKUnZdrJWpoHoYynZwQXkeIuQzQeeid2Bs6GINLoMoXhDvwp+FcaH3Gomvoft1xga1/cz6GkaEZVLWiWNuCB3xEtYt6XhAfcPUyEfIrtHXWCB8MTnoekhbLjLpTaMAQWofpa1n4aLkQKAf1NLzCL956qHq9vHfrMF99hbebeW9ahr5rfX+lkd8yff+wkcc4d1Wfq2axpf2ZHvW1IBoQPxpTWxANCAaEA2IBkQDogHRgGhANCB+k1mP34V/BdupAfHBrV87sfKgLGQ7eyoeeBYJV3pyqddCJFsuiOWI1GoOS0s2bU7Oz/PYjfouWxWI+Nbv6PGkeYT/EK/tJh3zUVWck6oJt8Av+zS2zJeKHpnd3QYQes4G5wjOBM8Y91Q8uioQXaX/fEY+SRyRmou+BELfyK6VbUvkNz1D0u9P9XclECP5yPHovk81hl4JCMElLQbto0xFJNnhfQlEdy8QtZ1g98Y9FQ8Bgg8nbkMViLLbor8sAdFtjAi/MyLMa8Tz5V5vqmFfjNNwEiBOqcb5uyrjRyDyi9U7Vw/oUx0RK9FJTJwQcnskjQrELQH3Q2KPuOu0LqdiVDzGxGlI3IbOaRSTU4yx4l2KRWKBsFwJvDrrCjWPK/MvzsK3oO+phdFFFUcElXpypQYI+CrWQvIIUtdhagLThd6+1lgRFggwXKjDEyulj1FVPPdcL7wSqBUI36MenYSYDcrZmP1NgzesDe/goPcecQfImY8AMdsqHiq50JlABzKMhaERJMYKebg0Fkk2IgxXQplQVs3jPL0x/0KdmvSd+CPevEivQMDa4AZXaoCAr2ItZP96ED+7Y9YGrxE1IPCNe8GoiITUl3gdfnDC5oDTd/CuHp2EuQseHINco0KBYHUNT+wFBWJXxWNMnAYbewWKFr1Ip6D/cWFQLBLLEcm5EnI+U/OAJ1z5Fp1Txz7KED6Jxr8gzQx3cqUGCHrBWkil0pL85hMRLo0IZabIiKDIMIWKSMf1elEk6cW6FAdGfNa16CTBcjYyjQpUgmghegWxI33cU/FQZYzAPvEUe0UMmYYFEKEYETlXwtluFi6KApHYFCqxwLWBT6JtjmtErgGiQET2xQgeVQGE78wacUprxBRcAkJFM9ykPBKK9RKn8KEEYhGdRBkXqiCRNCpwR4BoIRRFhCYtAWJHxcPGXLGxV7zEPUE0E4nBEmORWI6I5Up0cj5X8xAgvDYSITZsJBY7IrBSlBogCkQMsMFAUP20Vig9BwodUQjpondNwnBxlpXSRb6GRpeB5bRSZEAso5OwAJTHsknLTtKoAD8NykdEZuxYyYPH6o6KB5QxakAg7gmimUgMlhiLxHJELFeCF+uLKlkoZ2JkTmAny+F5Qsd7ZxdrKZEjiSUNjdQP4KsYIAKVClIYBWfrEfNlZL7KaKXB+km/wXplpZynPrYK0lmwnHUExf7q7d0HNr91stv8ZX1fvbfrXLyvPWu/rLvFr5b9dq9a5r/A8Pua93Eg7mjuhyz9yu13M0puBOKxtvbQ78EsbUA8iKUNiAextAHxIJY2IB7E0gbEg1jagHgQSxsQD2JpM/AvtO9TbWtAfJ1t84PH2556EDthX6VDNu+zZ1p98SlVr+Qta3WrkVXKnEt9jrBV00Zb3UbuvdKWmiTBWUIC8VJGX+h8zM+OQx7Coqv1h4Mvosa/8LV+5HjtNq/9rAsmeE3hy1rdIrJKWbJfAOFXgFhVFake3QfCV6/CskdyIIiXMsZ4994AUS24AkRVpeNzgXBLIOKxEojFxeAr+hzVrtzbPgiEWwFCY7fMUExDAQSesOc8hpM4VuR1XNGoECBUWyJyE84xlTIx4PqAX0Vea3Wq4VGLTaKMEC7P6WvANU6E8DM0z+ii+gccLkt9jpznQdykk7iIknLGkuvRZdwOulKTWglFjeljGUnPI/VWemnXKnCY2C2O4wo4Z5RLBIicxyD6Fer8C94rO8GyG+ARji/MhyEyMeAMVJZDegnc+TI2iTJGtBx2VQ76Xv6SE4GSlXcBOwZ5HX0ucVjqc+RRW9j/rK/2R+WMJdejS+oknNuqlYzSU1FzQPQ8Um/NXRvrSAocyTbyyb15etcatjMUCkTBY8iBiOwEy25QjQ2+YtUb66FvAY0ALHuxYcJqKFQ2mMWh5SQdDOJoLDkRwoDIBCuIAwJNjZo+x5LnAT0RJ0yVOteji55ssvPsbJ2I/9BLtJik59FLP2jr0Io+2pJsc57jqji4lOMaUeMxiH5FBELZCZZ/oRobXNAUR4RLLAqHMz4R0GqxSaLjPgIBE12FE5HnUSA4uknBvvCG+5HzPKAnYsfqkuuhI2LJ9VAgINCV6XlM6IfYOrRiUlsMM4SjMeF8igQ2VnkMTu9ANR4I2AnW7Z8iiyDVaVImhrIo+jGfmsBqWMQmGRKThIEYJDBShRORpia+KkctGXXS1FTqc+Q8D9bxUH7ukIAouR7KaBlgXbBqJRQ1xkxNUc8j9ZYyU7QO2JLrckQgvN5jhSWPAUyMTpebi4/sBBN/JGpsdJIqOGViqGpF79JyhcW0GpvkgrrB4DirZselxolIizWmByfxUVAnLdalPkfO8yAmhhebwFRZ4XoIo+WUxVBRQROJmDLleh6pt5St4eTGA7ZYXQ4CQsI/TZtKiBt09fU8d//w+egW7rLvvpI+3/532bb2W/s+IH7/9tcB8VsKeyAg/jjbGhANiAbEn9LQBkQDogHRgGhANCAaEA/e0P8pIFoX/fFbauz22z7vinn7abY96Bbqh8GoyPQnFjIGcVOughVV2KxxzEsI6xYVbI/Nza+1LDjnbuambNQVdlJnz1i9zROqFppjfhOIXH/CrwIR3AKIal2+apSPcg9JtSJne3hz3u8CUdIW/C4QXfEN3skyUosvQ36Yc5LPmyAi3vbZ5wDh7gCiGqVlH4iytJzt4U03fQCIcOvDY+9qChs3ALGwJgNiGVVEeRiJB1GPjJKA0GglkQtxEcWJCdUgAkniMGgpSAUOiOV1wJ0y+hgZBYwOYY3ohAi2x0nUM8APUd7JejSXvJ4lv6KP3I8QrpHlgV6BSoeoloiqBspMfXWeltoiYITAs39CdJqL7V/mhiyjipCn1jsb/WMtMooCAU2OITEmBiUScFNHRCBJHAZxHUoqYUEZXgccjCPUQNwgE6BljZAlcKkOop4Bfoh3if9RszmvBwIsOb8CfmrOE94iy2N2lbIL2XF9mJpgvcw5sa98RVsE9gEShENQOYHEMymiipyvrFDhvUvcg63IKCoE0l+95UIQnMKU0Nxvhl8hcUQ81YXoK6XKRmDo9H39TngViTUisVw8bO2c8kNgRb9qc1nPkl9hWhfekvc6QHWkvwYDhHJBLGfEV7RFOu5fhaSPPBXLMymiigRW2eATk+FBVCOjnMyIEF6ROuoZiMBlWCDSNOwRIIN88vAkL1Q2ciDAq5D6By8sEqT3El4nAaE+6Fo0l7yeJb9CU9oRMecaVbcmAcHaJJN2o48BQ5baIrAvjo3JBhdRqxZRRahKDGsbp6QaGSXqT5xYSMhwIeLU1GVAxCUCV+tFpiZf8jowNV0uakM+Nc3Oeb1SB8M6YX6I8E6GVZuLeur8CmnTDASifHGvDMovwd0PtEm0zNRXGsnFaot0cvkoN6TLuB24fBdRRTr+fdBl0T/WIqOo/kRgXobhQlxEcUJikiACSQ7EnM8jFXNAMl4HFmvRh4qL81kWNifWnSV9JxRPnoOnxP+o2lzUU+dXaJtGiSTGk+MF/BKqjxULQf+UMlNfIZJLri0C+1SJinvtYvv3NL1LQWJ5kxrc8lu6pXTp9nUlf01gcX0LH7O6ui1/WYdqe+6y0/32LdxU+ToQ921foWDxlwDx3sY221pj/yDbGhANiGZbA6IB0WxrjX1w2xoQDYj/Rdv+H0NK3eFsyc86AAAAAElFTkSuQmCC","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":7760,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"}],"totalChars":174753,"truncatedChars":0,"text":"# User ¶█Complete the assignment below, thoroughly:█# Target█Broker PTY control/attach/detach/reattach implementation and tests. Non-goal: daemon update mechanics except their interaction boundary.█# Change█Trace controller and viewer stream ownership from `spt rc` attach through broker session tasks. Explain what state produces a frozen terminal that recovers only after intentional detach and a new `spt rc`. Search for cancellation, half-open stream, connection epoch, displaced controller, and broker client reconnection handling.█# Acceptance█Return exact symbols, invariants, existing hazards/tests, and 3-5 falsifiable hypotheses with a safe reproducible harness proposal. No writes or runtime mutation.█# Assistant ¶█_**Planning skill diagnosis and context reading**_█# Tool call ¶█//Loading diagnosis workflow█read(path=\"skill://diagnose\", selector=null)█<out>█\u000e---█name: diagnose█description: 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.█---█# Diagnose█A discipline for hard bugs. Skip phases only when explicitly justified.█When 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.█## Phase 1 - Build a feedback loop█**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.█Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**█### Ways to construct one - try them in roughly this order█1. **Failing test** at whatever seam reaches the bug - unit, integration, e2e.█2. **Curl / HTTP script** against a running dev server [...5116ch elided...] # Phase 6 - Cleanup + post-mortem█Required before declaring done:█- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)█- [ ] Regression test passes (or absence of seam is documented)█- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)█- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)█- [ ] The hypothesis that turned out correct is stated in the commit / PR message - so the next debugger learns█**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.█\u000f█</out>█# Tool call ¶█//Mapping broker PTY files█glob(path=\"**/*{broker,pty,session,attach,control}*\", hidden=true, gitignore=true, limit=300)█<out>█\u000e# crates/█## spt/tests/█resume_no_control_steal_e2e.rs█attach_wedge_e2e.rs█run_no_dup_session_e2e.rs█## spt-daemon/█### src/█broker.rs█attach.rs█### tests/█inject_control_wedge.rs█netbroker.rs█broker.rs█attach.rs█control_stamp_lifetime.rs█## spt-store/src/█sessions.rs█attachment.rs█## spt-term/src/█pty.rs█## spt-net/src/net/█attach.rs█# docs/█## adr/█0027-unbound-endpoint-state-and-attach-on-session.md█0009-endpoint-access-control-model.md█0008-live-activity-buffer-pty-digest.md█0019-digest-adapter-declared-extractor-session-spanning.md█0018-broker-brain-process-isolation-restoration.md█0004-single-daemon-broker-brain-split-and-self-update.md█## spikes/█SPIKE-04-forkpty-parity.md█SPIKE-01-broker-handoff.md\u000f█</out>█# Tool call ¶█//Locating required domain docs█glob(path=\"{CONTEXT.md,docs/KNOWN-HAZARDS.md,docs/DEBUG-ROLLOUT.md,docs/adr/**,docs/ADR/**}\", hidden=true, gitignore=true, limit=200)█<out>█\u000eCONTEXT.md█# docs/█KNOWN-HAZARDS.md█DEBUG-ROLLOUT.md█## adr/█0037-materialized-project-index.md█0036-private-fork-distribution.md█0005-totp-seeded-spake2-node-pairing.md█0031-server-side-screen-grid-render-repaint.md█0034-per-minter-op-id-namespacing.md█0033-wake-resume-via-reconcile-intent.md█0025-live-daemon-coordinated-adapter-update.md█0022-spt-hosted-idle-delivery-translation-binary.md█0032-boundary-rotation-credential.md█0030-robust-wan-subnet-join.md█0029-adapter-update-arc-resolve-not-execute.md█0028-activity-gated-delivery-and-send-modifier-axes.md█0027-unbound-endpoint-state-and-attach-on-session.md█0026-multi-subnet-home-at-endpoint-run-creation.md█0024-multi-platform-adapter-spt-packaging.md█0023-always-on-endpoints-resident-supervised-binary-sigil-addressing.md█0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md█0020-event-envelope-sole-arriving-format-reply-to-removed.md█0012-psyche-trust-boundary-stdout-event-outbound.md█0009-endpoint-access-control-model.md█0008-live-activity-buffer-pty-digest.md█0019-digest-adapter-declared-extractor-session-spanning.md█0018-broker-brain-process-isolation-restoration.md█0016-platform-targeted-update-sets.md█0017-subnet-mesh-membership-seed-proof.md█0014-publish-target-repo-and-lapse-proof-canonical-url.md█0015-release-signing-two-key-trust-anchor.md█0013-psyche-reconciled-context-merge.md█0010-immutable-home-subnet-fork-not-rehome.md█0011-branchstore-git-kv-state-substrate.md█0006-multi-subnet-membership-model.md█0007-subnet-notification-primitive.md█0003-multi-instance-node-anchored-identity-model.md█0004-single-daemon-broker-brain-split-and-self-update.md█0002-bake-networking-into-core-collapse-spt-node.md█0001-clean-room-fork-from-claude-skill-owl.md\u000f█</out>█# Tool call ¶█//Finding attach control symbols█grep(pattern=\"\\\\b(attach|detach|reattach|controller|viewer|displaced|epoch|half[-_ ]open|cancel|CancellationToken|reconnect)\\\\b\", path=\"src;crates;packages;tests;test\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## access.rs█41|//! message traffic.█*42|//! Control surfaces (terminal attach, file transfer) are never replies; their█43|//! serves always gate as [`InboundClass::Unsolicited`].█44|█45|use spt_store::access::{AccessStore, RecentOutbound, REPLY_WINDOW_MS};█## applyhost.rs█320| // brain-restart trigger rides after the swap (ADR-0018 D3-3); it does not█*321| // attach to any session (the broker holds every hosted endpoint across the█322| // brain cycle - that is the whole point).█323| let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;█324|█...█451|/// the planned brain-restart trigger (ADR-0018 D3-3) rides after the binary█*452|/// swap. It does NOT attach to a session: the broker holds every hosted endpoint█453|/// across the brain cycle, so apply never touches one. `Err` means the broker█454|/// never answered (a dead broker fails the apply with the seat untouched).█455|fn connect_retry(broker_name: &str) -> io::Result<Brain> {█## attach.rs█*1|//! Remote terminal attach (M4-D5b, REQ-INST-8) - serve a byte-stream viewport█2|//! onto a local session for an operator on another node.█3|//!█4|//! [...42308ch elided...] emit `{\"commit\":true}`. The broker's inject worker must hit its█*102| // commit deadline, flush the buffered controller input, release the█103| // floor, and FAULT this binary (after which idle inbound SPOOLS - raw█104| // inject was removed as a delivery path, ADR-0022 amendment).█105| //█...█143|/// truncation and many settle-chunks wide, yet it fits inside a 24-row screen grid so a█*144|/// cold-attach repaint (ADR-0031 = current screen) still carries the HEAD (a taller█145|/// payload scrolls HEADSTART off the visible grid).█146|fn big_multiline_payload() -> String {█147| let mut s = String::from(\"HEADSTART \");█Showing files 1-20 of 161+. Use skip=20 for the next page, or narrow paths/pattern.█Skipped missing paths: packages, test\u000f█</out>█# Assistant ¶█_**Planning targeted code and doc exploration**_█# Tool call ¶█//Reading project domain model█read(path=\"CONTEXT.md\", selector=\"1-280\")█<out>█\u000e# spt-core█**Platform scope:** Windows + Linux for v1. macOS is out (no test machine available) but kept structurally easy - `portable-pty` and Iroh both support it, so macOS is a later test/CI-budget decision, not a re-architecture.█**Legacy migration:** it should be possible - ideally *automatic* - for a user to migrate an existing `claude_skill_owl` (modern SPT) install to spt-core (identity, agents, tracked Psyche context). Exact mechanism deferred to design; the commitment is that migration is a first-class supported path, not a manual rebuild.█Harness-independent core for the SPT ecosystem. Provides inter-agent messaging, live-agent lifecycle, terminal wrapping, self-update, and networking primitives - as both a Rust library workspace and a canonical reference binary. Designed so any agent runtime (Claude Code, Codex, Cursor, headless, future harnesses) can interface with the SPT ecosystem either by shelling out to the binary or by linking the crates directly.█Successor to `claude_skill_owl` (today's \"modern SPT\"), which is being rebuilt as `spt-core` to untether the system from Claude Code and lift it to a general-purpose agent-ecosystem core.█## Language█**spt-core**: [...55832ch elided...] mmand that lists endpoints **grouped by locality, most-recently-used within each group**: `on-node / current-project -> on-node / other-project -> off-node`, mirroring the *resolution policy*'s local-first preference. Selection **chains conditionally**: a **running** instance -> attach/tap-in (no adapter step - already live under one); a **non-running** endpoint -> into the **adapter selector** (*adapter selection*: history head = default -> prior adapters -> \"choose a different adapter\") -> *home subnet* / other creation prompts as needed -> launch; a **\"+ new endpoint\"** entry -> the full creation flow. Off-node picks respect the reach + consent gates (remote-drive of your own running instance is ungated; a co...█[Showing lines 1-283 of 883. Use :284 to continue. Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading PTY known hazards█read(path=\"docs/KNOWN-HAZARDS.md\", selector=null)█<out>█\u000e# Known Hazards█Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** - the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.█**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon<->broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.█---█## 1. Race conditions & ordering█### 1.1 Phantom INIT_SIGNOFF after grace period█- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spoole [...51630ch elided...] structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.█- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.█### 6.2 Soft-cleanup preserves state, removes `ready`█- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.█- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.█- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.█- **Sister cite:** `src/owl/stop.rs`.█...█...█<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->█[Showing lines 1-300 of 736. Use :301 to continue. Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading debug rollout guidance█read(path=\"docs/DEBUG-ROLLOUT.md\", selector=null)█<out>█\u000e# Debug rollout runbook█<!-- [doc->REQ-UPD-6] -->█Debug rollout is the maintainer-only fast path for testing a local spt-core█build across a trusted lab subnet. It uses the normal signed self-update█substrate: no raw peer file-copy, no production `spt` CLI surface, and no█embedded debug trust anchor.█Use this when debugging needs a local build to reach multiple lab nodes quickly.█Do not use it for public releases; use `docs/RELEASE-RUNBOOK.md` for that.█## Mental model█- A rollout is a signed `SignedUpdateSet`: one signed metadata record, with one█artifact digest per Rust target triple.█- Each recipient verifies the set under its node-local `release-keys.json`,█selects only its own platform artifact, stages it, and then follows the normal█consent/apply policy.█- Debug and stable are separate channels. A debug-pinned node accepts only█`channel = \"debug\"` offers; a stable-pinned node accepts only stable offers.█- Debug versions are monotonic within the debug channel. To recover from a bad█debug build, publish a higher debug version, even if the bytes are a previous█known-good binary.█- A broker-touching candidate may be staged, but apply refuses it while█broker-hel [...2707ch elided...] t triple.█- `WrongChannel` means the node is not pinned to `debug`.█- `Rollback` means the offered debug version is not greater than the node's█current debug-channel version.█- `RefusedClass(BrokerBreaking)` means the candidate touches broker-held█resources; quiesce hosted endpoints first.█## Agent checklist█1. Read `CONTEXT.md` \"debug rollout\" and ADR-0016 before changing the flow.█2. Keep debug tooling in `xtask` or other maintainer tooling, not the public█`spt` CLI/help surface.█3. Keep verification on the production update substrate: signed metadata,█per-platform artifact digest, channel pinning, monotonic version, and█apply-time re-verification.█4. Add or update traceability evidence for `REQ-UPD-6`.█5. Run `cargo fmt`, focused update tests, and `traceable-reqs check`.█\u000f█</out>█# Tool call ¶█//Reading broker isolation ADR█read(path=\"docs/adr/0018-broker-brain-process-isolation-restoration.md\", selector=\"1-280\")█<out>█\u000e# Restore broker/brain process isolation (correct the in-process-collapse regression)█## Status█Accepted (2026-06-09) - **extends and amends ADR-0004.** The design was independently verified (agent `doyle`, verified-with-amendments); implementation is a dedicated future milestone, sequenced next (before `spt-claude-code`). Full rationale, the `file:line` audit, and the per-decision alternatives live in `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.█## Context█ADR-0004 decided the broker/brain split **as two processes** - a stable broker kernel (PTY masters, harness children, sockets, the Iroh/QUIC endpoint) beneath a freely-restartable brain - specifically to satisfy the no-endpoint-drop self-update invariant (REQ-UPD-3): a routine (brain-only) update restarts the brain while the broker process survives, so no hosted endpoint terminates. The spikes (01/03/04/05/06) proved this with two separate binaries.█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>` ser [...6220ch elided...] g:** 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.█- **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).█[Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading PTY digest ADR█read(path=\"docs/adr/0008-live-activity-buffer-pty-digest.md\", selector=\"1-260\")█<out>█\u000e# Live activity buffer (PTY digest)█## Status█accepted (2026-06-01) · **source mechanism superseded by the 2026-06-12 amendment below** (product surface stands) · **implementation reconciled onto the log source in M9 (2026-06-13)** - the PTY-parse engine + `pty_digest` seam are retired; the digest is an on-demand projection of normalized session-log records · **source mechanism FULLY superseded by [ADR-0019](0019-digest-adapter-declared-extractor-session-spanning.md) (2026-06-13)** - the M9 \"no manifest seam / rides `[history]`\" stance is reversed: the digest gets its own adapter-declared `[digest]` extractor, thread-spanning, and a two-origin (activity + injected-context) merge. Only this ADR's product-surface framing survives (broadened from \"recent activity\" to \"recent context\").█## Context█We want a rolling, human-glanceable \"what is this agent doing right now\" view - the last ~N user turns + agent output between, with tool-usage sprints collapsed to a single `used: ...` line - that a Shell, CLI, or GUI pane can render. The obvious implementation (spt-core parses the live PTY byte stream) looks like it **contradicts the history-subsystem decision** (CONTEXT §history subsystem [...6042ch elided...] discharged (M9, 2026-06-13):** the PTY-parse engine (`spt-term`'s `DigestParser` + `spt-daemon`'s byte-feed `DigestEngine`) and the `[pty_digest]` manifest seam are **retired**. The digest is now an **on-demand projection** of the endpoint's normalized session-log records: `spt-term::projection` folds the published **digest-record contract** (the spt-core-owned `{role, text, tool}` overlay an adapter targets in its `[history]` lines, or pushes via `api digest-entry`) into the same structured `Digest`; `spt-daemon::digest::project_endpoint_digest` projects on demand at the snapshot-pull / reproject handlers and `DigestHub::project_and_publish` drives the delta-stream. Topology-independent (a Claude-Code-hosted endpoint's Path A JSONL projects with no b...█[Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading screen repaint ADR█read(path=\"docs/adr/0031-server-side-screen-grid-render-repaint.md\", selector=\"1-260\")█<out>█\u000e# Server-side screen grid + clean repaint on attach█## Status█accepted (2026-06-30) · companion to `V0.19.0-P6-SCREEN-GRID-DESIGN.md` · relates to [ADR-0008](0008-live-activity-buffer-pty-digest.md) (render-vs-content split) and [ADR-0001](0001-clean-room-fork-from-claude-skill-owl.md) (clean-room provenance).█## Context█A cold attach streams the entire retained **raw-byte ring** as its initial batch (`become_controller`, broker.rs:402-416). For an alt-screen TUI (Claude Code) that ring is a mid-stream rendering protocol; replayed raw into a fresh terminal it corrupts the screen + scrollback (field bugs #6/#7/#8). We need a server-side render model that interprets the byte stream into an authoritative current screen and synthesizes one clean repaint at attach.█Two questions this ADR settles:█1. Does a server-side PTY-byte interpreter **contradict ADR-0008**, which retired the PTY-byte parser?█2. May we take a **third-party VT parser** dependency under ADR-0001 (clean-room everything except the sister project's stable wire/schema formats)?█## Decision█<!-- [doc->REQ-BROKER-SCREEN-GRID] the server-side render grid + clean repaint on attach:█a clean-room `ScreenGrid` (vte: [...2699ch elided...] tStream`.█- `become_controller`/`add_viewer` emit a synthesized clean repaint as the initial batch; live frames after attach are unchanged raw bytes. Cross-node attach (#4) and any client are agnostic.█- Must preserve the attach/serve hazard invariants (REQ-HAZARD-INJECT-CONTROL-COEXIST 7.12, CONTROLLER-WRITER-REORDER 7.21, VIEWER-ISOLATION 7.7 + skip-to-live, the wedge family) - the grid is on the output path only, touches neither the input/effect journal nor the QUIC deadlines.█- Unlocks the follow-up sticky ID-marker redo (#8/#14 marker-half) as a grid-backed per-frame overlay (separate REQ).█- Composes with #12 (REQ-RC-WIN-VT-OUTPUT): the repaint is ANSI bytes; on Win10 conhost it renders only once VT-output-processing is enabled client-side (#12).█[Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Locating rc command implementation█grep(pattern=\"\\\\b(RemoteControl|remote_control|run_rc|cmd_rc|rc_attach|AttachIntent|--take|--view|Detach)\\\\b\", path=\"crates/spt/src;crates/spt/tests\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt/█## src/█### cli.rs█184| /// the degenerate single-node case of the cross-node attach (one pump,█*185| /// loopback peer). Detach with the **ctrl-b** prefix then `d`█186| /// (`ctrl-b ctrl-b` sends a literal ctrl-b); detaching leaves the session█187| /// running on the broker. `--view` watches read-only.█188| Rc {█...█1128| /// within one pump cadence; `spt subnet attach` reverses it.█*1129| Detach {█1130| /// The held subnet to stop serving.█1131| name: String,█1132| /// Also persist as the startup default (survives daemon restarts).█...█1330| }) => cmd_subnet_join(name, code, verbose),█*1331| Some(SubnetCmd::Detach { name, save }) => cmd_subnet_attachment(&name, true, save),█1332| Some(SubnetCmd::Attach { name, save }) => cmd_subnet_attachment(&name, false, save),█1333| Some(SubnetCmd::Leave { name }) => cmd_subnet_leave(&name),█1334| Some(SubnetCmd::Prune { node }) => cmd_subnet_prune(&node),█...█1714|/// makes `--view`/`--take` mutually exclusive, so at most one is set.█*1715|fn rc_intent(view: bool, take: bool) -> spt_net::net::attach: [...8193ch elided...] ncumbent is also local (`None == None`) - viewer-fallback fires█...█48|//!█*49|//! `resume_sessions` (still used by DRAINING callers) re-attaches as **`AttachIntent::Viewer`**█50|//! (a viewer never owns `driven_by` and is never stall-evicted). The RED->GREEN discriminator█51|//! this rig asserts is█52|//! the STEAL itself - the deterministic ROOT that CAUSES both A and B - observed as OUTPUT█...█483| // swap - each A_i went on receiving output after the respawn. RED (resume re-attaches█*484| // AttachIntent::Control): resolve_subscribe same_identity(None==None) become_controller'd█485| // each slot, dropping A_i -> its tally froze -> gained==0. GREEN (Viewer): A_i untouched.█486| // [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]█487| for (i, g) in gained.iter().enumerate() {\u000f█</out>█# Tool call ¶█//Indexing broker ownership symbols█grep(pattern=\"^(pub\\\\(crate\\\\) |pub )?(struct|enum|fn|impl) |become_controller|stall_evict_controller|reap_dead_controller|detach_conn|controller_writer|viewer_writer|SubscribeOutcome|controller_epoch\", path=\"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/conn.rs;crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/attach.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## broker.rs█59| NetStreamsReply, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,█*60| Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,█61| KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,█62| KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,█63| KIND_NET_SENT,█...█108|// [impl->REQ-HAZARD-VIEWER-ISOLATION]█*109|fn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {█110| r.is_err()█111|}█112|█...█117|/// dropped (the `tx` closes -> the writer ends) and removed from the map.█*118|struct ViewerSink {█119| /// The bounded live-frame queue to this viewer's writer thread.█120| tx: SyncSender<Envelope>,█121| /// The viewer's IPC send half - kept ONLY for identity matching on detach█...█180|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]█*181|fn brain_write_deadline() -> Duration {█182| match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {█183| Ok(ms) => ms█184| .parse::<u64>()█...█205|// [...16454ch elided...] } => {█*430| use crate::msg::SubscribeOutcome as O;█431| match outcome {█432| O::Controller | O::TookControl => {█433| role = Some(ServeRole::Controller);█...█552|// [impl->REQ-INST-8]█*553|pub fn request_attach(█554| brain: &mut Brain,█555| conn_id: u64,█556| session_id: u64,█...█571|// [impl->REQ-RC-CROSS-NODE-ATTACH]█*572|pub fn request_attach_endpoint(█573| brain: &mut Brain,█574| conn_id: u64,█575| session_id: u64,█...█597|// [impl->REQ-RCVIEW-1]█*598|pub fn send_attach_resize(█599| brain: &mut Brain,█600| stream_id: u64,█601| rows: u16,█...█613|// [impl->REQ-INST-8]█*614|pub fn send_attach_input(█615| brain: &mut Brain,█616| stream_id: u64,█617| bytes: &[u8],\u000f█</out>█# Assistant ¶█_**Planning targeted document grep and review**_█# Tool call ¶█//Finding PTY hazard sections█grep(pattern=\"^### .*?(attach|controller|viewer|brain|broker|wedge|half-open|reconnect|re-attach|stream|detach)\", path=\"docs/KNOWN-HAZARDS.md\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e 22|█*23|### 1.3 Stale `index.lock` wedge from prior git crash█24|- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.█25|- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.█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.█...█28|█*29|### 1.4 Deferred spool rows must not leak to the event stream█30|- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred -> event emitted at wrong time/priority.█31|- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.█32|- **spt-core mapping:** carries directly - the daemon's spool-drain has the same deferred-vs-immediate distinction.█...█206|<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] - [...32796ch elided...] Windows, `interprocess 2.4.2` routes the send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` - this local-socket send path exposes **no supported write timeout** (`set_timeout` -> `Unsupported`). An `rc --take` controller consumer that stopped reading blocked the write **indefi...█641|- **Invariant:** every write on a physical broker connection rides ONE broker-owned bounded/cancelable framed-write primitive with an **independent out-of-band aborter**. The `SendHalf` never leaves the conn object (a bypass cannot compile). An ABSOLUTE deadline stamped at write entry covers BOTH the serialized gate-wait AND OS write completion; a per-conn watchdog fires at that deadline and aborts the in-flight op - never relying on the write returning or on opportunistic stall-evict sampling. On dead...\u000f█</out>█# Tool call ¶█//Finding context terminal rules█grep(pattern=\"^(##|###).*?(terminal|remote control|self-update|broker)|\\\\b(controller|viewer|attach|detach|reattach|PTY ownership|broker owns|connection epoch)\\\\b\", path=\"CONTEXT.md\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e 38|**in-session relay**:█*39|A thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree - today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay - the daemon owns the PTY and consumes the same poll feed itself. Idle de...█40|█41|### Deliverable shape█42|█...█152|█*153|**message metadata (`json`)** - a sender may attach an opaque JSON metadata block (`--json-payload`), carried as a single attr-escaped `json=\"...\"` envelope attribute **alongside** (never replacing) the body. spt-core never interprets it - pure verbatim passthrough across every rail (spool / TCP / WAN / EVENT-PART), parsed only by the receiving adapter (its hooks and/or translation binary). Collision-proof by construction: the structured data lives **inside** the single `json` value, so it can never ...█154|█155|**resume-session seam** - two distinct forms:█156|- **fresh-with-preload:** resume with *cl [...17319ch elided...] filtered selection) and \"management GUI\" (per-endpoint panes: latest session output, live/project context, psyche log) sketches. `attach` is one operation under this frontend, not the whole frontend.█880|█881|**guided-resume picker (`spt resume`, no-arg)**:█*882|The CLI sibling of the frontend's guided resume - one command that lists endpoints **grouped by locality, most-recently-used within each group**: `on-node / current-project -> on-node / other-project -> off-node`, mirroring the *resolution policy*'s local-first preference. Selection **chains conditionally**: a **running** instance -> attach/tap-in (no adapter step - already live under one); a **non-running** endpoint -> into the **adapter selector** (*adapter selection*: history head = default ...█[Some lines truncated to 512 chars]\u000f█</out>█# Tool call ¶█//Reading rc attach loop█read(path=\"crates/spt/src/rc.rs\", selector=\"1080-1760\")█<out>█\u000e/// takes control. Returns when the session exits, the operator detaches, or an█/// error occurs.█// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]█pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {█run_attach_inner(endpoint_id, intent, false)█}█/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker█/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via█/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a█/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2█/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND█/// stamp), yet the confirmed live session IS attachable - \"attach to a live█/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).█/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a█/// session that dies after confirmation, so this is not a blank-hang reopening.█// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]█pub fn run_attach_session_confirmed(█endpoint_id: &str,█intent: AttachIntent,█) -> Result<(), String> {█run_attach_inner(endpoint_id, intent, true)█}█fn [...31827ch elided...] he broker-EOF-on-dead-child path█/// and the status-gate catch the ordinary dead cases first).█Stalled,█/// The serve side finished the stream having produced NO output - i.e. it█/// refused the attach (no live session for the endpoint, or the access gate█/// denied us). On the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH) this is█/// the D6 stale-row case: the registry routed us to a node that no longer has█/// the session. Distinct from [`Detached`](PumpEnd::Detached) (the OPERATOR█/// ended a live viewport) - here the REMOTE closed on us before anything ran,█/// so it renders an honest \"no live session\" instead of a false \"detached\".█// [impl->REQ-RC-CROSS-NODE-ATTACH]█NoLiveSession,█}█[Showing lines 1079-1763 of 3214. Use :1764 to continue]\u000f█</out>█# Tool call ¶█//Reading rc pump termination█read(path=\"crates/spt/src/rc.rs\", selector=\"1760-1945\")█<out>█\u000eenum PumpEnd {█...█/// so it renders an honest \"no live session\" instead of a false \"detached\".█// [impl->REQ-RC-CROSS-NODE-ATTACH]█NoLiveSession,█}█/// Generous first-event backstop for the attach pump (REQ-HAZARD-RC-ATTACH-FAILFAST█/// path b): if NO broker event arrives within this window after attach, the█/// session is treated as dead/wedged. Long on purpose - a fresh, mid-init alive█/// session (empty OutputLog, still painting) MUST pass, so this only catches a█/// true hang, never the working fresh-attach.█const FIRST_EVENT_GRACE: Duration = Duration::from_secs(30);█/// The path-b stall decision (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach has█/// stalled iff NO broker event has ever arrived AND the generous grace has█/// elapsed. Pulled out so the decision is unit-testable without a 30s pump run.█// [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]█fn first_event_stalled(seen_any: bool, elapsed: Duration) -> bool {█!seen_any && elapsed >= FIRST_EVENT_GRACE█}█/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).█#[derive(Debug, Clone, Copy, PartialEq, Eq)]█enum ReadDisposition {█/// Transient poll-slice timeout - loop and poll again.█Retry,█[...7954ch elided...] } => {█if seq < cursor {█continue; // already rendered - re-serve dedup█}█cursor = seq + 1;█rendered_any = true;█let bytes = decode_bytes(&data_b64)█.map_err(|e| format!(\"decode output: {e}\"))?;█// Track the harness's mouse-reporting mode from its output█// (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.█mouse_scanner.feed(mouse_mode, &bytes);█...█}█...█}█}█}█...█}█}█}█[Showing lines 1759-1948 of 3214. Use :1949 to continue]\u000f█</out>█# Tool call ¶█//Reading attach serve loop█read(path=\"crates/spt-daemon/src/attach.rs\", selector=\"136-550\")█<out>█\u000e█/// Read one [`BrokerEvent`] for an attached CONTROLLER, RESUMING-FROM-FLOOR on a█/// b4 drop-don't-block output-gap instead of fataling.█///█/// b4 made the controller a non-blocking `try_send` that DROPS frames when its█/// bounded channel is full (a controller that falls behind its own echo under a█/// hard flood). The next frame this reader sees is then a FORWARD gap that the█/// strict reject-gap (`read_event`, legacy single-session path) FATALS. A█/// controller CANNOT snap-above like a viewer (B2: it is authoritative - it█/// advances `delivered_through`; skipping rolled frames would be a not-exactly-once█/// resume). So instead it RE-FETCHES: re-subscribe from█/// [`Brain::controller_resume_floor`] (`= delivered_through`, frozen at the drop by█/// `contiguous_advance`) and the broker replays the dropped frames from the ring.█///█/// Exactly-once holds IFF the ring still retains the floor (`tail - delivered_through█/// <= cap_chunks`). If it does not - the controller fell behind further than the█/// ring holds - the re-subscribe's first frame is STILL a forward gap (floor█/// unchanged across two resumes). That is [`ControllerIrrecoverablyBehind`]: the█/// caller surfaces i [...22106ch elided...] // Applied/NetSent acks and unrelated events: not ours to handle.█_ => {}█}█}█}█/// Operator-side: open an attach stream on a dialed connection and send the█/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes█/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes█/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the█/// durable id for the exactly-once stream open (safe to block for its ack -█/// no attach traffic can be in flight before the stream exists); the Request█/// send itself is unjournaled (see module docs - a replayed Request re-gates█/// and re-attaches idempotently).█// [impl->REQ-INST-8]█pub fn request_attach(█...█) -> io::Result<u64> {█...█}█[Showing lines 135-553 of 627. Use :554 to continue]\u000f█</out>█# Tool call ¶█//Reading broker sink ownership█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"80-205,960-1420\")█<out>█\u000e/// `Arc<Mutex<SendHalf>>` - [`BrokerConn`] owns the half and every write is█/// bounded + cancelable + poison-on-failure (see [`crate::conn`]). Identity█/// matching stays `Arc::ptr_eq`.█pub(crate) type SharedSend = Arc<BrokerConn>;█/// Default bounded output-log depth (chunks). Large enough that a brain restart█/// window never evicts unreplayed output in practice; the bound is what keeps a█/// disconnected session from buffering without limit.█const DEFAULT_LOG_CHUNKS: usize = 4096;█/// The viewer-channel depth (frames). A bounded SPSC queue per viewer; when it█/// OVERFLOWS the viewer is evicted - the drain `try_send`s and never blocks█/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render█/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely█/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread█/// owns the replay batch), so a viewer attaching to a busy session is not█/// spuriously evicted.█const VIEWER_CHANNEL_DEPTH: usize = 256;█/// Soft cap on concurrent viewers per session (REQ-HAZARD-VIEWER-ISOLATION:█/// bounds the writer-thread count). A `Viewer` attach beyond the cap is refused.█const MAX_VI [...28402ch elided...] ,█session_id,█reason: \"write-failed\",█};█for frame in &initial {█// Bounded, cancelable conn write (round-4): a viewer that stops█// reading poisons + retires ITS conn at the deadline instead of█// parking this thread in the OS write forever.█// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]█if !matches!(send.write(frame), Ok(ConnWrite::Done)) {█return;█}█}█while let Ok(frame) = rx.recv() {█if !matches!(send.write(&frame), Ok(ConnWrite::Done)) {█return;█}█}█note.reason = \"channel-closed\";█// `tx` dropped. If this was an EVICTION for overflow (append set the flag█// before dropping the sink), emit a KIND_VIEWER_EVICTED marker - DISTINCT from█...█}\u000f█</out>█# Tool call ¶█//Reading brain subscribe lifecycle█read(path=\"crates/spt-daemon/src/brain.rs\", selector=\"1000-1085,1560-1685\")█<out>█\u000e pub fn attach_as_viewer_snap(█...█) -> io::Result<()> {█...█self.subscribe_with(session_id, from_seq, AttachIntent::Viewer, by.map(str::to_string))?;█self.session_cursors.insert(session_id, from_seq);█Ok(())█}█/// Cold-start multi-session resume (restoration D4-2, ADR-0018 Q6): query the█/// broker for **every** hosted session and re-attach **each** in resume mode█/// from the broker's per-session delivered cursor (`resume_seq`). This is the█/// production replacement for the retired single-session `BrainState` handoff█/// frame - a brain the supervisor respawns (crash *or* update) reconstructs█/// all session continuity by querying the persistent side, never a brain->brain█/// message. Returns the ids re-attached (empty when the broker hosts none -█/// the supervised daemon brain's no-op-today case).█///█/// Each session is seeded into [`Self::session_cursors`] at its `resume_seq`,█/// so [`Brain::read_event`] dedups the broker's at-least-once boundary re-send█/// and accepts a post-eviction ring-floor jump (no reject-gap). Output is█/// at-least-once; input/effects stay exactly-once via the broker [...10532ch elided...] , bounded by an optional wall-clock█/// `deadline`. The carrier dispatch (REQ-HAZARD-PUMP-IPC-DEADLINE):█///█/// - [`BrainConn::Whole`] - the ordinary blocking client: a direct█/// [`read_frame`]; `deadline` is irrelevant (a whole brain never sets█/// `io_timeout`, so callers always pass `None` here).█/// - [`BrainConn::Split`] - the pump carrier: the reader thread already owns█/// the blocking read, so a bounded read is just [`Receiver::recv_timeout`]█/// on the remaining budget. A `Timeout` surfaces [`io::ErrorKind::TimedOut`]█/// - the poison signal the pump escalates to a supervised restart (NEVER a█/// per-peer retry). A `Disconnected` channel means the reader thread exited█/// (the conn hung up); surface it as `UnexpectedEof`.\u000f█</out>█# Tool call ¶█//Reading connection cancellation█read(path=\"crates/spt-daemon/src/conn.rs\", selector=\"1-180,219-680\")█<out>█\u000e//! Bounded, cancelable, poison-on-failure framed writes for one physical█//! broker connection (UPDATE-WEDGE round-4).█//!█//! ## The failure class this kills█//!█//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard█//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes█//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` - a local-socket█//! path with **no supported write timeout** (`set_timeout` -> `Unsupported`).█//! A connected consumer that stops reading (an `rc --take` whose process█//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the█//! logical stall-evict (`stall_evict_controller`) releases only the controller█//! ROLE - it never cancels the in-flight pipe write or closes the physical█//! connection, so the writer (and every write queued behind the same gate)█//! stays wedged until the peer process dies (~128 s in the field capture, on a█//! brain restart).█//!█//! ## The primitive█//!█//! [`BrokerConn`] owns the send half outright - it never leaves this module,█//! so an unbounded bypass write cannot compile. Every write:█//!█//! 1. acquires the serialized write GATE with a bounded wait (an [...25840ch elided...] ad(Ordering::Acquire)█),█);█{█let mut d = recover(&self.inner.dog);█d.shutdown = true;█self.inner.dog_cv.notify_all();█}█if let Some(dog) = self.dog.take() {█let _ = dog.join();█}█// Halves (gate or graveyard) drop AFTER the watchdog is gone - no raw█// handle use can race the CloseHandle/limbo path.█}█}█fn poisoned_err() -> io::Error {█io::Error::new(█io::ErrorKind::BrokenPipe,█\"broker conn poisoned (bounded write retired the physical connection)\",█)█}█fn timeout_err() -> io::Error {█io::Error::new(█io::ErrorKind::TimedOut,█\"broker conn write exceeded its bound (conn poisoned + aborted)\",█)█}█#[cfg(test)]█mod tests {█...█}\u000f█</out>█# Assistant ¶█_**Identifying missing broker functions**_█# Tool call ¶█//Locating broker ownership bodies█grep(pattern=\"fn (append|become_controller|controller_writer|handle_conn|dispatch_subscribe|dispatch_unsubscribe|run_drain|detach_if|all_sinks|converge_perch_stamps)|KIND_UNSUBSCRIBE|detach_if\\\\(\", path=\"crates/spt-daemon/src/broker.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 68| KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,█*69| KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,█70|};█71|use crate::nethost::{NetHost, NET_EFFECT_SESSION};█72|use crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};█...█647|// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]█*648|fn converge_perch_stamps(█649| endpoint: &str,█650| want_driven_by: Option<&str>,█651| want_controlled: bool,█...█794|█*795| fn append(&mut self, bytes: &[u8]) -> Option<u64> {█796| let seq = self.next_seq;█797| self.next_seq += 1;█798| self.ring.push_back((seq, bytes.to_vec()));█...█890| /// controller's identity.█*891| fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {█892| // Drop the prior controller sink first (its writer's live loop ends when█893| // tx drops), and bump the generation so (a) an in-flight deadline-evict█894| // for the old controller can't unseat this one and (b) a prior writer█...█1258| /// `driven_by` / `viewer_count` when a r [...1876ch elided...] roller already took over, or the session is gone.█*3487| fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {█3488| let req: UnsubscribeReq = serde_json::from_value(env.payload)█3489| .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;█3490| let log = {█...█3499| };█*3500| recover_log(&log).detach_if(send);█3501| Ok(())█3502| }█3503|█...█4726| #[test]█*4727| fn append_overflow_flags_the_evicted_viewer_for_skip_to_live() {█4728| let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));█4729| // A viewer whose live queue is depth-1 and which we NEVER drain (rx held,█4730| // unread) - it cannot keep up with the drain. We do NOT use `add_viewer`\u000f█</out>█# Tool call ¶█//Reading complete pump outcomes█read(path=\"crates/spt/src/rc.rs\", selector=\"1938-2025\")█<out>█\u000efn pump(█...█) -> Result<PumpEnd, String> {█...█loop {█...█match ev {█BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {█for rec in decoder.push(&bytes) {█match rec {█AttachRecord::Output { seq, data_b64 } => {█if seq < cursor {█continue; // already rendered - re-serve dedup█}█cursor = seq + 1;█rendered_any = true;█let bytes = decode_bytes(&data_b64)█.map_err(|e| format!(\"decode output: {e}\"))?;█// Track the harness's mouse-reporting mode from its output█// (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.█mouse_scanner.feed(mouse_mode, &bytes);█stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;█// Re-assert the reserved row if the harness output just█// destroyed our scroll region - alt-screen e [...3183ch elided...] d-output, preserves the prior bits, and is idempotent.█#[cfg(windows)]█#[test]█fn with_vt_output_enables_virtual_terminal_processing() {█use windows_sys::Win32::System::Console::{█ENABLE_PROCESSED_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WRAP_AT_EOL_OUTPUT,█};█let prior = ENABLE_WRAP_AT_EOL_OUTPUT;█let got = with_vt_output(prior);█assert!(█got & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0,█\"VT-output processing enabled\"█);█assert!(got & ENABLE_PROCESSED_OUTPUT != 0, \"processed output enabled\");█assert!(█got & ENABLE_WRAP_AT_EOL_OUTPUT != 0,█\"prior console bits preserved\"█);█...█}█...█}█[Showing lines 1937-2028 of 3214. Use :2029 to continue]\u000f█</out>█# Tool call ¶█//Reading attach wire helpers█read(path=\"crates/spt-daemon/src/attach.rs\", selector=\"552-627\")█<out>█\u000e/// and re-attaches idempotently).█// [impl->REQ-INST-8]█pub fn request_attach(█brain: &mut Brain,█conn_id: u64,█session_id: u64,█from_seq: u64,█open_op: MintedOp,█intent: AttachIntent,█) -> io::Result<u64> {█request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)█}█/// Like [`request_attach`], but for the cross-node leg (#4,█/// REQ-RC-CROSS-NODE-ATTACH): the remote operator cannot know the owning node's█/// local session id, so it passes `endpoint_id = Some(endpoint)` (and a█/// placeholder `session_id` of `0` the owning node ignores). The owning node█/// resolves the endpoint to its OWN session id server-side█/// ([`resolve_local_session`]) - authoritative for its own table. `None` here is█/// exactly [`request_attach`]: the local path's wire bytes stay byte-identical.█// [impl->REQ-RC-CROSS-NODE-ATTACH]█pub fn request_attach_endpoint(█brain: &mut Brain,█conn_id: u64,█session_id: u64,█from_seq: u64,█open_op: MintedOp,█intent: AttachIntent,█endpoint_id: Option<&str>,█) -> io::Result<u64> {█let opened = brain.net_open_stream(conn_id, Some(open_op))?;█let line = ndjson::encode_line(&Attach [...741ch elided...] )> {█let line = ndjson::encode_line(&AttachRecord::Resize { rows, cols });█brain.net_stream_send(stream_id, &line, None, false)?;█Ok(())█}█/// Operator-side: send one input record (the keystrokes) onto an attach█/// stream. `wire_op` is the operator-minted durable id the TARGET journals the█/// PTY write under - exactly-once at the keys, however often the record█/// crosses the wire. The send itself is unjournaled (see module docs).█// [impl->REQ-INST-8]█pub fn send_attach_input(█brain: &mut Brain,█stream_id: u64,█bytes: &[u8],█wire_op: u64,█) -> io::Result<()> {█let line = ndjson::encode_line(&AttachRecord::Input {█data_b64: encode_bytes(bytes),█op_id: wire_op,█});█brain.net_stream_send(stream_id, &line, None, false)?;█Ok(())█}█\u000f█</out>█# Tool call ¶█//Reading PTY ownership layer█read(path=\"crates/spt-term/src/pty.rs\", selector=null)█<out>█\u000e//! Native-PTY session backend - ConPTY on Windows 10+, `forkpty(3)` on Unix,█//! both via `portable-pty` (the exact crate + handoff shape Spikes #1 / #4█//! proved).█//!█//! [`PtySession`] spawns a child under a real PTY, owns the master reader and█//! writer, and serves the [`SessionSurface`] contract. It is **single-surface█//! per handle** - one child's PTY, not the multiplexed fleet (that supervisor is█//! the M3b broker).█//!█//! ## Concurrency discipline (Spike #5, binding)█//!█//! `resize()` races the reader thread on Windows; the master is therefore held█//! behind a `Mutex` and every `resize` takes that lock. The writer is a shared█//! `Arc<Mutex<...>>` so the A2 DSR drain thread can answer ConPTY's startup█//! cursor query through the same serialized writer the caller injects input█//! through - no torn writes.█use std::io::{Read, Write};█use std::sync::{Arc, Mutex};█use portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, ExitStatus, MasterPty};█use crate::surface::{SessionSurface, SurfaceError, SurfaceSize};█/// Shared, serialized handle to the PTY master writer. Cloned to the A2 drain█/// thread so the DSR auto-answer and caller input never interlea [...8108ch elided...] ();█w.write_all(bytes).map_err(SurfaceError::Io)?;█w.flush().map_err(SurfaceError::Io)█}█fn resize(&self, size: SurfaceSize) -> Result<(), SurfaceError> {█// Hold the master lock so resize never races a concurrent reader/resize█// (Spike #5: resize races the reader on Windows).█self.master█.lock()█.unwrap()█.resize(size.into())█.map_err(|e| SurfaceError::Io(io_other(e)))█}█}█/// Fold a `portable-pty` error (a private `anyhow::Error` alias) into a█/// `std::io::Error` so the crate's public surface never leaks `anyhow`. Generic█/// over `Display` to avoid naming the un-exported error type.█fn io_other<E: std::fmt::Display>(e: E) -> std::io::Error {█std::io::Error::other(e.to_string())█}█\u000f█</out>█# Tool call ¶█//Reading attach network protocol█read(path=\"crates/spt-net/src/net/attach.rs\", selector=null)█<out>█\u000e//! Remote terminal attach records (M4-D5b, REQ-INST-8) - the byte-stream█//! viewport onto an instance running on another node.█//!█//! Remote-control is a **distinct mode from local operation** (CONTEXT█//! §Remote-control vs local operation): compute + files stay on the target█//! node; the operator's node is a viewport - a Shell-like driven surface,█//! daemon-to-daemon over the broker-owned QUIC streams (D4). The records here█//! are the wire shape of that viewport: the operator node opens a bidi stream,█//! sends [`AttachRecord::Request`], and the target node pumps its session's█//! `OutputLog` chunks back as [`AttachRecord::Output`] while injecting█//! [`AttachRecord::Input`] into the PTY through the journaled exactly-once█//! input path.█//!█//! Framing is the shared NDJSON ([`crate::net::ndjson`]); chunk bytes ride█//! base64 inside the JSON (the same posture as the IPC catalog's█//! `data_b64` fields - no binary fork of the codec). `Output.seq` is the█//! target session's `OutputLog` sequence verbatim, so the operator side dedups█//! replays (a target-brain restart redrives journaled sends) by cursor, and a█//! restarted operator brain resubscribes the QUIC stream and ski [...10592ch elided...] AttachDecoder::new();█assert_eq!(dec.push(&r.encode_line()), vec![r]);█}█}█// [unit->REQ-KICK-1] the kick protocol round-trips: the `Take` intent (the█// explicit displacement request) and the `Displaced{by}` loud notice the█// displaced controller renders both survive the wire intact.█#[test]█fn kick_take_and_displaced_round_trip() {█let take = AttachRecord::Request {█session_id: 9,█from_seq: 0,█intent: AttachIntent::Take,█endpoint_id: None,█};█let displaced = AttachRecord::Displaced { by: \"hfenduleam\".into() };█for r in [take, displaced] {█let mut dec = AttachDecoder::new();█assert_eq!(dec.push(&r.encode_line()), vec![r]);█}█}█}█\u000f█</out>█# Tool call ¶█//Reading key broker tests list█read(path=\"crates/spt-daemon/tests\", selector=null)█<out>█\u000e.█- docs_server_e2e.rs 5.4KB 22h ago█- propagate.rs 30.1KB 23h ago█- pumpdeadline.rs 6.5KB 5d ago█- pump.rs 36.4KB 5d ago█- false_promote.rs 15.3KB 5d ago█- net_worker_starve.rs 10.7KB 5d ago█- brain_resume_conn_deadlock.rs 16.4KB 5d ago█- conn_blackhole_lifecycle.rs 33.3KB 5d ago█- brain_decouple.rs 37.0KB 5d ago█- inject_control_wedge.rs 160.7KB 5d ago█- two_origin_spanning.rs 12.0KB 1w ago█- wake_single_flight.rs 6.8KB 1w ago█- endpoint_survival.rs 7.8KB 1w ago█- xfer.rs 13.6KB 1w ago█- wanmsg.rs 15.0KB 1w ago█- twohost.rs 69.4KB 1w ago█- replicate.rs 9.7KB 1w ago█- sync.rs 23.8KB 1w ago█- notifsync.rs 10.9KB 1w ago█- netstream.rs 12.9KB 1w ago█- netbroker.rs [...729ch elided...] residency_expectation_e2e.rs 13.3KB 1w ago█- psyche_event_turn_e2e.rs 9.7KB 1w ago█- psyche_context_file_e2e.rs 9.9KB 1w ago█- presence.rs 7.1KB 1w ago█- pairjoin.rs 6.8KB 1w ago█- mesh.rs 21.0KB 1w ago█- legacy_resident_sweep_e2e.rs 9.3KB 1w ago█- handoff.rs 7.2KB 1w ago█- driven_by_selfheal.rs 20.8KB 1w ago█- digest.rs 8.1KB 1w ago█- daemon_lifecycle_real_brain.rs 5.4KB 1w ago█- control_stamp_lifetime.rs 19.5KB 1w ago█- budget.rs 9.7KB 1w ago█- brain_swap.rs 8.0KB 1w ago\u000f█</out>█# Tool call ¶█//Indexing attach reconnect tests█grep(pattern=\"#\\\\[(test|cfg)|fn [a-zA-Z0-9_]+\\\\(|REQ-(RC|HAZARD|BRAIN)|reconnect|detach|controller|viewer|epoch|half|poison|stall\", path=\"crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/broker.rs;crates/spt-daemon/tests/control_stamp_lifetime.rs;crates/spt-daemon/tests/inject_control_wedge.rs;crates/spt-daemon/tests/netbroker.rs;crates/spt-daemon/tests/attach_wedge_e2e.rs;crates/spt/tests/attach_wedge_e2e.rs;crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/src/rc.rs\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/tests/█### attach.rs█13|//! on the stream table's handshake-proven `remote_id_hex`;█*14|//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies█15|//! mid-attach; the broker keeps the QUIC stream, the session, the output█16|//! log, and the effect journal. The successor brain re-serves the SAME█17|//! stream from seq 0 - the worst-case stale cursor: the replayed input█...█27|use spt_daemon::attach::{█*28| read_controller_event_resuming, request_attach, send_attach_input, serve_attach,█*29| AttachServeOutcome, ControllerIrrecoverablyBehind,█30|};█31|// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.█*32|#[cfg(unix)]█33|use spt_daemon::attach::request_attach_endpoint;█34|use spt_daemon::brain::{Brain, BrainState, BrokerEvent};█35|use spt_daemon::effect::{Minter, MintedOp};█...█49|█*50|fn init_home() {█51| HOME.get_or_init(|| {█52| let dir = tempfile::TempDir::new().expect(\"temp dir\");█53| std::env::set_var(\"SPT_HOME\", dir.path());█...█58|static SEQ: AtomicU32 = AtomicU32::new(0);█*59|fn unique_name() -> String {█60| let n = SEQ.fetch_add(1, Ordering::Relaxed);█6 [...27556ch elided...] set: bool) {█191| use std::sync::atomic::Ordering::Release;█192| match param {█193| 1000 | 1002 | 1003 => mode.enabled.store(set, Release),█...█198|█*199|/// Bug #14 (REQ-RC-IDMARKER-DISABLE): ship the top-right endpoint-id status█200|/// marker DISABLED. As a one-shot absolutely-positioned paint it scrolls off█201|/// screen and is never re-stickied (#14), and its DECSC/clear/SGR injection█202|/// splices into the harness's in-flight drawing, leaving residual artifacts on█...█209|█*210|/// Whether the controller reserved-row identity marker is active for this attach:█*211|/// only for a controller (a viewer's viewport is read-only) AND only while the█212|/// marker is enabled (bug #14 ships it off).█Skipped missing paths: crates/spt-daemon/tests/attach_wedge_e2e.rs\u000f█</out>█# Tool call ¶█//Finding update interaction tests█grep(pattern=\"old_gen_drained|stall_evict|CONN_WRITE_POISONED|black[-_ ]hole|suspend|refresh|update|detach|reattach|reconnect\", path=\"crates/spt-daemon/tests;crates/spt/tests;tests;scripts;docs\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/tests/█## attach.rs█140|/// origin), drive `marker` through the PTY and render it back over the wire,█*141|/// then detach and confirm the serve loop ended `Detached` (session outlives).█142|/// Returns the rendered viewport bytes + the (still-live) target brain so the█143|/// caller can assert session survival and clean up.█144|#[allow(clippy::too_many_arguments)]█*145|fn attach_drive_detach(█146| operator: &mut Brain,█147| target_broker: &str,█148| mut target: Brain,█...█191|█*192| // Detach: the viewport ends with the session still running.█193| operator█194| .net_stream_send(stream_b, &[], None, true)█*195| .expect(\"detach\");█196| let (outcome, target) = server.join().expect(\"serve thread\");█*197| assert_eq!(outcome, AttachServeOutcome::Detached);█198| (viewport, target)█199|}█200|█...█295|// endpoint\" over the byte-identical cross-node attach pump; the session outlives█*296|// the viewport (detach never kills it).█297|#[test]█298|fn spt_hosted_bringup_then_cross_node_attach_drives_the_pty() {█299| init_home();█...█360|█*361| // Detach: the brought-up session outlives the viewport.█362| opera [...43249ch elided...] : the journaled dial records NO phantom conn - the journal█...█260|█*261| // B: the black hole - accepts the QUIC handshake (`membership: None` => it█262| // registers the conn) but never runs the proof responder, so A's█263| // `prove_membership` awaits a control-stream reply that never comes.█264| let _broker_b = net_broker(&name_b, Identity::generate(), &dir.path().join(\"b\"));█...█273| .net_dial(b_addr, Some(MintedOp::new(Minter::Cli, OP)))█*274| .expect_err(\"a black-holed dial must error, never block forever\");█275| let elapsed = started.elapsed();█276|█277| // The broker REPLIED an ordinary error within the bound - not a wedge, and█Showing files 1-20 of 152+. Use skip=20 for the next page, or narrow paths/pattern.█Skipped missing paths: scripts\u000f█</out>█# Assistant ¶█_**Requesting specific code sections**_█# Tool call ¶█//Reading output log transitions█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"620-970\")█<out>█\u000e/// Pure divergence decision for the control/viewer stamp convergence█/// (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): does the perch's RECORDED█/// (`driven_by`, `controlled`, `viewer_count`) differ from the live session TRUTH?█/// Returns `(control_write_needed, viewer_write_needed)` - the divergence gate that█/// keeps the per-poll re-assert from writing (fsync) when nothing changed.█// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]█fn stamp_divergence(█rec_driven_by: Option<&str>,█rec_controlled: bool,█rec_viewers: u32,█want_driven_by: Option<&str>,█want_controlled: bool,█want_viewers: u32,█) -> (bool, bool) {█let control = rec_driven_by != want_driven_by || rec_controlled != want_controlled;█let viewer = rec_viewers != want_viewers;█(control, viewer)█}█/// Converge a perch's control/viewer stamps to the live session truth, OFF any log█/// lock (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). The broker stays the single writer;█/// this closes the STAMP-BEFORE-BIND loss - a picker-created endpoint's spawn stamps█/// `controlled=true` BEFORE the adapter binds its perch, so `mutate_info` returns█/// `NotFound` and the edge stamp is swallowed; the adapter's bi [...15800ch elided...] ontroller).█fn clear_controller(&mut self) {█if self.controller.take().is_some() {█self.stamp_driven_by();█}█}█/// B-2 (REQ-CONTROLLER-LIVENESS-REAP): lazily drop a controller whose WRITER█/// THREAD has EXITED - the record-vs-truth honesty step, run on the authoritative█/// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The█/// writer exits when a socket write fails (a severed / keepalive-killed controller█/// conn) or its `tx` drops; while it is parked on `rx.recv()` (a LIVE but idle█/// controller) it is NOT finished, so a live controller is never touched (the█/// no-false-reap guard). Reaping here makes the broker report honest truth so the█...█}█[Showing lines 619-973 of 6009. Use :974 to continue]\u000f█</out>█# Tool call ¶█//Reading sink writer tasks█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"1420-1605\")█<out>█\u000efn viewer_writer(█...█) {█...█// `tx` dropped. If this was an EVICTION for overflow (append set the flag█// before dropping the sink), emit a KIND_VIEWER_EVICTED marker - DISTINCT from█// a session-exit EOF - so the serving brain skips to live (re-subscribes from█// the ring floor) instead of dying silently. A NORMAL teardown (session end /█// conn drop) leaves the flag false -> no marker, exit quietly. This write is in█// the dedicated writer thread, OFF the log lock / off the drain (the W1/b4█// isolation is preserved). REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT.█// [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]█if evicted.load(Ordering::Acquire) {█let marker = evicted_envelope(session_id);█let _ = send.write(&marker);█}█}█/// Round-4 observability bridge: a controller writer whose bounded conn write█/// was DEADLINE-retired (`TimedOut` - the conn watchdog aborted a wedged█/// write, or a gate-wait behind one exhausted the bound) IS the█/// brain-subscriber stall-evict fact the W2 tally counts. Pre-round-4 that█/// wedge aged until a logical sampler (`stall_evict_controller`) observed it;█/// the physical bound now fires FIRST, so the tally is [...7945ch elided...] send.lifecycle_event(█\"writer-exit\",█&format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),█);█return;█}█}█if let Some(seq) = seq {█// (a) CONTIGUOUS cursor (b4 / B2 gapless-resume - doyle's gate): with the█// drop-on-full handoff in `append`, the controller's live channel can SKIP█// a seq (dropped while Full). The frame still went to the socket (the live█// operator sees it), but the durable resume cursor must NOT jump past the█// gap - see [`contiguous_advance`].█contiguous_advance(&delivered_through, seq);█}█}█...█}█[Showing lines 1419-1608 of 6009. Use :1609 to continue]\u000f█</out>█# Tool call ¶█//Reading connection cleanup path█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"2960-3220\")█<out>█\u000e█/// The output-log high-water (`next_seq`) of a hosted session, or `None` if█/// absent. It advances every time the child emits output - the█/// **functional**-survival seam for the D7-1 process-level E2E (doyle F1):█/// after a brain-PROCESS swap, writing input to the broker-held PTY child and█/// watching THIS advance proves the path is alive end-to-end (input -> child ->█/// echo -> broker ring), not merely that the handle is held. A read-only direct█/// peek, like [`session_pid`](Self::session_pid).█// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]█pub fn session_output_seq(&self, id: u64) -> Option<u64> {█self.sessions█.lock()█.unwrap()█.get(&id)█.map(|h| recover_log(&h.log).next_seq)█}█/// Drive one brain connection: handshake, then dispatch its command frames█/// until it disconnects. On disconnect, detach this connection from any█/// sessions it subscribed to - the sessions themselves persist (B2: a brain█/// restart must not disturb the hosted children).█fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {█// RAII handler tally (panic-safe): [...10825ch elided...] ream_subs {█host.detach_stream_sub(*id, &send);█}█// And presence: the liveness log + its ring persist (D4c).█if my_presence_sub {█host.detach_presence_sub(&send);█}█}█Ok(())█}█/// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning█/// connection, start its output drain and exit waiter, and reply `spawned`.█/// Every hosted session gets its **own** drain + exit-waiter threads (KH█/// 7.4): a hung child blocks only its own threads - shell instances ride█/// exactly this isolation (one owner's hung shell binary stalls neither█/// another owner's delivery nor the broker's control dispatch).█[Showing lines 2959-3223 of 6009. Use :3224 to continue]\u000f█</out>█# Tool call ¶█//Reading subscribe dispatch path█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"3425-3510\")█<out>█\u000e fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {█...█recover(&self.sessions).insert(█...█HostedSession {█...█adapter: req.adapter.clone(),█install_dir: req.install_dir.clone().map(std::path::PathBuf::from),█},█);█let resp = Spawned {█session_id: id,█pid,█};█let frame = Envelope::new(█KIND_SPAWNED,█serde_json::to_value(resp).expect(\"Spawned serializes\"),█);█send_frame(send, &frame);█Ok(id)█}█/// Re-attach a brain to a session's output under the W2.5 controller/viewer█/// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,█/// exactly-once resume. The `intent` + `by` select the role; the broker█/// resolves it (controller take / viewer add / busy-refuse / loud displace)█/// and replies the [`SubscribeOutcome`] so the serving brain can wake█/// (controller-only), stay wake-neutral (viewer), or refuse (busy). A█/// same-`by` re-subscribe silently re-takes the controller - the gate-#7█/// self-kick guard.█fn dispatch_subscribe [...2537ch elided...] return Ok(());█}█}█};█recover_log(&log).detach_if(send);█Ok(())█}█/// Inject input bytes into a session's PTY.█///█/// With an `op_id` the write is routed through the broker-owned█/// [`EffectJournal`] for exactly-once delivery: a replay of the same█/// `(session_id, op_id)` after a brain crash is deduped (the PTY is not█/// written twice), and either outcome - fresh apply or dedup - is acked back█/// with an [`crate::msg::KIND_APPLIED`] frame so the brain can retire the█/// effect (B5 / KNOWN-HAZARDS 7.2). Without an `op_id` it is the legacy█/// direct write (no dedup, no ack).█// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]█[Showing lines 3424-3513 of 6009. Use :3514 to continue]\u000f█</out>█# Tool call ¶█//Reading current wedge hazard rules█read(path=\"docs/KNOWN-HAZARDS.md\", selector=\"408-460,480-515,556-590,604-660\")█<out>█\u000e### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain `[REQ-HAZARD-VIEWER-ISOLATION]`█<!-- [doc->REQ-HAZARD-VIEWER-ISOLATION] -->█- **Failure:** the W2.5 controller/viewer model lets ANY number of read-only `--view` attachers ride one session's broker `OutputLog`. The single drain thread fans each output chunk to every attacher. If a viewer's socket is fanned out with a **blocking** write under the log lock (the controller's authoritative path), one wedged viewer (a slow terminal, a black-holed WAN peer, a hostile non-reader) stalls the drain - freezing the controller's stream and backing up the PTY child. A single watcher must never be able to degrade or hang the driver.█- **Invariant:** the drain writes the **controller** on the authoritative blocking bounded path (it alone advances `delivered_through`), but each **viewer** gets an **isolated bounded SPSC queue + a dedicated writer thread**; the drain `try_send`s under the log lock and **evicts** any viewer whose queue is `Full` (fell behind the live stream) or `Disconnected` (its writer died on a dead socket) - the drain thread **never touches a viewer socket**, so no viewer write can backpr [...61725ch elided...] ed as a **project-only pending slice under the COMMUNE suffix, whatever the source drop's kind** - a signoff-suffix pending would be reaped by `sweep_stale_signoff` on the next listener start (the signoff sentinel is write-once-per-generation), recreating the black-hole on the signoff path; the commune-named pending is a pure project-slice carrier both suffixes ingest identically and the sweep never sees. The already-committed live slice is droppe...█- **spt-core mapping:** `spt-live/src/ingest.rs` `ingest_drops` (deferred-project preserve + `Ingested.preserved`); `route_slices` project gate unchanged (`REQ-STORE-CONTEXT-BRANCH-FILL`); kin `REQ-HAZARD-DROP-FILE-SINGLE-WRITER` (core stays the single writer - the preserve rewrite is core's own write).█...█...█[Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading attach restart tests█read(path=\"crates/spt-daemon/tests/attach.rs\", selector=\"1-230,760-1035\")█<out>█\u000e//! D5b regression - remote terminal attach (REQ-INST-8): drive a real PTY█//! session on daemon A from daemon B, byte-stream viewport over the█//! broker-owned wire; compute stays on A.█//!█//! Two in-process brokers (hermetic loopback endpoints, relays disabled) play█//! the two daemons; a real echo child runs under A's broker PTY. The█//! invariants:█//!█//! - **remote drive**: keystrokes sent from B arrive at A's PTY through the█//! journaled input path, and the session's output streams back to B as█//! sequenced records - B sees its own marker echoed by A's child;█//! - **origin is transport truth** (KNOWN-HAZARDS 7.5): the serve loop gates█//! on the stream table's handshake-proven `remote_id_hex`;█//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies█//! mid-attach; the broker keeps the QUIC stream, the session, the output█//! log, and the effect journal. The successor brain re-serves the SAME█//! stream from seq 0 - the worst-case stale cursor: the replayed input█//! dedups at the PTY-write journal (typed exactly once), re-transmitted█//! output dedups at B's render cursor (rendered exactly once) - including█//! input B sent INTO the d [...19737ch elided...] re-stamps█// driven_by=None asynchronously - so poll for it (production clears the same█// async way when the worker connection drops).█let cleared = (0..200).any(|_| {█if spt_store::info::read_info(&perch_path)█.and_then(|i| i.driven_by)█.is_none()█{█return true;█}█std::thread::sleep(std::time::Duration::from_millis(10));█false█});█assert!(cleared, \"viewport over -> marker cleared (broker re-stamps None)\");█target.kill_session().expect(\"cleanup kill\");█}█// -- W2.5 controller/viewer model - broker-IPC-level gate evidence ------------█// These exercise the OutputLog controller/viewer fan-out directly over the local█// broker IPC: each `attach_as` carries a distinct `by` identity, so the role\u000f█</out>█# Tool call ¶█//Reading brain resume wedge test█read(path=\"crates/spt/tests/resume_no_control_steal_e2e.rs\", selector=\"1-80,430-500\")█<out>█\u000e//! UPDATE-WEDGE round 2 (REQ-BRAIN-RESUME-NO-CONTROL-STEAL) - the multi-broker-PTY-█//! session RESPAWN rig (doyle-ruled 2026-07-09). This is the coverage the counter-54█//! single-black-holed-session A'-rig (`trial_drain_drive_e2e.rs`) LACKED: N REAL broker█//! PTY sessions, each with its OWN local controller producing output, PRESENT at a real█//! brain respawn - the exact field condition (7 spt-hosted PTYs at a `fetch --apply`) the█//! resume-steal bug needed and the single-session rig never exercised.█//!█//! ## The field incident this locks down (root, log-anchored - docs/UPDATE-WEDGE-2-RCA.md)█//!█//! `spt update fetch --apply` (v0.30.2 -> v0.30.3) PROMOTED CLEANLY (the counter-54 fix█//! worked) but under 7 live spt-hosted broker PTYs it FROZE all 7 + failed every `spt rc`█//! retake. ROOT (SME docs/UPDATE-WEDGE-2-SME-todlando.md): `run_brain` calls█//! `resume_sessions()` at boot, which re-attached EVERY session `KIND_SESSIONS` returns as█//! `subscribe_with(AttachIntent::Control, by:None)`. The docstring's \"a None identity never█//! displaces (falls back to viewer)\" is a MYTH for FREE / SA\u000eME-LOCAL-IDENTITY slots:█//! `resolve_subscribe` (broker.rs) stall-evicts first, then [...7775ch elided...] // each slot, dropping A_i -> its tally froze -> gained==0. GREEN (Viewer): A_i untouched.█// [int->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]█for (i, g) in gained.iter().enumerate() {█assert!(█*g > 0,█\"session {i} (sid {}) LOST its controller across the brain respawn: its \\█controller conn received {g} new output frames in the post-resume window (froze \\█at {}). The respawn's resume_sessions STOLE the controller slot (Control \\█re-attach -> become_controller displaced the local controller) then never drained \\█it - Failure A. GREEN (resume re-attaches Viewer) leaves every controller intact. \\█ticks_before={t0:?} ticks_after={t1:?}\",█sessions[i].0,█t1[i],█);█}█\u000f█</out>█# Tool call ¶█//Reading reconnection test names█read(path=\"crates/spt/src/rc.rs\", selector=\"2700-3214\")█<out>█\u000emod tests {█...█fn translate_key_event_emits_exact_xterm_vt() {█...█assert_eq!(ev(KeyCode::Char('z'), ctrl), vec![0x1a]);█// --- Alt+<char> -> ESC-prefixed (meta) ---█assert_eq!(ev(KeyCode::Char('x'), alt), vec![0x1b, b'x']);█// --- named editing/control keys ---█assert_eq!(ev(KeyCode::Enter, N), vec![b'\\r']); // 0x0d█assert_eq!(ev(KeyCode::Tab, N), vec![b'\\t']); // 0x09█assert_eq!(ev(KeyCode::BackTab, N), vec![0x1b, b'[', b'Z']);█assert_eq!(ev(KeyCode::Esc, N), vec![0x1b]);█// Backspace / Ctrl+Backspace - relocated W7 evidence:█// [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]█assert_eq!(ev(KeyCode::Backspace, N), vec![0x7f]); // char-delete DEL█// [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]█assert_eq!(ev(KeyCode::Backspace, ctrl), vec![0x08]); // word-delete ^H█// --- CSI tilde keys (unmodified) ---█assert_eq!(ev(KeyCode::Delete, N), vec![0x1b, b'[', b'3', b'~']);█assert_eq!(ev(KeyCode::Insert, N), vec![0x1b, b'[', b'2', b'~']);█assert_eq!(ev(KeyCode::PageUp, N), vec![0x1b, b'[', b'5', b'~']);█assert_eq!(ev(KeyCode::PageDown, N), vec![0x1b, b'[ [...22825ch elided...] let mut fired = false;█let before = sink.len();█if scanner.feed(alt) {█fired = true;█sink.write_all(&status.assert_bytes()).unwrap();█}█assert!(fired, \"the alt-screen enter trips the re-assert scanner\");█assert!(sink.len() > before, \"a re-assert was appended after the trigger\");█assert!(█sink[before..].starts_with(b\"\\x1b[2;24r\"),█\"the re-assert re-sets the DECSTBM margin (not just a repaint)\"█);█// (c) pump EXIT: restore (reset region + clear row 1).█sink.write_all(&StatusRow::restore_bytes()).unwrap();█assert!(█sink.ends_with(b\"\\x1b[r\\x1b[1;1H\\x1b[2K\"),█\"exit restores the full scroll region and clears the reserved row\"█);█}█}█\u000f█</out>█# Assistant ¶█_**Formulating hypotheses on brain respawn stall**█**Analyzing refresh command and connection reuse**█**Tracing brain dispatcher lifecycle and streams**█**Analyzing controller detach and recovery behavior**█**Searching cursor usage and refresh commands**_█# Tool call ¶█//Tracing daemon refresh boundary█grep(pattern=\"Daemon.*Refresh|Refresh.*Daemon|cmd_daemon_refresh|daemon refresh|KIND_BRAIN_RESTART|brain_restart|resume_session_cursors|resume_sessions\\\\(\", path=\"crates/spt/src;crates/spt-daemon/src;crates/spt/tests;crates/spt-daemon/tests\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt/█### src/█#### cli.rs█1475| DaemonCmd::Status => cmd_daemon_status(json),█*1476| // [impl->REQ-DAEMON-REFRESH]█*1477| DaemonCmd::Refresh => cmd_daemon_refresh(),█1478| DaemonCmd::Brain {█1479| generation,█1480| start_reason,█...█4057|█*4058|/// `spt daemon refresh` - cycle the daemon's brain in place with NO binary swap█*4059|/// (REQ-DAEMON-REFRESH): the apply_staged brain-cycle path minus the swap, so the█4060|/// broker and every hosted PTY survive by construction. Requires a running█*4061|/// daemon (nothing to refresh otherwise - `daemon start` is the cold path). A█4062|/// broker without a supervisor wired accepts but cannot act; that is loud, not█4063|/// silent - the operator asked for a cycle that did not happen.█*4064|// [impl->REQ-DAEMON-REFRESH]█*4065|fn cmd_daemon_refresh() -> i32 {█4066| if !spt_daemon::is_running() {█*4067| eprintln!(\"DAEMON_NOT_RUNNING: nothing to refresh - `spt daemon start` brings it up fresh\");█4068| return 1;█4069| }█*4070| match spt_daemon::refresh_brain(&spt_daemon::broker_socket_name()) {█4071| Ok(true) => {█[...25137ch elided...] ;█#### resume.rs█188| let mut r = connect_retry(&name);█*189| let mut resumed = r.resume_sessions().expect(\"resume all hosted sessions\");█190| resumed.sort_unstable();█191| let mut want = vec![sid_a, sid_b];█192| want.sort_unstable();█...█259| let resumed = r█*260| .resume_sessions()█261| .expect(\"resume succeeds despite eviction\");█262| assert_eq!(resumed, vec![sid], \"the session re-attached\");█263|█...█329| let mut r = connect_retry(&name);█*330| let resumed = r.resume_sessions().expect(\"resume\");█331| assert_eq!(resumed, vec![seed_sid], \"resumed the seed session\");█332|█333| // First fresh spawn: delivers under both old and new code (it is the one█Showing files 1-20 of 21. Use skip=20 for the next page, or narrow paths/pattern.\u000f█</out>█# Tool call ¶█//Reading current brain startup resume█read(path=\"crates/spt-daemon/src/brainproc.rs\", selector=\"150-320\")█<out>█\u000e/// meaningful only once the broker-owned NetHost is up, so we gate on the█/// existing `net-status` verb (which also serves as the broker-liveness probe)█/// and start them exactly once, on the first `enabled` reply. That single gate█/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the█/// NetHost bind in the background and `net-status` flips `enabled` when it█/// attaches, at which point the brain starts the consumers - no extra signal.█///█/// It inherits the broker's already-unelevated token as a plain child, so it█/// runs no de-elevation guard of its own.█// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]█pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {█// Persist this brain generation's stderr to the rotating SPT_HOME/logs sink█// (REQ-DAEMON-STDERR-PERSIST) - a detached brain's stdio is /dev/null, so every█// diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs█// FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.█// [impl->REQ-DAEMON-STDERR-PERSIST]█let _ = crate::stderrlog::install(\"brain\", generation);█let name = broker_socket_name();█[...8883ch elided...] toration D2-1):** both are standalone IPC clients█/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the█/// restartable brain. Each mints its own registry + stop flag and connects█/// independently; when the brain process exits these in-process threads die with█/// it, and the respawned brain re-spawns them from the current binary. Mirrors█/// the wiring the broker boot used to do, `node_hex` now sourced from the█/// `net-status` reply rather than the broker's identity load.█// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]█// [impl->REQ-DAEMON-9]█fn spawn_net_consumers(node_hex: &str) {█let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));█let stop = Arc::new(AtomicBool::new(false));█...█}█[Showing lines 149-323 of 2116. Use :324 to continue]\u000f█</out>█# Tool call ¶█//Finding broker session task spawn█grep(pattern=\"drain|exit-waiter|thread::Builder|thread::spawn|take_reader|stamp_reaped|sessions.remove\", path=\"crates/spt-daemon/src/broker.rs\", selector=\"3200-3425\", case=false, gitignore=true, skip=null)█<out>█\u000e 3218| /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning█*3219| /// connection, start its output drain and exit waiter, and reply `spawned`.█*3220| /// Every hosted session gets its **own** drain + exit-waiter threads (KH█3221| /// 7.4): a hung child blocks only its own threads - shell instances ride█3222| /// exactly this isolation (one owner's hung shell binary stalls neither█3223| /// another owner's delivery nor the broker's control dispatch).█...█3334|█*3335| // Output drain -> append under the log lock (fast: ring + viewer fan-out),█3336| // then deliver to the controller OFF the lock with a bounded deadline█3337| // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no█3338| // longer inline under the log lock, so a backed-up controller can never█...█3340| // deadline (bounded wedge, never permanent).█*3341| let log_drain = Arc::clone(&log);█*3342| let drain = session█*3343| .drain(move |chunk| {█3344| // append() does the controller handoff inline now (one non-blocking█3345| // try_send - b4) and returns Some(epoch) [...916ch elided...] us) = waiter.wait() {█3364| let ev = ExitEvent {█3365| session_id: id,█...█3376| // Bounded conn write (round-4): a wedged sink can no longer█*3377| // park the exit-waiter thread indefinitely.█3378| let _ = sink.write(&frame);█3379| }█3380| }█...█3385| // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]█*3386| recover_log(&log_exit).stamp_reaped();█3387| // Reap the dead session from the table.█3388| recover(&sessions_exit).remove(&id);█3389| });█...█3417| input,█*3418| drain,█3419| log,█3420| endpoint: req.endpoint.clone(),█3421| translation,\u000f█</out>█# Tool call ¶█//Reading broker session tasks█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"3220-3440\")█<out>█\u000e /// connection, start its output drain and exit waiter, and reply `spawned`.█/// Every hosted session gets its **own** drain + exit-waiter threads (KH█/// 7.4): a hung child blocks only its own threads - shell instances ride█/// exactly this isolation (one owner's hung shell binary stalls neither█/// another owner's delivery nor the broker's control dispatch).█// [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]█fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {█let req: SpawnReq =█serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;█// Single-flight WAKE (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP): the broker is the█// sole spawner post-W3, so its own spawn choke is the single-flight authority.█// Dedup a duplicate wake/resume for an endpoint that already has a live session█// OR a spawn in flight - closing the check-then-spawn TOCTOU that let one wake█// spawn two identical launch trees (perri's flynn dup: duplicate perch writers█// then stomped info.json, routing injects to a contended record). The claim +█// the live-ses [...9324ch elided...] anslation_binary.clone(),█translation_respawns,█adapter: req.adapter.clone(),█install_dir: req.install_dir.clone().map(std::path::PathBuf::from),█},█);█let resp = Spawned {█session_id: id,█pid,█};█let frame = Envelope::new(█KIND_SPAWNED,█serde_json::to_value(resp).expect(\"Spawned serializes\"),█);█send_frame(send, &frame);█Ok(id)█}█/// Re-attach a brain to a session's output under the W2.5 controller/viewer█/// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,█/// exactly-once resume. The `intent` + `by` select the role; the broker█[Showing lines 3219-3443 of 6009. Use :3444 to continue]\u000f█</out>█# Tool call ¶█//Indexing lifecycle regression tests█grep(pattern=\"^fn [a-zA-Z0-9_]+\\\\(|^#\\\\[(test|cfg)|\\\\[int->|\\\\[unit->\", path=\"crates/spt-daemon/tests/brain_decouple.rs;crates/spt-daemon/tests/brain_resume_conn_deadlock.rs;crates/spt-daemon/tests/conn_blackhole_lifecycle.rs;crates/spt-daemon/tests/false_promote.rs;crates/spt/tests/resume_no_control_steal_e2e.rs;crates/spt/tests/attach_wedge_e2e.rs;crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/control_stamp_lifetime.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/tests/█### brain_decouple.rs█69|static SEQ: AtomicU32 = AtomicU32::new(0);█*70|fn unique_name() -> String {█71| let n = SEQ.fetch_add(1, Ordering::Relaxed);█72| format!(\"spt-daemon-braindecouple-{}-{}.sock\", std::process::id(), n)█73|}█...█75|static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();█*76|fn init_home() {█77| HOME.get_or_init(|| {█78| let dir = tempfile::TempDir::new().expect(\"temp dir\");█79| std::env::set_var(\"SPT_HOME\", dir.path());█...█83|█*84|fn kill_pid(pid: u32) {█85| #[cfg(windows)]█86| let _ = std::process::Command::new(\"taskkill\")█87| .args([\"/PID\", &pid.to_string(), \"/F\", \"/T\"])█...█96|/// that fills a non-reading controller's socket and parks its writer.█*97|fn flood_spawn_req(endpoint: &str) -> SpawnReq {█98| #[cfg(unix)]█99| let (program, args) = (\"yes\".to_string(), vec![\"FLOODFLOODFLOODFLOOD\".to_string()]);█100| #[cfg(windows)]█...█121|█*122|fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {█123| write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");█124|}█125|█*126|fn connect(name: &str) -> Stream {█127| for _ in 0..200 { [...22879ch elided...] *263| // intends to kill it. [int->REQ-DAEMON-STOP-LIVE-SESSION-WARN]█264| cmd.no_window().args([\"daemon\", \"stop\", \"--force\"]).env(\"SPT_HOME\", home.path());█265| output_bounded(cmd, Duration::from_secs(20))█266| };█...█302| // L2 keystone: the broker survived the dead-child + dropped-pump and still serves.█*303| // [int->REQ-HAZARD-ATTACH-WEDGE]█304| assert!(█305| fresh_online,█306| \"after a dead PTY child + a dropped operator pump, a NEW `endpoint run` must \\█...█314| // L3 (folded): the dead endpoint is offlined within a reconcile tick.█*315| // [int->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]█316| assert!(█317| victim_offlined,█318| \"the dead victim endpoint must be OFFLINED within a reconcile tick (its broker \\\u000f█</out>█# Tool call ¶█//Finding wedge RCA documents█glob(path=\"docs/**/*{WEDGE,wedge,RCA,SME,CODEREAD,RULING}*.md\", hidden=true, gitignore=true, limit=200)█<out>█\u000e# docs/█UPDATE-WEDGE-2-ROUND3-CODEREAD.md█UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md█GATEWAY-RCA-STEP1.md█F-035-RCA.md█F-035-RELAY-RCA.md█PUMP-W2-DISPATCH-RULING.md█MUSL-W3-DISPATCH-RULING.md█MUSL-W1-DISPATCH-RULING.md█W6-DISPATCH-RULING.md█W5-DISPATCH-RULING.md█W4-DISPATCH-RULING.md█W3-DIGEST-GENERATION-UNION-RULING.md█W2-DESIGN-RULING.md\u000f█</out>█# Assistant ¶█_**Reviewing reconnect test sections**_█# Tool call ¶█//Reading rc reconnect e2e█read(path=\"crates/spt/src/rc.rs\", selector=\"2130-2430\")█<out>█\u000e // (doyle-ruled single-node fixture): a broker death under a live viewport█// auto-reconnects - the capture shows pre-bounce output, then the centered█// reconnect banner, then post-bounce output from the re-established serve,█// and the viewport still ends HONESTLY (session exit -> Exited). Everything█// is in-process against an isolated home: the broker + seed sockets are█// home-tagged (no live-box collision), and the seed channel stays UP through█// the bounce so establish_attach's CONNECT-ONLY is_running() probe stays█// satisfied (REQ-RC-RECONNECT-TRUTH: rc never auto-starts a daemon - the seed█// channel up lets the retry re-connect the broker child; a seed death would█// instead yield DaemonDown + a bounded retry with NO resurrection - the gated█// step's remote leg owns the real-daemon `spt daemon stop` shape at B).█#[test]█// Both broker children are kill()+wait()ed on the assert path; a panicking█// test leaks them only until process teardown (their pipes die with us).█#[allow(clippy::zombie_processes)]█fn attach_viewport_reconnects_across_a_broker_bounce() {█let _home = crate::testutil::isolated_home [...12529ch elided...] Session));█}█// [unit->REQ-RC-RECONNECT] the give-up decision: bounded window, boundary█// inclusive - unit-tested without a 30s wait.█#[test]█fn reconnect_window_expiry_decision() {█assert!(!reconnect_expired(Duration::from_secs(0)));█assert!(!reconnect_expired(RECONNECT_WINDOW - Duration::from_millis(1)));█assert!(reconnect_expired(RECONNECT_WINDOW));█assert!(reconnect_expired(RECONNECT_WINDOW + Duration::from_secs(1)));█}█// [unit->REQ-RC-RECONNECT] the reconnect banner: full clear + home, the text█// CENTERED on the terminal (row rows/2, col centered on the char count), and█// an over-wide text clamps to column 1 instead of underflowing off-screen.█#[test]█[Showing lines 2129-2433 of 3214. Use :2434 to continue]\u000f█</out>█# Tool call ¶█//Reading attach wedge e2e█read(path=\"crates/spt/tests/attach_wedge_e2e.rs\", selector=\"1-340\")█<out>█\u000e//! Wave 2 (v0.12.1) **L2** - REQ-HAZARD-ATTACH-WEDGE robustness gate, on the REAL█//! dummy-harness fixture + a REAL detached `spt daemon run` (no mocks, no in-proc█//! `reconcile_once`). Companion to `dummy_harness_e2e.rs` (L0 keystone bring-up).█//!█//! The hazard (v0.12.0 real-harness finding): a *legitimately* dead PTY child (real█//! crash/kill) plus an operator pump that was DROPPED without a clean detach (closed█//! tab / `taskkill /F`) must NOT wedge the broker - every NEW attach / `endpoint run`█//! must still be served, the dead endpoint must be offlined within a reconcile tick,█//! and `daemon stop` must complete bounded.█//!█//! ## What this proves (prove-don't-change, like the M11-W4 capstone)█//!█//! The original B-side root hypothesized the broker's loopback forward (`serve_attach`█//! -> `send_stream` -> a bounded 64 KB `tokio::io::duplex`) blocks forever on a dead█//! operator and parks the 2-worker net runtime. On the post-L0 code that mechanism does█//! NOT hold: the loopback duplex is drained broker-INTERNALLY by the operator row's own█//! read pump (`nethost.rs` `RecvHalf::Loopback`), which for an ordinary attach stream█//! (`retentive_cap == 0`) never parks - s [...12592ch elided...] ked by the dead loopback peer.\\n=== brain stderr ===\\n{brain_stderr}\"█);█// L3 (folded): the dead endpoint is offlined within a reconcile tick.█// [int->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]█assert!(█victim_offlined,█\"the dead victim endpoint must be OFFLINED within a reconcile tick (its broker \\█session is reaped on child death; reconcile_hosted_liveness clears the latch).\\n\\█=== brain stderr ===\\n{brain_stderr}\"█);█// L4 (folded): daemon stop is bounded (the output_bounded above would have panicked█// on timeout); assert it also succeeded cleanly.█assert!(█stop.status.success(),█\"`daemon stop` must complete bounded + clean after the wedge trigger: {}\",█String::from_utf8_lossy(&stop.stderr)█);█}█\u000f█</out>█# Tool call ¶█//Reading daemon attach integration█read(path=\"crates/spt-daemon/tests/attach.rs\", selector=\"1-375,760-1035\")█<out>█\u000e//! D5b regression - remote terminal attach (REQ-INST-8): drive a real PTY█//! session on daemon A from daemon B, byte-stream viewport over the█//! broker-owned wire; compute stays on A.█//!█//! Two in-process brokers (hermetic loopback endpoints, relays disabled) play█//! the two daemons; a real echo child runs under A's broker PTY. The█//! invariants:█//!█//! - **remote drive**: keystrokes sent from B arrive at A's PTY through the█//! journaled input path, and the session's output streams back to B as█//! sequenced records - B sees its own marker echoed by A's child;█//! - **origin is transport truth** (KNOWN-HAZARDS 7.5): the serve loop gates█//! on the stream table's handshake-proven `remote_id_hex`;█//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies█//! mid-attach; the broker keeps the QUIC stream, the session, the output█//! log, and the effect journal. The successor brain re-serves the SAME█//! stream from seq 0 - the worst-case stale cursor: the replayed input█//! dedups at the PTY-write journal (typed exactly once), re-transmitted█//! output dedups at B's render cursor (rendered exactly once) - including█//! input B sent INTO the d [...25997ch elided...] re-stamps█// driven_by=None asynchronously - so poll for it (production clears the same█// async way when the worker connection drops).█let cleared = (0..200).any(|_| {█if spt_store::info::read_info(&perch_path)█.and_then(|i| i.driven_by)█.is_none()█{█return true;█}█std::thread::sleep(std::time::Duration::from_millis(10));█false█});█assert!(cleared, \"viewport over -> marker cleared (broker re-stamps None)\");█target.kill_session().expect(\"cleanup kill\");█}█// -- W2.5 controller/viewer model - broker-IPC-level gate evidence ------------█// These exercise the OutputLog controller/viewer fan-out directly over the local█// broker IPC: each `attach_as` carries a distinct `by` identity, so the role\u000f█</out>█# Tool call ¶█//Reading stamp lifetime tests█read(path=\"crates/spt-daemon/tests/control_stamp_lifetime.rs\", selector=\"1-420\")█<out>█\u000e//! F-026 W1 #2 (REQ-HAZARD-CONTROL-STAMP-LIFETIME) - a control/viewer stamp never█//! outlives its session, on a REAL broker over the real local IPC (no PTY/transport█//! mocks). Red-first: the exit-waiter used to `sessions.remove(&id)` with NO stamp█//! clear, so `/exit` (which kills the CHILD, not the controller conn) left█//! `controlled` / `driven_by` / `viewer_count` latched in info.json forever - and█//! gossiped a stale `controller_node` cross-node.█//!█//! ## The bug this gate locks down█//!█//! `/exit` ends the RC session INDIRECTLY: the endpoint suspends when the child█//! binary terminates, so the teardown runs the REAP path (exit-waiter ->█//! `sessions.remove`), NOT the controller-detach path (`detach_if` ->█//! `clear_controller` -> `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap█//! path dropped the OutputLog with the controller slot still occupied, so the█//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,█//! hall-a's info.json still read `controlled:true` and hfenduleam still gossiped█//! `controller_node=self`.█//!█//! ## What the fix does█//!█//! The exit-waiter calls `OutputLog::stamp_reaped()` on reap, which clears all█//! [...16332ch elided...] ,█after_offline.controlled, after_offline.driven_by, after_offline.status,█);█// -- Reap scoped (decoy child + spawner). --█if let Some(p) = broker.session_pid(decoy_sid) {█kill_pid(p);█}█drop(spawner);█drop(probe);█// Arm A - ONLINE + sticky: BOTH stamps cleared AND the perch offlined.█// [int->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]█assert!(█!after_online.controlled,█\"B3 arm A: reap must clear controlled on an online sessionless spt-hosted perch\"█);█assert_eq!(█after_online.driven_by, None,█\"B3 arm A: reap must clear driven_by (attached_node derives from it) on the online perch\"█);█assert!(█offlined.contains(&online_id),█...█);█...█}█[Showing lines 1-423 of 446. Use :424 to continue]\u000f█</out>█# Tool call ¶█//Reading broker subscribe tests index█read(path=\"crates/spt-daemon/tests/broker.rs\", selector=\"1-80,700-1040,1250-1640\")█<out>█\u000e//! B1 integration: a [`Broker`] hosts one real PTY child and relays its output█//! and injected input over the versioned local IPC, on both OSes.█//!█//! The child is an echo program (`cat` on Unix, `findstr \".\"` on Windows - the█//! same cross-OS shape spt-term's injection tests use): bytes the brain injects█//! round-trip through the real PTY and come back as `output` events. We then█//! kill it and observe the `exit` event. This exercises the whole B1 contract -█//! spawn under a broker PTY, stream output, accept input, report exit - through█//! the actual named-pipe / UDS transport.█//!█//! The brain is driven **single-threaded**: send a command, then read frames█//! until the expected reply/event arrives (accumulating any interleaved output).█//! interprocess `Stream::split()` shares the underlying fd, so a split client█//! would only signal EOF once *both* halves drop; keeping one `Stream` means█//! dropping it closes the fd, the broker's command loop hits EOF, and it tears█//! the session down deterministically - which we then assert.█// [int->REQ-DAEMON-2]█use std::sync::atomic::{AtomicU32, Ordering};█use std::sync::Arc;█use std::thread;█use std::time::Duration;█use inte [...16761ch elided...] wn the echo child; the broker auto-subscribes this conn as controller.█send(█&mut conn,█KIND_SPAWN,█serde_json::to_value(echo_spawn_req()).unwrap(),█);█let sid = loop {█let f = read_frame(&mut conn).expect(\"frame before spawned\");█if f.kind == KIND_SPAWNED {█break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;█}█};█// The production dedup-below + snap-above filter - the EXACT discipline brain.rs█// `read_event` applies in resume mode: drop `seq < cursor` (a deduped█// at-least-once boundary re-send), accept `seq >= cursor` and advance█// `cursor = seq + 1`. As it accepts, it asserts the accepted seqs never go█...█}█[Range 1250-1640 is beyond end of file (1219 lines total); skipped]\u000f█</out>█# Tool call ¶█//Finding broker connection lifecycle tests█grep(pattern=\"fn .*?(disconnect|detach|unsubscribe|controller|viewer|reap|epoch|writer|attach).*\\\\(\", path=\"crates/spt-daemon/src/broker.rs;crates/spt-daemon/tests/broker.rs;crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/control_stamp_lifetime.rs\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/█## src/█### broker.rs█108|// [impl->REQ-HAZARD-VIEWER-ISOLATION]█*109|fn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {█110| r.is_err()█111|}█112|█...█859| /// [`resolve_subscribe`] reads this.█*860| fn controller_by(&self) -> Option<&str> {█861| self.controller.as_ref().and_then(|c| c.by.as_deref())█862| }█863|█...█866| /// the log lock, then converged onto the perch OFF the lock.█*867| fn has_controller(&self) -> bool {█868| self.controller.is_some()█869| }█870|█...█872| /// for the convergence re-assert.█*873| fn live_viewer_count(&self) -> u32 {█874| self.viewers.len() as u32█875| }█876|█...█890| /// controller's identity.█*891| fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {█892| // Drop the prior controller sink first (its writer's live loop ends when█893| // tx drops), and bump the generation so (a) an in-flight deadline-evict█894| // for the old controller can't unseat this one and (b) a prior writer█...█960| /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).█*961| fn clear_contro [...6759ch elided...]█1317| let (_broker, name, _spawner, sid) = labeled_session(\"cv-gate7-combined\");█1318|█1319| // Controller on a pump conn (so a bounded read is possible).█...█1477|#[test]█*1478|fn re_serve_resets_resume_cursor_after_a_pre_attached_consume() {█1479| init_home();█1480| let dir = tempfile::tempdir().expect(\"tempdir\");█1481| let name = unique_name();█### control_stamp_lifetime.rs█134|#[test]█*135|fn reap_clears_control_and_viewer_stamps() {█136| init_stamp_home();█137| let node = \"PEERNODEHEX_CTLSTAMP\";█138| let id = format!(\"ctlstamp-{}\", std::process::id());█...█345|#[test]█*346|fn control_reap_clears_sticky_stamps_no_session_online_and_boot_sweep() {█347| init_stamp_home();█348| let node = \"PEERNODEHEX_B3\";█349| let pid = std::process::id();\u000f█</out>█# Assistant ¶█_**Planning detailed controller viewer tests**_█# Tool call ¶█//Reading controller viewer integration█read(path=\"crates/spt-daemon/tests/attach.rs\", selector=\"1040-1455\")█<out>█\u000euse spt_daemon::msg::SubscribeOutcome;█/// Spawn a LABELED echo session on a fresh broker; return (broker, name, brain,█/// sid). The spawner is the broker's by=None local controller placeholder.█fn labeled_session(label: &str) -> (Arc<Broker>, String, Brain, u64) {█init_home();█let dir = HOME.get().expect(\"home\").path().to_path_buf();█let name = unique_name();█let broker = net_broker(&name, &dir.join(label));█let mut spawner = connect_retry(&name);█let sid = spawner█.spawn_session(SpawnReq {█endpoint: label.to_string(),█..echo_spawn_req()█})█.expect(\"spawn labeled echo\");█(broker, name, spawner, sid)█}█/// Read events until the broker's `Subscribed` outcome arrives (skipping any█/// replayed output / size frames), returning the resolved role.█fn read_outcome(brain: &mut Brain) -> SubscribeOutcome {█for _ in 0..2000 {█match brain.read_event().expect(\"event\") {█BrokerEvent::Subscribed { outcome, .. } => return outcome,█_ => continue,█}█}█panic!(\"no Subscribed outcome\");█}█// [int->REQ-RCVIEW-1] [int->REQ-KICK-1] The controller/viewer/take matrix and the█[...19641ch elided...] l resume bookkeeping matches the snapshot (generation bumped, the█// session carried, the cursor rehydrated) alongside the resume subscribe.█assert_eq!(succ.session_id(), Some(sid), \"the carried session id is set\");█assert_eq!(█succ.next_seq(),█prior_next_seq,█\"the legacy resume cursor is rehydrated from the snapshot\"█);█assert_eq!(█succ.generation(),█prior_generation + 1,█\"handoff bumps the generation (a logic handoff, not a second reader)\"█);█// Cleanup: reap the echo child (the session id is carried on `succ`).█let mut succ = succ;█let _ = succ.kill_session();█drop(succ);█let _ = &broker; // hold the broker for the whole test, then drop█}█[Showing lines 1039-1458 of 1550. Use :1459 to continue]\u000f█</out>█# Assistant ¶█_**Analyzing refresh stall causes on PTYs**█**Investigating brain dispatch connection epochs**_█# Tool call ¶█//Tracing dispatch stream workers█grep(pattern=\"run_dispatch_loop|net_streams|serve_attach|stream_id|active|worker|subscribe|epoch|generation|connection\", path=\"crates/spt-daemon/src/dispatch.rs\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e 9|//! table for **peer-initiated** streams, classifies each by its first NDJSON█*10|//! record, and hands it to the right serve loop on its own worker thread.█11|//!█12|//! ## Direction (the gotcha-#9 fact, now first-class)█13|//!█...█43|//!█*44|//! ## Worker model█45|//!█*46|//! Each claimed stream gets its own worker thread with its **own** brain IPC█*47|//! connection (the broker is multi-client by design; the probe-brain pattern█*48|//! every E2E test uses). The peek and the serve use **separate** connections:█*49|//! the peek brain subscribes from 0 to read the first line and is dropped;█*50|//! the serve loop then subscribes from 0 on a fresh connection so it owns its█51|//! stream's event feed alone (no double-subscription interleave). Origin is█52|//! the table's `remote_id_hex` - handshake-proven, never payload█53|//! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.█...█74|█*75|use crate::attach::serve_attach;█76|use crate::brain::{Brain, BrokerEvent};█77|use crate::notifsync::{apply_notif_feed, NotifPolicy};█78|use crate::propagate::serve_update;█...█85|/// Poll cadence for the stream-table watch (the dispatcher's only busy-wait;█*8 [...11522ch elided...] true,█...█673| }█*674| Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {█675| return DispatchOutcome::Served(format!(\"wan feed: {handled} record(s)\"));█676| }█677| Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),█...█693| if let Err(e) =█*694| run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)█695| {█696| eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");█697| }█...█736| let reg = json!({\"subnet\": \"home\", \"endpoint_id\": \"ling\",█*737| \"instance\": {\"node\": \"n1\", \"status\": \"Active\", \"epoch\": 4}});█738| assert_eq!(classify_first_line(&reg), StreamFamily::Registry);█739| }█740|\u000f█</out>█# Tool call ¶█//Tracing net stream lifecycle█grep(pattern=\"struct .*Stream|stream_id|subscribe|unsubscribe|eof|retentive|epoch|generation|initiated_locally|remove\\\\(|send_stream|open_stream|recv\", path=\"crates/spt-daemon/src/nethost.rs\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e 37|use spt_net::net::endpoint::{█*38| BindScope, Connection, EndpointAddr, LocalDiscovery, NetEndpoint, RecvStream, RelayPolicy,█39| SendStream, SPT_NET_ALPN,█40|};█41|use spt_net::net::mesh::seedproof::ProofRole;█...█48|use crate::msg::{█*49| net_presence_event_envelope, net_stream_data_envelope, net_stream_eof_envelope,█50| NetPresenceEvent, NetStreamInfo, PRESENCE_CONNECTED, PRESENCE_DIAL_FAILED, PRESENCE_DISCONNECTED,█51|};█52|█...█58|/// Broker-side QUIC-op deadline (REQ-HAZARD-BROKER-QUIC-DEADLINE), in ms. Each█*59|/// brain-waiting QUIC await - `dial` / `open_stream` / `send_stream` - is bounded█60|/// by this so a black-holed or dead peer fails PROMPTLY with an ORDINARY error█61|/// the broker replies, never an unbounded await that wedges the single-threaded█62|/// pump (the B-half of REQ-HAZARD-PUMP-IPC-DEADLINE; the 2.2h hfenduleam wedge).█...█108|/// the PTY `OutputLog`: large enough that a brain-restart window never evicts█*109|/// unreplayed chunks in practice; bounded so an unsubscribed stream can't█110|/// buffer without limit.█111|const DEFAULT_STREAM_RING_CHUNKS: usize = 4096;█112|█...█129| /// A broker-minted in-process loopback (no [...31052ch elided...] \");█1795| sent.fetch_add(1, AtOrd::Relaxed);█1796| }█*1797| host.send_stream(owner, &[], true).expect(\"finish\");█1798| })█1799| };█1800|█...█1814| host.ring_len(shell).unwrap() <= CAP,█*1815| \"the retentive ring never exceeds the backpressure cap (bounded)\"█1816| );█1817| assert!(█1818| sent.load(AtOrd::Relaxed) < N,█...█1835| writer.join().expect(\"writer joins\");█*1836| assert!(finished, \"the tunnel saw EOF after the owner finished\");█1837| let expected: Vec<u8> = frames.concat();█1838| assert_eq!(got.len(), expected.len(), \"no byte lost or duplicated\");█1839| assert_eq!(got, expected, \"byte-exact, in order, under backpressure\");\u000f█</out>█# Tool call ¶█//Finding dispatch lifecycle tests█grep(pattern=\"restart|resume|stream|worker|brain|serve_attach|detach|reconnect|generation|epoch\", path=\"crates/spt-daemon/tests;crates/spt/tests\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/tests/█## access.rs█4|//! reply (stateful firewall), and admits a whitelisted node - with edits to█*5|//! the store taking effect mid-stream (the gate re-reads per check).█6|//!█7|//! Two in-process brokers (hermetic loopback endpoints) play the two daemons,█8|//! the same harness as `wanmsg.rs`. The payload-origin spoof negative lives█...█18|use spt_daemon::access::note_outbound;█*19|use spt_daemon::brain::{Brain, BrokerEvent};█20|use spt_daemon::effect::{Minter, MintedOp};█21|use spt_daemon::nethost::{NetConfig, NetHost};█22|use spt_daemon::registryhost::RegistryHost;█...█73|█*74|fn connect_retry(name: &str) -> Brain {█75| for _ in 0..300 {█*76| if let Ok(b) = Brain::cold_start(name, 1) {█77| return b;█78| }█79| thread::sleep(Duration::from_millis(10));█80| }█*81| panic!(\"brain could not connect\");█82|}█83|█*84|fn wait_for_stream(brain: &mut Brain) -> (u64, String) {█85| for _ in 0..400 {█*86| let reply = brain.net_streams().expect(\"net-streams\");█*87| if let Some(s) = reply.streams.first() {█*88| return (s.stream_id, s.remote_id_hex.clone());█89| }█90| thread [...48959ch elided...] is_resume fallback when█*42|/// no `[session.resume]`) spawns the long-lived quiet harness, so█43|/// `launch_harness_brokered_in` has a real program to run.█*44|fn register_resume_adapter(name: &str) {█45| let toml = format!(█46| \"[adapter]\\nname=\\\"{name}\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\█47| min_spt_core_version=\\\"0\\\"\\n\\n\\█...█53| spt_runtime::registry::register(&spt_store::perch::adapters_dir(), &src, 1000)█*54| .expect(\"register the resume adapter\");█55|}█56|█57|/// Seed a previously-ONLINE, spt-hosted (controllable), relay-less endpoint with a█*58|/// ledger row to resume from - the daemon-restart-orphaned shape.█59|fn seed_orphaned_online_endpoint(id: &str, adapter: &str) {█[Showing lines 1-1080 of 1452 (49.9KB limit). Use :1081 to continue]\u000f█</out>█# Assistant ¶█_**Assessing connection ID lifecycle**_█# Tool call ¶█//Reading dispatch ownership model█read(path=\"crates/spt-daemon/src/dispatch.rs\", selector=\"1-70,188-235,255-405\")█<out>█\u000e//! Inbound net-stream dispatch (D9-1) - the daemon-side acceptor that turns█//! the hand-wired serve loops of D5-D8 into production behavior.█//!█//! Every E2E test so far drove the serve half manually (a probe brain calling█//! `serve_sync` / `serve_update` / `apply_notif_feed` / `receive_wan` against█//! a known stream). In production nothing did - a peer could dial and write█//! and the bytes sat in the broker's stream log unread. This module is the█//! missing consumer: a daemon-hosted loop that watches the broker's stream█//! table for **peer-initiated** streams, classifies each by its first NDJSON█//! record, and hands it to the right serve loop on its own worker thread.█//!█//! ## Direction (the gotcha-#9 fact, now first-class)█//!█//! The stream table holds locally-opened outbound streams too. The dispatcher█//! claims only rows with `initiated_locally == false` - serving our own█//! requester stream would deadlock the requester against ourselves. The flag█//! is broker-recorded at registration (`open_stream` vs the accept loop),█//! never inferred.█//!█//! ## Classification (first-record demux)█//!█//! Streams are single-purpose; the opener's first record names the protoc [...10204ch elided...] :Failed(e.to_string()),█},█None => {█// Refuse politely: finish our send side, claim stays.█let _ = brain.net_stream_send(stream_id, &[], None, true);█DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())█}█}█}█StreamFamily::Xfer => {█// The wire names whose whitelist gates (the rule set); the origin█// it is checked against stays handshake-proven. Blank refuses.█let endpoint = line█.get(\"endpoint\")█.and_then(|v| v.as_str())█.filter(|e| !e.is_empty())█.map(str::to_string);█match endpoint {█...█}█}█...█}█}\u000f█</out>█# Tool call ¶█//Reading stream subscriber ownership█read(path=\"crates/spt-daemon/src/nethost.rs\", selector=\"151-295,448-705,1380-1533\")█<out>█\u000e/// discipline applied to the network (D4b): the stream's single read pump is█/// the only producer; a brain connection attaches as the subscriber; every█/// subscriber send happens under the enclosing `Mutex`, so live chunks and a█/// replay can never interleave out of `seq` order. Chunks produced while no█/// brain is attached sit in the ring and replay on the next subscribe - a█/// brain restart loses nothing (Spike #3 invariant B/D).█// [impl->REQ-DAEMON-2]█struct StreamLog {█stream_id: u64,█/// TRANSPORT read counter: bumped once per [`append`](Self::append) (one█/// read-pump `read_chunk`), INDEPENDENT of byte size. NOT a logical-chunk█/// count - QUIC may coalesce several sender writes into one receiver█/// `read_chunk` (or split one), so `next_seq` can be < the number of logical█/// chunks the sender issued for the SAME bytes. Correctness (byte-exact +█/// exactly-once) holds via these transport seqs + the recv cursor regardless;█/// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows█/// coalescing flake) and must gate on BYTES instead.█next_seq: u64,█ring: VecDeque<(u64, Vec<u8>)>,█cap_chunks: usize,█[...21662ch elided...] t e = streams.get(&stream_id).ok_or_else(|| {█io::Error::new(█io::ErrorKind::NotFound,█format!(\"no such stream {stream_id}\"),█)█})?;█Arc::clone(&e.log)█};█log.lock().unwrap().attach(sub, from_seq);█Ok(())█}█/// Detach `sub` from a stream iff it is the current subscriber (brain█/// disconnect cleanup; the stream + its ring persist).█pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {█let log = {█let streams = self.shared.streams.lock().unwrap();█streams.get(&stream_id).map(|e| Arc::clone(&e.log))█};█if let Some(log) = log {█log.lock().unwrap().detach_if(sub);█}█}\u000f█</out>█# Tool call ¶█//Reading broker connection identifiers█read(path=\"crates/spt-daemon/src/conn.rs\", selector=\"1-285\")█<out>█\u000e//! Bounded, cancelable, poison-on-failure framed writes for one physical█//! broker connection (UPDATE-WEDGE round-4).█//!█//! ## The failure class this kills█//!█//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard█//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes█//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` - a local-socket█//! path with **no supported write timeout** (`set_timeout` -> `Unsupported`).█//! A connected consumer that stops reading (an `rc --take` whose process█//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the█//! logical stall-evict (`stall_evict_controller`) releases only the controller█//! ROLE - it never cancels the in-flight pipe write or closes the physical█//! connection, so the writer (and every write queued behind the same gate)█//! stays wedged until the peer process dies (~128 s in the field capture, on a█//! brain restart).█//!█//! ## The primitive█//!█//! [`BrokerConn`] owns the send half outright - it never leaves this module,█//! so an unbounded bypass write cannot compile. Every write:█//!█//! 1. acquires the serialized write GATE with a bounded wait (an [...11642ch elided...] BAND (both directions -█/// the halves share one OS object) and latch the poison. Idempotent and█/// safe to repeat: `CancelIoEx` on a handle with nothing pending is a█/// no-op, and re-cancel closes the race where a write was issued a beat█/// AFTER a cancel (cancellation only reaches ALREADY-issued ops).█/// `cause` (the organic caller's write error) rides the retirement record█/// when the retirement is NOT deadline-driven.█fn poison_and_cancel(&self, cause: Option<&io::Error>) {█if !self.poisoned.swap(true, Ordering::AcqRel) {█// First poison of this conn: loud, once. The token is class-split█// (F-039 leg a): deadline -> CONN_WRITE_POISONED (the wedge█...█}█...█}█...█}█[Showing lines 1-288 of 815. Use :289 to continue]\u000f█</out>█# Tool call ¶█//Locating conn IDs lifecycle█grep(pattern=\"NEXT_CONN|conn_id|alloc.*conn|BrokerConn::|ConnId|register.*conn|detach_stream_sub|handle_conn\\\\(\", path=\"crates/spt-daemon/src\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## attach.rs█554| brain: &mut Brain,█*555| conn_id: u64,█556| session_id: u64,█557| from_seq: u64,█558| open_op: MintedOp,█...█560|) -> io::Result<u64> {█*561| request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)█562|}█563|█564|/// Like [`request_attach`], but for the cross-node leg (#4,█...█573| brain: &mut Brain,█*574| conn_id: u64,█575| session_id: u64,█576| from_seq: u64,█577| open_op: MintedOp,█...█580|) -> io::Result<u64> {█*581| let opened = brain.net_open_stream(conn_id, Some(open_op))?;█582| let line = ndjson::encode_line(&AttachRecord::Request {█583| session_id,█584| from_seq,█## brain.rs█1372| &mut self,█*1373| conn_id: u64,█1374| op: Option<MintedOp>,█1375| ) -> io::Result<NetStreamOpened> {█1376| self.send(█...█1378| serde_json::to_value(NetStreamOpenReq {█*1379| conn_id,█1380| op_id: op.map(|o| o.seq),█1381| minter: op.map(|o| o.minter),█1382| })█## broker.rs█2751| thread::spawn(move || {█*2752| let _ = me.handle_ [...23578ch elided...] n: None,█1503| remote_addr: serde_json::Value::Null,█...█1580| kind: PRESENCE_CONNECTED.to_string(),█*1581| conn_id: 1,█1582| remote_id_hex: \"aa\".into(),█1583| reason: None,█1584| remote_addr: serde_json::Value::Null,█### sync.rs█106| let open_op = io.open_op()?;█*107| let report = request_sync(&mut *io.brain, io.conn_id, &want, open_op, &cs, &self.scratch)?;█108| (self.hooks.on_pull)(peer_hex, &report);█109| }█110| Ok(())█### update.rs█114| &mut *io.brain,█*115| io.conn_id,█116| open_op,█117| &BrokerAbi::current(),█118| &policy,█Showing files 1-20 of 22+. Use skip=20 for the next page, or narrow paths/pattern.\u000f█</out>█# Tool call ¶█//Finding dispatch restart test█grep(pattern=\"run_dispatch_loop\\\\(|claimed|restart.*dispatch|dispatch.*restart|re-serve.*stream|SAME stream\", path=\"crates/spt-daemon/tests;crates/spt/tests;crates/spt-daemon/src\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/█### tests/█#### attach.rs█772|// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain█*773|// crash mid-attach: the successor re-serves the SAME broker-held stream from█774|// seq 0 (worst-case stale cursor) - the replayed Request/input/output all█775|// dedup through the journals, input B sent INTO the dead window arrives, and█776|// B's viewport holds every marker exactly once.█...█854|█*855| // Life 2: handoff, then re-serve the SAME stream from seq 0 - the█856| // worst-case replay. serve_attach re-handles the replayed Request (gate +█857| // re-attach from 0, so every output record re-transmits - B's render█858| // cursor drops the duplicates), replays MARKER_ONE's input (PTY-write█...█1126|█*1127| // The \"successor\" re-serves the SAME operator's stream (same by) on a new█1128| // connection - the brain-restart re-take.█1129| let mut successor = connect_retry(&name);█1130| successor.attach_as(sid, 0, AttachIntent::Control, Some(\"node-A\")).expect(\"successor control\");█#### dispatch.rs█108| thread::spawn(move || {█*109| let _ = run_dispatch_loop(&name, &paths, registry, &flag, Duratio [...10666ch elided...] th█*274| // claimed it.█275| match spool::wan_seen_at(&perch_path, &msg.op_id) {█276| Ok(true) => return WanOutcome::Duplicate,█277| Ok(false) => {}█...█353| // back to the matching request outcome, and an unknown/garbled token is█*354| // treated as NoReply (unconfirmed - never optimistically claimed delivered).█355| #[test]█356| fn wan_outcome_token_round_trips_and_unknown_is_noreply() {█357| let pairs = [█## spt/tests/█### idle_edge_drain_e2e.rs█227|█*228| // -- (8) The spooled deferred row must be DELIVERED (claimed by the idle-edge█229| // take + injected via the shared leg) -> the spool clears. Poll briefly in█230| // case the inject worker's commit lags the CLI return. --█231| let mut pending_after = pending_before;\u000f█</out>█# Assistant ¶█_**Identifying missing dispatcher restart test**_█# Tool call ¶█//Reading brain refresh startup sequence█read(path=\"crates/spt-daemon/src/brainproc.rs\", selector=\"175-288\")█<out>█\u000e // *counter* is rehydrated, never the wall-clock stamp.█eprintln!(█\"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",█std::process::id(),█reason.as_arg(),█);█// Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild█// continuity from the broker - re-attach EVERY hosted session in resume mode█// from its delivered cursor, the production replacement for the retired█// brain->brain `BrainState` handoff. Today the supervised daemon brain hosts no█// PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),█// so this is a no-op now and forward-correct when daemon-hosted sessions land█// (the live-agent adapter). A failure here is non-fatal - the brain still█// serves; the next heartbeat probes broker liveness. NOTE: this is the PTY█// message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the█// run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`█// below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).█// CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO- [...5385ch elided...] ker-side writer BLOCKS, never█// EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial█// window, so without this the wedge never drains: the promotion DRAINED gate█// (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never█// drives the evict) stays false for the full trial -> \"alive but never ready\"█// rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery█// (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on█// BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);█// best-effort - a transient sessions error must never fell the heartbeat (the█...█}█[Showing lines 174-291 of 2116. Use :292 to continue]\u000f█</out>█# Assistant ¶█_**Clarifying dispatcher and rc process roles**█**Investigating subscription cleanup race conditions**_█# Tool call ¶█//Reading broker disconnect cleanup exact█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"2970-3220\")█<out>█\u000e pub fn session_output_seq(&self, id: u64) -> Option<u64> {█self.sessions█.lock()█.unwrap()█.get(&id)█.map(|h| recover_log(&h.log).next_seq)█}█/// Drive one brain connection: handshake, then dispatch its command frames█/// until it disconnects. On disconnect, detach this connection from any█/// sessions it subscribed to - the sessions themselves persist (B2: a brain█/// restart must not disturb the hosted children).█fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {█// RAII handler tally (panic-safe): the round-4 gate's seam for \"the█// poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).█struct HandlerTally<'a>(&'a AtomicUsize);█impl Drop for HandlerTally<'_> {█fn drop(&mut self) {█self.0.fetch_sub(1, Ordering::AcqRel);█}█}█self.conn_handlers.fetch_add(1, Ordering::AcqRel);█let _tally = HandlerTally(&self.conn_handlers);█let (mut recv, send) = conn.split();█// Round-4: the send half is owned by the bounded/cancelable conn█// primitive from birth [...10239ch elided...] ream_subs {█host.detach_stream_sub(*id, &send);█}█// And presence: the liveness log + its ring persist (D4c).█if my_presence_sub {█host.detach_presence_sub(&send);█}█}█Ok(())█}█/// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning█/// connection, start its output drain and exit waiter, and reply `spawned`.█/// Every hosted session gets its **own** drain + exit-waiter threads (KH█/// 7.4): a hung child blocks only its own threads - shell instances ride█/// exactly this isolation (one owner's hung shell binary stalls neither█/// another owner's delivery nor the broker's control dispatch).█[Showing lines 2969-3223 of 6009. Use :3224 to continue]\u000f█</out>█# User ¶█Your current interruptible wait was interrupted because an IRC message arrived from your parent agent `Main`.█Parent IRC message:█Leading field/log hypothesis: controller conns are broker role=`brain stream-subscriber`; refresh closes old brain connection while hosted PTYs survive, and new brain only logs `BRAIN_RESUMED` session cursors-not active remote control stream subscriptions. Existing remote `spt rc` frontend then hangs until detach+reattach opens a new stream. Please confirm/refute exact call chain and explain partial 4/5 observation.█# Assistant ¶█_**Clarifying brain cursor and stream handling**█**Analyzing controller connection cleanup and attachment**_█# Tool call ¶█//Reading serve attach state machine█read(path=\"crates/spt-daemon/src/attach.rs\", selector=\"136-550\")█<out>█\u000e█/// Read one [`BrokerEvent`] for an attached CONTROLLER, RESUMING-FROM-FLOOR on a█/// b4 drop-don't-block output-gap instead of fataling.█///█/// b4 made the controller a non-blocking `try_send` that DROPS frames when its█/// bounded channel is full (a controller that falls behind its own echo under a█/// hard flood). The next frame this reader sees is then a FORWARD gap that the█/// strict reject-gap (`read_event`, legacy single-session path) FATALS. A█/// controller CANNOT snap-above like a viewer (B2: it is authoritative - it█/// advances `delivered_through`; skipping rolled frames would be a not-exactly-once█/// resume). So instead it RE-FETCHES: re-subscribe from█/// [`Brain::controller_resume_floor`] (`= delivered_through`, frozen at the drop by█/// `contiguous_advance`) and the broker replays the dropped frames from the ring.█///█/// Exactly-once holds IFF the ring still retains the floor (`tail - delivered_through█/// <= cap_chunks`). If it does not - the controller fell behind further than the█/// ring holds - the re-subscribe's first frame is STILL a forward gap (floor█/// unchanged across two resumes). That is [`ControllerIrrecoverablyBehind`]: the█/// caller surfaces i [...22106ch elided...] // Applied/NetSent acks and unrelated events: not ours to handle.█_ => {}█}█}█}█/// Operator-side: open an attach stream on a dialed connection and send the█/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes█/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes█/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the█/// durable id for the exactly-once stream open (safe to block for its ack -█/// no attach traffic can be in flight before the stream exists); the Request█/// send itself is unjournaled (see module docs - a replayed Request re-gates█/// and re-attaches idempotently).█// [impl->REQ-INST-8]█pub fn request_attach(█...█) -> io::Result<u64> {█...█}█[Showing lines 135-553 of 627. Use :554 to continue]\u000f█</out>█# Assistant ¶█_**Identifying stream log overflow issue**█**Analyzing request eviction in long sessions**_█# Tool call ¶█//Tracing net stream cursor gaps█grep(pattern=\"NetStreamData|net_stream_cursors|stream_cursors|net.*gap|from_seq|cursor.*stream|KIND_NET_STREAM_DATA\", path=\"crates/spt-daemon/src/brain.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 41| KillReq, NetDialReq, NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent,█*42| NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,█43| NetStreamSubscribeReq, NetStreamsReply, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,█44| SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,█45| ViewerEvictedEvent,█...█51| KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,█*52| KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED,█53| KIND_MET_MEMBER, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET,█54| KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,█55| KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,█...█159| /// A contiguous chunk of a stream's reads (the cursor has advanced past it).█*160| NetStreamData {█161| stream_id: u64,█162| seq: u64,█163| bytes: Vec<u8>,█...█269| ne [...13699ch elided...] 17| /// attach floor) would instead be ACCEPTED below the intended floor. The█2118| /// primary assertions pin the floor at FROM=100 (not 0) and the█2119| /// accept-and-advance above it.█...█2129| // session_cursors[sid]=FROM (snap-above armed at the requested floor, NOT 0)█*2130| // and next_seq=FROM, and writes a subscribe(from_seq=FROM) the stub drains.█2131| brain█2132| .attach_as_viewer_snap(sid, FROM, Some(\"operator-node\"))█2133| .expect(\"attach_as_viewer_snap\");█...█2136| Some(FROM),█*2137| \"attach_as_viewer_snap arms the snap-above cursor at the requested floor (from_seq), not 0\"█2138| );█2139|█2140| // The ring rolled between reads: the next Output frame carries a seq far\u000f█</out>█# Tool call ¶█//Reading net stream gap behavior█read(path=\"crates/spt-daemon/src/brain.rs\", selector=\"795-842\")█<out>█\u000e KIND_SESSIONS_REPLY => {█let ev: SessionsReply = serde_json::from_value(env.payload)█.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;█return Ok(BrokerEvent::Sessions(ev));█}█KIND_PAIR_JOINED => {█let ev: PairJoinReply = serde_json::from_value(env.payload)█.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;█return Ok(BrokerEvent::PairJoined(ev));█}█KIND_MET_MEMBER => {█let ev: MetMember = serde_json::from_value(env.payload)█.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;█return Ok(BrokerEvent::MetMember(ev));█}█KIND_NET_STREAM_DATA => {█let ev: NetStreamData = serde_json::from_value(env.payload)█.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;█// Same exactly-once discipline as PTY output, per stream:█// accept the contiguous next seq, drop a replay dup [...999ch elided...] seq: ev.seq,█bytes,█});█}█KIND_NET_STREAM_EOF => {█let ev: NetStreamEof = serde_json::from_value(env.payload)█.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;█return Ok(BrokerEvent::NetStreamEof {█stream_id: ev.stream_id,█next_seq: ev.next_seq,█});█}█KIND_NET_PRESENCE_EVENT => {█let ev: NetPresenceEvent = serde_json::from_value(env.payload)█.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;█...█}█[Showing lines 794-845 of 2178. Use :846 to continue]\u000f█</out>█# Assistant ¶█_**Identifying sequence gap causing dispatcher freeze**_█# Tool call ¶█//Reporting exact refresh chain█irc(op=\"send\", to=\"Main\", message=\"Refute global omission: `run_brain` cursor-only `resume_session_cursors` is PTY-session bookkeeping, but after first 500ms `net_status` it calls `spawn_net_consumers`; fresh `run_dispatch_loop` has empty `claimed`, enumerates every broker-held peer stream, and `worker` does `peek_first_line(stream, [...773ch elided...] imary brain calls `net_stream_subscribe` then PTY `attach_as`, so label becomes `role=brain stream-subscriber stream=X controller session=Y`; separate `wire` Brain sends output. More detail in final.\", replyTo=null, await=false, from=null, timeoutMs=null, peek=null)█<out>█\u000eDelivered to 1 peer(s):█- Main: injected\u000f█</out>█# Assistant ¶█_**Formulating eviction and connection hypotheses**█**Analyzing controller channel and stream claim issues**█**Identifying stale attach controller race condition**█**Tracing active worker stream takeover**█**Identifying frozen stream cause and missing removals**_█# Tool call ¶█//Finding stale stream cleanup tests█grep(pattern=\"finished.*stream|stream.*finished|old stream|stale stream|remove.*stream|stream_infos\\\\(\\\\).*len|streams\\\\.len|peer rows\", path=\"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/tests;crates/spt/tests\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/█## src/█### nethost.rs█256| /// Attach `sub` as the subscriber, replaying every retained chunk with█*257| /// `seq >= from_seq` (then eof, if already finished) before live streaming.█258| fn attach(&mut self, sub: SharedSend, from_seq: u64) {█259| // Attribution (F-039 legs b+d): stream-subscriber role fact + attach█260| // record - this is one of the two RCA-pinned shared-carrier classes.█...█1442| /// `(bytes, finished)`; `finished` => the read side ended (the poller closes).█*1443| /// `None` => no such stream. Empty bytes + `finished == false` => nothing pending.█1444| // [impl->REQ-SHELL-4]█1445| pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {█1446| let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);█...█1698| let infos = host.stream_infos();█*1699| assert_eq!(infos.len(), 2, \"operator + peer rows\");█1700| let op = infos█1701| .iter()█1702| .find(|s| s.stream_id == op_stream)█## tests/█### netstream.rs█195| let streams = receiver.net_streams().expect(\"net-streams\").streams;█*196| assert_eq!(streams.len(), 1, \"the stream survived the brain restart\");█197| assert_eq!(streams[0].stream_id, b_stream);█198| receiver█199| .net_stream_subscribe(b_stream, cursor)█### notifsync.rs█74|/// a receiver that previously dialed out must exclude its own ids or it reads█*75|/// a stale stream and hangs (the D5b/D6c lesson - wait_for_stream_except).█76|fn wait_for_stream_except(brain: &mut Brain, known: &[u64]) -> u64 {█77| for _ in 0..400 {█78| let reply = brain.net_streams().expect(\"net-streams\");\u000f█</out>█# Tool call ¶█//Locating same origin resolver█grep(pattern=\"fn resolve_subscribe|Same|same.*origin|Control.*controller_by|BusyControlled|Take\", path=\"crates/spt-daemon/src/broker.rs\", selector=\"1800-1900,3440-3508\", case=false, gitignore=true, skip=null)█<out>█\u000e 1806|/// Drain the inject floor's buffered controller input to the PTY **after** an█*1807|/// injected sequence, then release the floor. Each step takes the buffered bytes█1808|/// OFF the lock before ENQUEUEING it to the lone input-writer (the W1 rule: never█1809|/// hold a lock across a PTY write - now never block on it either); input arriving█1810|/// mid-flush keeps buffering, so strict order is preserved until the buffer█...█1837|/// broker serving. Safe for the SHORT map ops it guards (`sessions` / `pair_holds`█*1838|/// inserts+removes stay coherent on recovery - the same argument as the floor). The█1839|/// per-session `OutputLog` RING needs the extra coherence clamp of [`recover_log`].█1840|/// CLASS INVARIANT (KNOWN-HAZARDS): NO bare `.lock().unwrap()` on a broker-resident█1841|/// lock reachable from serve/dispatch - use `recover`/`recover_log` or a documented█...█1871| loop {█*1872| let chunk = lock_floor(floor).take_or_release();█1873| match chunk {█1874| Some(bytes) => {█1875| input.enqueue(bytes);█...█3443| /// exactly-once resume. The `intent` + `by` select the role; the broker█*3444| /// resolves it (con [...391ch elided...] d: &SharedSend) -> Result<u64, String> {█3450| let req: SubscribeReq = serde_json::from_value(env.payload)█...█3455| // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child█*3456| // fast-detect MUST NOT live here - `try_wait` locks the same child mutex█3457| // the per-session exit-waiter holds for the WHOLE life of a live child█3458| // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live█3459| // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter█...█3506| /// With an `op_id` the write is routed through the broker-owned█*3507| /// [`EffectJournal`] for exactly-once delivery: a replay of the same█3508| /// `(session_id, op_id)` after a brain crash is deduped (the PTY is not\u000f█</out>█# Tool call ¶█//Reading resolver and unsubscribe█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"1800-1885,3440-3510\")█<out>█\u000efn input_writer(session: Arc<PtySession>, rx: Receiver<InputRecord>) {█while let Ok(bytes) = rx.recv() {█let _ = session.write_input(&bytes);█}█}█/// Drain the inject floor's buffered controller input to the PTY **after** an█/// injected sequence, then release the floor. Each step takes the buffered bytes█/// OFF the lock before ENQUEUEING it to the lone input-writer (the W1 rule: never█/// hold a lock across a PTY write - now never block on it either); input arriving█/// mid-flush keeps buffering, so strict order is preserved until the buffer█/// drains empty and the floor frees.█/// Lock the per-session inject floor, RECOVERING a poisoned mutex instead of█/// panicking (B6, REQ-HAZARD-INJECT-WORKER-POISON). The floor is SHARED by the█/// inject worker (`run_inject_worker` open / `flush_inject_floor`) and the█/// controller-input path (`dispatch_input` Layer C `buffer_if_held`). A panic under█/// the lock on EITHER poisons it; a bare `.lock().unwrap()` at the next site then█/// panics too - and a panic in the inject WORKER kills its thread WITHOUT reaping█/// the translation child, ORPHANING a live binary while `event_tx.send` fails, so█/// force-native delivery r [...6433ch elided...] self.sessions);█match sessions.get(&req.session_id) {█Some(h) => Arc::clone(&h.log),█None => {█eprintln!(\"DBG unsubscribe: no session {}\", req.session_id);█return Ok(());█}█}█};█recover_log(&log).detach_if(send);█Ok(())█}█/// Inject input bytes into a session's PTY.█///█/// With an `op_id` the write is routed through the broker-owned█/// [`EffectJournal`] for exactly-once delivery: a replay of the same█/// `(session_id, op_id)` after a brain crash is deduped (the PTY is not█/// written twice), and either outcome - fresh apply or dedup - is acked back█/// with an [`crate::msg::KIND_APPLIED`] frame so the brain can retire the\u000f█</out>█# Tool call ¶█//Reading stale writer epoch replacement█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"880-970,1245-1285,1500-1595\")█<out>█\u000e /// writer thread (symmetric with [`add_viewer`]) - it is no longer written█/// inline under the log lock. The writer thread advances the authoritative█/// `delivered_through` cursor as it writes (D4-1: only on a successful write,█/// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]█/// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`█/// additionally makes a stale deadline-evict a no-op AND stops a superseded█/// writer mid-INITIAL-BATCH (P1c - a writer still flushing its owned `initial`█/// batch ignores the dropped `tx`, so the epoch gate is what guarantees a█/// single live writer per connection and a monotonic on-wire seq stream,█/// [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]). Stamps `driven_by` to the new█/// controller's identity.█fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {█// Drop the prior controller sink first (its writer's live loop ends when█// tx drops), and bump the generation so (a) an in-flight deadline-evict█// for the old controller can't unseat this one and (b) a prior writer█// still flushing its [...9816ch elided...] via snap-above) plus its own TERMINAL█// control frames - notably the `Displaced` kick the displaced controller█// MUST still receive. The loop ends naturally when `tx` drops. Gating█// output here would suppress that `Displaced` (the cv-matrix loud-take█// hang). The reorder hazard is the INITIAL-BATCH replay, gated above.█mark_blocked(true);█let res = send.write(frame);█mark_blocked(false);█match res {█Ok(_) => {}█Err(e) => {█note_controller_write_retired(sid, send.id(), &e);█// Bounded writer-exit record (F-039 leg d).█// [impl->REQ-CONN-POISON-ATTRIBUTION]█send.lifecycle_event(█...█);█...█}█}█...█}█...█}\u000f█</out>█# Assistant ¶█_**Investigating resolve_subscribe lines**_█# Tool call ¶█//Locating subscribe resolver definition█grep(pattern=\"resolve_subscribe\", path=\"crates/spt-daemon/src/broker.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 858| /// or `None` when undriven. The `by`-keyed ownership logic in█*859| /// [`resolve_subscribe`] reads this.█860| fn controller_by(&self) -> Option<&str> {█861| self.controller.as_ref().and_then(|c| c.by.as_deref())█862| }█...█1147| /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.█*1148| fn resolve_subscribe(█1149| &mut self,█1150| sub: SharedSend,█1151| from_seq: u64,█...█3470| };█*3471| let outcome = recover_log(&log).resolve_subscribe(█3472| Arc::clone(send),█3473| req.from_seq,█3474| req.intent,█...█5855| let outcome =█*5856| log.resolve_subscribe(nsend, 0, AttachIntent::Control, Some(\"newcomer\".to_string()));█5857|█5858| assert_eq!(█5859| outcome,\u000f█</out>█# Tool call ¶█//Reading same origin takeover rules█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"1128-1208\")█<out>█\u000e fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {█...█let evicted = Arc::new(AtomicBool::new(false));█let writer_evicted = Arc::clone(&evicted);█let session_id = self.session_id;█let writer =█thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));█self.viewers.insert(█vid,█ViewerSink {█tx,█send: sub,█evicted,█_writer: writer,█},█);█}█/// Resolve a controller/viewer subscribe under the W2.5 model█/// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs█/// the controller take / viewer add / loud displace as a side effect. See the█/// identity matrix in the body - the `by`-keyed logic is what makes a█/// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.█fn resolve_subscribe(█&mut self,█sub: SharedSend,█from_seq: u64,█intent: AttachIntent,█by: Option<String>,█) -> SubscribeOutcome {█if intent == AttachIntent::Viewer {█if self.viewers.len() >= MAX_V [...2318ch elided...] ubscribeOutcome::Controller;█}█// A DIFFERENT remote controls it.█match intent {█AttachIntent::Take => {█// The ONLY loud-displace path: notify the incumbent, then take.█// W1: route the Displaced notice through the OLD controller's█// writer thread (best-effort, off the dispatch thread) - the█// incumbent we're kicking may be exactly the wedged one, so an█// inline write here would block the new attacher's dispatch.█if let Some(old) = &self.controller {█let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(\"\"));█...█}█...█}█...█}█}█[Showing lines 1127-1211 of 6009. Use :1212 to continue]\u000f█</out>█# Assistant ¶█_**Identifying stale row replay race as root cause**_█# Tool call ¶█//Reporting stronger stale stream root█irc(op=\"send\", to=\"Main\", message=\"Stronger exact root candidate found: `NetShared.streams` NEVER removes finished stream rows (`StreamLog::finish` only sets `finished=true`; no `streams.remove` exists). Every fresh dispatcher resets `claimed` and reclaims ALL historical peer attach streams concurrently. A finished old rc stream who [...701ch elided...] e streams over 4096 inbound chunks cannot be reclassified, `peek_first_line(0)` errors gap and one-shot `claimed` prevents retry. Both are safely falsifiable in one broker/dispatcher restart harness.\", replyTo=null, await=false, from=null, timeoutMs=null, peek=null)█<out>█\u000eDelivered to 1 peer(s):█- Main: injected\u000f█</out>█# Assistant ¶█_**Analyzing rc stream ordering and completion**█**Evaluating stale and active stream race conditions**█**Testing stream handling with dispatcher stop**█**Designing isolated refresh harness**█**Planning deterministic broker eviction test**_█# Tool call ¶█//Finding net ring cap seam█grep(pattern=\"ring_cap|with.*cap|start.*ring|DEFAULT_STREAM_RING_CHUNKS\", path=\"crates/spt-daemon/src/nethost.rs;crates/spt-daemon/tests\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/█## src/█### nethost.rs█110|/// buffer without limit.█*111|const DEFAULT_STREAM_RING_CHUNKS: usize = 4096;█112|█113|/// Max bytes pulled per stream read (one ring chunk).█114|const STREAM_READ_CHUNK: usize = 64 * 1024;█...█200|█*201| /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.█202| fn set_retentive(&mut self, cap: usize) {█203| self.retentive_cap = cap;█204| }█...█483| streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,█*484| ring_cap: usize,█485| /// The conn-liveness event log (D4c) - one per host, all conns interleaved.█486| presence: Mutex<PresenceLog>,█487|}█...█495| streams: Mutex::new(HashMap::new()),█*496| ring_cap: DEFAULT_STREAM_RING_CHUNKS,█*497| presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),█498| }█499| }█500|}█...█646| let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);█*647| let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));█648| let room = Arc::new(tokio::sync::Notify::new());█649| shared.streams.lock().unwrap().insert(█650| id,█...█1426|█*1427| /// Mark [...1127ch elided...] /! nextest (process-per-test). No spt.exe is spawned; the only child is the mock role.█### resume.rs█46|fn served(name: &str, dir: &std::path::Path, cap: usize) -> Arc<Broker> {█*47| let broker = Broker::bind_in_with_net_cap(name, dir.join(\"effects.log\"), None, cap)█48| .expect(\"bind broker\");█49| let serve = Arc::clone(&broker);█50| thread::spawn(move || {█...█226|// [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION] the post-eviction ring-floor clamp█*227|// (doyle D4-2 watch-item): with a tiny output-log ring cap, the dead-window█228|// output overruns the ring, evicting the chunks at the resume cursor. A█229|// re-attaching brain whose `resume_seq` is now BELOW the surviving ring floor█230|// must resume forward from the floor - the resume-mode dedup-below + snap-above\u000f█</out>█# Assistant ¶█_**Inspecting stream info and hazards**_█# Tool call ¶█//Reading PTY attach hazards█read(path=\"docs/KNOWN-HAZARDS.md\", selector=\"330-520\")█<out>█\u000e## 6. Documented regressions (non-obvious invariants)█...█### 6.7 Broker and brain MUST be separate processes (in-process collapse silently breaks no-endpoint-drop update) `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`█- **Failure:** the daemon hosts the broker as a background *thread* in the single `spt daemon` process (`daemon.rs:165-170`, `Arc<Broker>` + `thread::spawn(serve)`) instead of a separate process. A brain restart onto a swapped binary then cannot happen without killing the broker thread - closing every PTY, orphaning every harness child, dropping every socket. So `spt update apply` degrades to an in-process `Brain::handoff` no-op: the binary swaps on disk but the running daemon keeps executing the old code until an unrelated restart/logon. The no-endpoint-drop self-update pillar (REQ-UPD-3, ADR-0004) is silently unrealized. Observed live 2026-06-09: `enlyzeam` ran 0.3.0 with 0.3.2 on disk for ~a day, still reproducing the bug the update fixed.█...█- **Origin:** unintended spec/impl drift from ADR-0004 (the broker *process* was specced + spiked but built in-process), discovered during the v0.3.2 fleet update verify. Full audit + decisions: `docs/BROKER-BRAIN-SPLIT-RESTORATION.md` [...57921ch elided...] 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 delivery with NO hook poll.█- **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).█<!-- [doc->REQ-HAZARD-DELIVERY-STARVATION] -->█---█### 7.24 A delegated live adapter apply must NEVER report success without swapping, and the live-update seam must use ONE parent-aware adapter matcher `[REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP]`█...█...█[Showing lines 329-523 of 736. Use :524 to continue. Some lines truncated to 768 chars]\u000f█</out>█# Assistant ¶█_**Confirming PTY ownership in pty.rs**_█# Tool call ¶█//Indexing PTY session symbols█grep(pattern=\"pub struct PtySession|pub fn spawn|pub fn drain|pub fn write_input|pub fn resize|pub fn wait|pub fn kill|take_reader|MasterPty|writer\", path=\"crates/spt-term/src/pty.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 5|//! [`PtySession`] spawns a child under a real PTY, owns the master reader and█*6|//! writer, and serves the [`SessionSurface`] contract. It is **single-surface█7|//! per handle** - one child's PTY, not the multiplexed fleet (that supervisor is█8|//! the M3b broker).█9|//!█...█12|//! `resize()` races the reader thread on Windows; the master is therefore held█*13|//! behind a `Mutex` and every `resize` takes that lock. The writer is a shared█14|//! `Arc<Mutex<...>>` so the A2 DSR drain thread can answer ConPTY's startup█*15|//! cursor query through the same serialized writer the caller injects input█16|//! through - no torn writes.█17|█18|use std::io::{Read, Write};█...█20|█*21|use portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, ExitStatus, MasterPty};█22|█23|use crate::surface::{SessionSurface, SurfaceError, SurfaceSize};█24|█*25|/// Shared, serialized handle to the PTY master writer. Cloned to the A2 drain█26|/// thread so the DSR auto-answer and caller input never interleave mid-write.█27|pub(crate) type SharedWriter = Arc<Mutex<Box<dyn Write + Send>>>;█28|█...█32|/// (or the A3 `send_keys`/`send_line` helpers), drain output via█*33|/// [`Pt [...2898ch elided...] Arc::clone(&self.writer)█191| }█192|█193| /// Poll for child exit without blocking.█...█202| /// Block until the child exits, yielding its status.█*203| pub fn wait(&self) -> Result<ExitStatus, SurfaceError> {█204| self.child.lock().unwrap().wait().map_err(SurfaceError::Io)█205| }█206|█...█209| /// [`PtySession::wait`] / [`PtySession::try_wait`].█*210| pub fn kill(&self) -> Result<(), SurfaceError> {█211| let res = self.killer.lock().unwrap().kill();█212| #[cfg(windows)]█213| {█...█236| fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError> {█*237| let mut w = self.writer.lock().unwrap();█238| w.write_all(bytes).map_err(SurfaceError::Io)?;█239| w.flush().map_err(SurfaceError::Io)█240| }\u000f█</out>█# Assistant ¶█_**Searching for drain function**_█# Tool call ¶█//Finding PTY drain implementation█grep(pattern=\"drain\\\\(|take_reader\\\\(|drain_pty|spawn_drain\", path=\"crates/spt-daemon/src/broker.rs;crates/spt-term/src\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/src/█### broker.rs█3342| let drain = session█*3343| .drain(move |chunk| {█3344| // append() does the controller handoff inline now (one non-blocking█3345| // try_send - b4) and returns Some(epoch) ONLY when the controller is█3346| // stalled-past-deadline / gone and must be evicted. mark it gone OFF█...█4808| #[test]█*4809| fn a_full_controller_does_not_starve_a_viewer_or_block_the_drain() {█4810| let mut log = OutputLog::new(1, 1024, String::new(), (24, 80));█4811| log.controller_epoch.store(9, Ordering::Release);█4812| // A CONTROLLER whose 2-slot channel we NEVER drain (rx held, unread) - a█...█5013| #[test]█*5014| fn input_enqueue_drops_and_stamps_on_full_then_heals_on_drain() {█5015| const DEPTH: usize = 3;█5016| // Consumer is the test; it does NOT drain until we say so -> the queue fills.█5017| let (w, rx) = test_input_writer(DEPTH);█## spt-term/src/█### projection.rs█244| if turns.len() > n {█*245| turns.drain(..turns.len() - n);█246| }█247| Digest { turns }█248|}█...█457| }█*458| turns.d [...160ch elided...] n take_reader(&self) -> Result<Box<dyn Read + Send>, SurfaceError> {█179| self.reader█180| .lock()█181| .unwrap()█### reader.rs█173| {█*174| let reader = self.take_reader()?;█175| let writer = self.shared_writer();█176| Ok(Drain::spawn(reader, writer, sink))█177| }█### stream.rs█51| /// and the cooperative stop flag.█*52| pub fn drain(&self) -> &Drain {█53| &self.drain█54| }█55|█...█73| let (tx, rx) = sync_channel::<Vec<u8>>(capacity);█*74| let drain = self.drain(move |chunk| {█75| // Bounded send: blocks when `capacity` chunks are unconsumed█76| // (backpressure); drops silently once the receiver is gone so a█77| // torn-down consumer never wedges the pump.\u000f█</out>█# Assistant ¶█_**Inspecting remote vs local attach paths**_█# Tool call ¶█//Reading rc establish ownership█read(path=\"crates/spt/src/rc.rs\", selector=\"1070-1285,1760-2028\")█<out>█\u000efn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {█...█std::thread::spawn(move || {█...█while let Ok(ev) = event::read() {█match ev {█...█}█}█});█}█/// Attach a local terminal to the broker-held session for `endpoint_id` under█/// `intent` (REQ-RCVIEW-1 / REQ-KICK-1): `Control` drives a free endpoint (and is█/// busy-refused with guidance on a controlled one), `Viewer` watches read-only,█/// `Take` kicks the incumbent controller (loud `Displaced` notice to them) and█/// takes control. Returns when the session exits, the operator detaches, or an█/// error occurs.█// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]█pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {█run_attach_inner(endpoint_id, intent, false)█}█/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker█/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via█/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a█/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2█/// skeleton-write short-circuits [...22466ch elided...] rinting them literally (#12): it█// adds VT + processed-output, preserves the prior bits, and is idempotent.█#[cfg(windows)]█#[test]█fn with_vt_output_enables_virtual_terminal_processing() {█use windows_sys::Win32::System::Console::{█ENABLE_PROCESSED_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WRAP_AT_EOL_OUTPUT,█};█let prior = ENABLE_WRAP_AT_EOL_OUTPUT;█let got = with_vt_output(prior);█assert!(█got & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0,█\"VT-output processing enabled\"█);█assert!(got & ENABLE_PROCESSED_OUTPUT != 0, \"processed output enabled\");█assert!(█got & ENABLE_WRAP_AT_EOL_OUTPUT != 0,█\"prior console bits preserved\"█);█...█}█...█}\u000f█</out>█# Assistant ¶█_**Searching for establish_attach function**_█# Tool call ¶█//Locating rc establishment function█grep(pattern=\"fn establish_attach|struct EstablishedAttach|net_dial_loopback|request_attach_endpoint|net_stream_subscribe\", path=\"crates/spt/src/rc.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 28|█*29|use spt_daemon::attach::{request_attach_endpoint, send_attach_input, send_attach_resize};█30|use spt_daemon::effect::{Minter, MintedOp};█31|use spt_daemon::brain::{now_ms, Brain, BrokerEvent};█32|use spt_daemon::msg::decode_bytes;█...█1388|// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]█*1389|fn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {█1390| // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon█1391| // launcher. It must NEVER auto-start the broker - the historical█1392| // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,█...█1425| let dialed = brain█*1426| .net_dial_loopback()█1427| .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;█1428| (dialed.conn_id, sid, None)█1429| }█...█1469| Some(_) => brain█*1470| .net_dial_loopback()█1471| .map_err(|e| {█1472| std::io::Error::other(format!(\"loopback re-dial: {e}\"))█1473| })?█...█1485| };█*1486| request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)█1487| },█1488| )█1489| .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;█...█1492| brain█*1493| .net_stream_subscribe(stream_id, 0)█1494| .map_err(|e| EstablishFail::Error(format!(\"stream subscribe: {e}\")))?;█1495| Ok(Established {█1496| brain,█...█2357| /// canonical broker socket + a hermetic net host (loopback scope - the█*2358| /// net_dial_loopback carrier) and serves until killed. Env-gated: inert in█2359| /// every normal sweep.█2360| #[test]█2361| fn broker_host_child() {\u000f█</out>█# Tool call ¶█//Reading rc attach establishment█read(path=\"crates/spt/src/rc.rs\", selector=\"1360-1510\")█<out>█\u000eenum EstablishFail {█...█/// isn't running - start it\" exit; inside the reconnect loop it's just another█/// retry (the operator may bring the daemon back), never a resurrection.█/// [impl->REQ-RC-RECONNECT-TRUTH]█DaemonDown,█/// A setup error (broker connect / dial / attach-open / subscribe).█Error(String),█}█/// Drive the full attach-establish sequence: ensure daemon -> broker conn ->█/// session resolve (LOCAL first, cross-node on miss) -> dial -> attach-open (with█/// the A-4b tracing retry) -> stream subscribe. Factored from `run_attach_inner`█/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).█///█/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection█/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself█/// is not supported\"), so local attach cannot ride a self-dialed conn; instead█/// the broker wires an in-process loopback conn whose streams surface as the█/// SAME net-stream events the cross-node QUIC path produces. From█/// `request_attach` down this is byte-identical to a cross-node attach - the█/// one-pump invariant lives at the transport leaf inside the broker, never here.█// [...6474ch elided...] / Drive an established viewport to its FINAL end: the B-3 reconnect loop█/// (REQ-RC-RECONNECT) - each pass is one full viewport (initial resize -> pump).█/// A severed viewport (broker bounce, remote conn drop - the reconnectable█/// class) paints a centered reconnect banner and RE-DRIVES [`establish_attach`]█/// inside a bounded window; every other end is FINAL and returns to the█/// caller's parting copy. The FAULT-MATRIX row-9 promise (\"rc reconnects\") is█/// made TRUE here, not edited down (triage F-3).█///█/// The SEAM (doyle rig ruling #4): everything terminal-owning stays at the█/// interactive caller - raw-mode guard, stdin-reader spawn, StatusRow█/// construction/teardown, the parting copy. This fn only needs an established█[Showing lines 1359-1513 of 3214. Use :1514 to continue]\u000f█</out>█# Assistant ¶█_**Summarizing existing test coverage**█**Formulating falsifiable hypotheses for active dispatch tests**█**Designing stale serve simulation tests**█**Designing stale stream attach handling**█**Extending daemon refresh tests with eviction scenarios**_█# Tool call ¶█//Reading cursor resume split█read(path=\"crates/spt-daemon/src/brain.rs\", selector=\"1025-1085\")█<out>█\u000e /// REQ-BRAIN-RESUME-NO-CONTROL-STEAL). Genuinely daemon-brain-driven sessions (the█/// live-agent adapter future) will re-attach as Control on a drained carrier; that█/// set is empty today.█///█/// [WARN] DEADLOCK WARNING - the CALLER MUST DRAIN this conn (UPDATE-WEDGE round 3,█/// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK). This subscribes the sessions onto THIS conn.█/// A NON-DRAINING consumer will SELF-DEADLOCK: an actively-streaming session backs█/// the conn up, the broker-side subscriber writer blocks holding the conn's single█/// `SharedSend` mutex ACROSS a blocking `write_frame` (broker.rs█/// `viewer_writer`/`controller_writer`), the dispatch reply path (`send_frame`) needs█/// that same lock, so any request/reply the non-draining consumer issues is starved█/// (dead-peer-independent - the v0.30.4 field re-wedge). The supervised daemon brain█/// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn█/// only in `net_status()`/`sessions()` drain-and-discard loops; it drives no PTY█/// session - brainproc.rs:184 - and nothing on its side consumes the output, §3█/// guard), so it MUST call [`Se [...2069ch elided...] on-brain-driven sessions land (the live-agent adapter future),█/// attach THOSE on a DEDICATED drained carrier (Option B, deferred) - never on this█/// request/reply conn.█// [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]█// [impl->REQ-BRAIN-RESUME-NO-CONTROL-STEAL]█pub fn resume_session_cursors(&mut self) -> io::Result<Vec<u64>> {█let reply = self.sessions()?;█let mut resumed = Vec::with_capacity(reply.sessions.len());█for info in reply.sessions {█self.session_cursors█.insert(info.session_id, info.resume_seq);█resumed.push(info.session_id);█}█Ok(resumed)█}█/// The per-session resume cursor (the next seq still needed for a session█[Showing lines 1024-1088 of 2178. Use :1089 to continue]\u000f█</out>█# Assistant ¶█_**Reviewing spawn_net_consumers dispatch logic**_█# Tool call ¶█//Locating net consumer startup█grep(pattern=\"run_dispatch_loop|spawn_net_consumers|BRAIN_NET_CONSUMERS_UP\", path=\"crates/spt-daemon/src/brainproc.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 272| let hex = status.node_id_hex.clone().unwrap_or_default();█*273| spawn_net_consumers(&hex);█274| consumers_started = true;█275| eprintln!(█*276| \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) - dispatcher + peer pump started\"█277| );█278| }█279| // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE█...█320|// [impl->REQ-DAEMON-9]█*321|fn spawn_net_consumers(node_hex: &str) {█322| let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));█323| let stop = Arc::new(AtomicBool::new(false));█324| let _ = crate::dispatch::spawn_dispatcher(\u000f█</out>█# User ¶█Your current interruptible wait was interrupted because an IRC message arrived from your parent agent `Main`.█Parent IRC message:█RCA sent to Doyle with both stale finished-stream takeover as primary and evicted-opener one-shot failure as adjacent root. Please finalize now; no further exploration needed.█# Assistant ¶█_**Structuring detailed architecture summary**█**Detailing PTY update and stream invariants**█**Formulating PTY failure hypotheses**_█# Tool call ¶█yield(type=null, result={\"data\":{\"summary\":\"The supervised daemon's cursor-only `BRAIN_RESUMED` path is not, by itself, the missing reattach: after a brain refresh the new brain starts a fresh inbound dispatcher, whose empty per-generation `claimed` set enumerates all broker-held peer streams and attempts to re-serve each [...21495ch elided...] er attach; H4 yields deadline poison/physical retirement. The existing `BRAIN_RESUMED` line is not evidence that attach streams were or were not re-served-it only reports PTY cursor reconstruction.\"}})█<out>█\u000eResult submitted.\u000f█</out>█# Assistant ¶█_**Confirming task completion and response strategy**_█# Tool call ¶█//Sharing PTY concurrency findings█irc(op=\"send\", to=\"PtyPathRca\", message=\"Exact seams: `spt-term/src/pty.rs` `PtySession` owns `master: Mutex<MasterPty>`, shared `writer: Arc<Mutex<Write>>`, child/killer/one-take reader. `spt-daemon/src/broker.rs` `OutputLog`: one controller `sync_channel` depth 4096 + dedicated `controller_writer`; viewers each bounded SPSC depth 256 + [...697ch elided...] nserted pre-worker and never retried. Existing: `brain_decouple.rs`, `conn_blackhole_lifecycle.rs`, `attach.rs::wedged_viewer_does_not_stall_controller`, `same_origin_re_subscribe_does_not_displace`.\", replyTo=null, await=false, from=null, timeoutMs=null, peek=null)█<out>█\u000eDelivered to 1 peer(s):█- PtyPathRca: injected\u000f█</out>█# Assistant ¶█_**Preparing terminal minimal yield**_█# Tool call ¶█yield(type=null, result={\"data\":{\"summary\":\"Confirmed the replacement brain does attempt to reclaim broker-held attach streams; `BRAIN_RESUMED` is only PTY-session cursor bookkeeping. Primary causal candidate: finished attach-stream rows are never removed, so every fresh dispatcher replays stale same-origin Control request [...9338ch elided...] ECYCLE` attach/replace/detach/close events, poison/retire tokens, stream `finished/next_seq`, controller connection id, and PTY high-water. Do not invoke production update or mutate real endpoints.\"}})█<out>█\u000eResult submitted.\u000f█</out>█# Assistant ¶█_**Investigating delayed dispatch after connection poison**█**Analyzing connection poison delays and locking behavior**█**Identifying buffered frame delays on poison**█**Assessing loopback and dispatch error behavior**█**Analyzing stale stream poison timing**█**Identifying PTY output blocking cause**█**Diagnosing broker connection poisoning**█**Analyzing dispatch delay and wire command timing**_█# Tool call ¶█//Reading connection poison teardown█read(path=\"crates/spt-daemon/src/conn.rs\", selector=\"270-650\")█<out>█\u000eimpl Inner {█...█// [impl->REQ-CONN-POISON-ATTRIBUTION]█fn lifecycle(&self, event: &str, extra: &str) {█let sep = if extra.is_empty() { \"\" } else { \" \" };█eprintln!(█\"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",█self.attribution()█);█}█/// Abort the connection's in-flight I/O OUT OF BAND (both directions -█/// the halves share one OS object) and latch the poison. Idempotent and█/// safe to repeat: `CancelIoEx` on a handle with nothing pending is a█/// no-op, and re-cancel closes the race where a write was issued a beat█/// AFTER a cancel (cancellation only reaches ALREADY-issued ops).█/// `cause` (the organic caller's write error) rides the retirement record█/// when the retirement is NOT deadline-\u000edriven.█fn poison_and_cancel(&self, cause: Option<&io::Error>) {█if !self.poisoned.swap(true, Ordering::AcqRel) {█// First poison of this conn: loud, once. The token is class-split█// (F-039 leg a): deadline -> CONN_WRITE_POISONED (the wedge█// observable), organic fast-fail -> CONN_WRITE_RETIRED.█eprintln!(█[...13751ch elided...] estGatePin {█inner: Arc::clone(&self.inner),█half: Some(half),█}█}█}█impl Drop for BrokerConn {█fn drop(&mut self) {█// Once-per-conn `transport-close` lifecycle record (leg d): the handle█// is about to close - the terminal record of this conn id's life, with█// how it ended (organic EOF vs poisoned vs deadline-poisoned).█self.inner.lifecycle(█\"transport-close\",█&format!(█\"poisoned={} timed_out={}\",█self.inner.poisoned.load(Ordering::Acquire),█self.inner.timed_out.load(Ordering::Acquire)█),█);█{█let mut d = recover(&self.inner.dog);█...█}█...█}█}█[Showing lines 269-653 of 815. Use :654 to continue]\u000f█</out>█# Tool call ¶█//Reading net send client wait█read(path=\"crates/spt-daemon/src/brain.rs\", selector=\"1380-1485\")█<out>█\u000e pub fn net_open_stream(█...█) -> io::Result<NetStreamOpened> {█self.send(█...█serde_json::to_value(NetStreamOpenReq {█conn_id,█op_id: op.map(|o| o.seq),█minter: op.map(|o| o.minter),█})█.expect(\"NetStreamOpenReq serializes\"),█)?;█let deadline = self.call_deadline();█loop {█match self.read_event_until(deadline)? {█BrokerEvent::NetStreamOpened(ev) => return Ok(ev),█BrokerEvent::Error { message } => return Err(io::Error::other(message)),█_ => continue,█}█}█}█/// Write bytes to a held stream over the broker (D4b). With `op_id` the█/// write is exactly-once and this blocks for the `net-sent` ack (returned);█/// without one it is a direct fire-and-forget write (`None` returned).█/// `finish` closes the send side after the write.█// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]█pub fn net_stream_send(█&mut self,█stream_id: u64,█bytes: &[u8],█op: Option<MintedOp>,█finish: bool,█) -> io::Result<Option<NetSent>> {█self.send [...3018ch elided...] /// the dispatcher falls back to the ring peek).█// [impl->REQ-STREAM-OPENER-DURABLE]█pub fn net_stream_opener(&mut self, stream_id: u64) -> io::Result<Option<Vec<u8>>> {█self.send(█KIND_NET_STREAM_OPENER,█serde_json::to_value(NetStreamOpenerReq { stream_id })█.expect(\"NetStreamOpenerReq serializes\"),█)?;█loop {█match self.read_event()? {█BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_OPENER_REPLY => {█let reply: NetStreamOpenerReply = serde_json::from_value(env.payload)█.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;█...█}█...█}█}█}█[Showing lines 1379-1488 of 2236. Use :1489 to continue]\u000f█</out>█# Tool call ¶█//Reading broker net send dispatch█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"4080-4205\")█<out>█\u000e /// from abandoned earlier attempts are swept here (no separate reaper - pairing█/// is rare and the map is tiny).█// [impl->REQ-JOIN-TWO-PHASE]█fn dispatch_pair_meet(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {█let req: PairMeetReq = serde_json::from_value(env.payload)█.map_err(|e| format!(\"bad pair-meet payload: {e}\"))?;█let host = self█.net█.get()█.ok_or(\"net disabled: this broker was bound without a network host\")?;█match host.pair_meet(&req.subnet, req.verbose) {█Ok(addr) => {█// The canonical lowercase-hex name (== node_id_hex everywhere),█// derived from the met address's proven id.█let member_hex = spt_proto::identity::PublicKey::from_bytes(addr.id.as_bytes())█.map(|p| p.to_hex())█.unwrap_or_default();█let session_id = self.next_id.fetch_add(1, Ordering::Relaxed);█{█let mut holds = recover(&self.pair_holds);█let now = Instant::now();█holds.retain(|_, h| h.deadline > now); // swee [...4009ch elided...] sion_id);█}█pair_fail_frame(&fail)█}█};█send_frame(send, &frame);█Ok(())█}█/// Open a bidi stream on a held connection (D4b). Journaled exactly-once█/// under the same discipline as [`Self::dispatch_net_dial`]: the minted█/// `stream_id` is recorded inside the `apply_once` closure, and a deduped█/// replay is answered with the stream the first apply opened.█// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]█fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {█let req: NetStreamOpenReq = serde_json::from_value(env.payload)█.map_err(|e| format!(\"bad net-stream-open payload: {e}\"))?;█...█}█[Showing lines 4079-4208 of 6063. Use :4209 to continue]\u000f█</out>█# Tool call ¶█//Finding dispatch failure logging█grep(pattern=\"DISPATCH:|DispatchOutcome::Failed|net_stream_send\\\\(|send_stream\\\\(\", path=\"crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/attach.rs;crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/broker.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## dispatch.rs█217| DispatchOutcome::Served(_) | DispatchOutcome::Unknown => ClaimState::Terminal,█*218| DispatchOutcome::Failed(_) => {█219| let attempts = attempts + 1;█220| if attempts >= MAX_DISPATCH_ATTEMPTS {█221| ClaimState::Terminal█...█238|/// request and await the reply on the same stream (sync.rs `request_sync` and█*239|/// propagate.rs `fetch_update` both `net_stream_send(.., finish=true)` on the█240|/// opener), so every pull that arrives while the brain is down reads█241|/// `finished=true` while genuinely PENDING - filtering it breaks context sync█242|/// across exactly the refresh window this milestone fixes.█...█353| } else {█*354| eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");█355| }█356| let mut c = claims_done.lock().unwrap();█357| c.insert(█...█467| if take_injected_transient() {█*468| return DispatchOutcome::Failed(█469| \"injected transient worker-start failure (test valve)\".into(),█470| );█471| }█...█474| Ok(None) => return DispatchOutcome::Unknown,█*47 [...6958ch elided...] send: &SharedSend) -> Result<(), String> {█4263| let req: NetStreamSendReq = serde_json::from_value(env.payload)█4264| .map_err(|e| format!(\"bad net-stream-send payload: {e}\"))?;█4265| let bytes =█...█4269| None => host█*4270| .send_stream(req.stream_id, &bytes, req.finish)█4271| .map_err(|e| e.to_string()),█4272| Some(op) => {█4273| let minter = req.minter.unwrap_or(Minter::Legacy);█...█4281| .apply_once(key, EffectKind::NetSend, || {█*4282| host.send_stream(req.stream_id, &bytes, req.finish)█4283| })█4284| .map_err(|e| format!(\"net-stream-send failed: {e}\"))?;█4285| let frame = Envelope::new(\u000f█</out>█# Assistant ¶█_**Assessing current dispatch fixes and retries**_█# Tool call ¶█//Reading current dispatcher retry lifecycle█read(path=\"crates/spt-daemon/src/dispatch.rs\", selector=\"180-390,430-610\")█<out>█\u000eenum ClaimState {█...█/// retries exhausted. Terminal outcomes never re-enter the queue.█Terminal,█/// A transient failure released the claim; eligible again at `next_at`.█Retry { attempts: u32, next_at: Instant },█}█/// Bounded retry budget per stream per dispatcher lifetime - transient setup█/// failures recover, a persistently-failing stream goes terminal loud.█const MAX_DISPATCH_ATTEMPTS: u32 = 3;█/// Backoff base for a released claim (doubles per failed attempt).█const DISPATCH_RETRY_BASE: Duration = Duration::from_millis(500);█/// The backoff before retry number `attempts` (1-based) runs.█fn retry_backoff(attempts: u32) -> Duration {█DISPATCH_RETRY_BASE * 2u32.saturating_pow(attempts.saturating_sub(1))█}█/// PURE claim decision: may this row be (re)claimed now? `Some(prior█/// failures)` = claim it; `None` = leave it (in flight, terminal, or backing█/// off). The unit-tested seam for the no-hot-loop ruling.█// [impl->REQ-DISPATCH-CLAIM-RETRY]█fn should_claim(state: Option<&ClaimState>, now: Instant) -> Option<u32> {█match state {█None => Some(0),█Some(ClaimState::Retry { attempts, next_at }) if now >= *next_at => Some(*attempts),█[...15647ch elided...] sed: unlabeled/unknown session\".into())█}█}█}█StreamFamily::Xfer => {█// The wire names whose whitelist gates (the rule set); the origin█// it is checked against stays handshake-proven. Blank refuses.█let endpoint = line█.get(\"endpoint\")█.and_then(|v| v.as_str())█.filter(|e| !e.is_empty())█.map(str::to_string);█match endpoint {█Some(endpoint) => {█let root = spt_store::xfer::transfers_dir();█let progress = paths.scratch.join(\"xfer-progress\");█match serve_xfer(█...█) {█...█}█}█...█}█}█...█}█}\u000f█</out>█# Tool call ¶█//Tracing stream retirement implementation█grep(pattern=\"KIND_NET_STREAM_RETIRE|dispatch_net_stream_retire|retire_stream|net_stream_retire|opener\", path=\"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/dispatch.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## broker.rs█66| KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,█*67| KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,█68| KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,█69| KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,█70|};█...█3100| }█*3101| KIND_NET_STREAM_RETIRE => {█*3102| if let Err(msg) = self.dispatch_net_stream_retire(env, &send) {█3103| send_error(&send, &msg);█3104| }█3105| }█3106| KIND_NET_STREAM_OPENER => {█*3107| if let Err(msg) = self.dispatch_net_stream_opener(env, &send) {█3108| send_error(&send, &msg);█3109| }█3110| }█...█4319| // [impl->REQ-REDISPATCH-FINISHED-RETIRE]█*4320| fn dispatch_net_stream_retire(&self, env: Envelope, send [...8640ch elided...] // Subscribe from the ring FLOOR: a rolled ring's chunk 0 is█576| // gone, and a below-floor cursor trips the brain's gap check.█...█916|█*917| // [unit->REQ-INST-6] the rest op's opener disambiguates by shape too.█918| let rest =█919| json!({\"kind\": \"request\", \"endpoint\": \"ling\", \"event\": \"suspend\", \"op_id\": \"a:1\"});█920| assert_eq!(classify_first_line(&rest), StreamFamily::Rest);█...█946| for v in [█*947| json!({\"kind\": \"chunk\", \"sync_id\": \"s\", \"offset\": 0, \"data_b64\": \"\"}), // mid-protocol, not an opener█948| json!({\"kind\": \"request\"}), // request without any discriminant█949| json!({\"kind\": \"request\", \"endpoint\": \"ling\"}), // rest-ish but no event█950| json!({\"kind\": \"nonsense\"}),\u000f█</out>█# Tool call ¶█//Reading connection drop tail█read(path=\"crates/spt-daemon/src/conn.rs\", selector=\"630-690\")█<out>█\u000eimpl BrokerConn {█...█pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {█let mut g = recover(&self.inner.gate);█let half = g.half.take().expect(\"pin: gate half present\");█TestGatePin {█inner: Arc::clone(&self.inner),█half: Some(half),█}█}█}█impl Drop for BrokerConn {█fn drop(&mut self) {█// Once-per-conn `transport-close` lifecycle record (leg d): the handle█// is about to close - the terminal record of this conn id's life, with█// how it ended (organic EOF vs poisoned vs deadline-poisoned).█self.inner.lifecycle(█\"transport-close\",█&format!(█\"poisoned={} timed_out={}\",█self.inner.poisoned.load(Ordering::Acquire),█self.inner.timed_out.load(Ordering::Acquire)█),█);█{█let mut d = recover(&self.inner.dog);█d.shutdown = true;█self.inner.dog_cv.notify_all();█}█if let Some(dog) = self.dog.take() {█let _ = dog.join();█}█// Halves (gate or graveyard) drop AFTER the watchdog is gone - no raw█// handle use [...295ch elided...] nd::TimedOut,█\"broker conn write exceeded its bound (conn poisoned + aborted)\",█)█}█#[cfg(test)]█mod tests {█use super::*;█use crate::transport::{DaemonTransport, LocalSocketTransport};█use interprocess::local_socket::traits::Stream as _;█use interprocess::local_socket::Stream;█use std::sync::atomic::AtomicU32;█/// A real (loopback local-socket) conn pair - the send half wrapped in the█/// primitive, the client end returned so a test can hold it alive or drop█/// it to force an ORGANIC write failure.█fn make_conn() -> (BrokerConn, Stream) {█static SEQ: AtomicU32 = AtomicU32::new(0);█let name = format!(█\"spt-daemon-connattr-{}-{}.sock\",█...█);█...█}█...█}█[Showing lines 629-693 of 815. Use :694 to continue]\u000f█</out>█# Tool call ¶█//Reading broker stream send exact█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"4250-4315\")█<out>█\u000e fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {█...█match req.op_id {█...█Some(op) => {█...█let applied_now = outcome == Outcome::Applied;█send_frame(send, &build(stream_id, req.conn_id, Some(op), applied_now));█Ok(())█}█}█}█/// Write bytes to a held stream's send side (D4b). With an `op_id` the█/// write is exactly-once through the broker-owned journal (a replayed█/// chunk after a brain crash is not written twice - Spike #3 invariant D█/// on the send path) and acked with `net-sent`; without one it is a direct█/// write (no dedup, no ack), mirroring the PTY `input` contract.█// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]█fn dispatch_net_stream_send(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {█let req: NetStreamSendReq = serde_json::from_value(env.payload)█.map_err(|e| format!(\"bad net-stream-send payload: {e}\"))?;█let bytes =█decode_bytes(&req.data_b64).map_err(|e| format!(\"bad net-stream-send base64: {e}\"))?;█let host = self.net.get().ok_or(\"net disabled\")? [...1321ch elided...] n.█fn dispatch_net_streams(&self, send: &SharedSend) -> Result<(), String> {█let host = self.net.get().ok_or(\"net disabled\")?;█let frame = Envelope::new(█KIND_NET_STREAMS_REPLY,█serde_json::to_value(NetStreamsReply {█streams: host.stream_infos(),█})█.expect(\"NetStreamsReply serializes\"),█);█send_frame(send, &frame);█Ok(())█}█/// Retire a completed stream row from redispatch eligibility (ADR-0038 A):█/// the dispatcher reports a serve loop that ran to completion, and the row█/// leaves the table so no future dispatcher generation can rediscover and█/// replay it (the KNOWN-HAZARDS 7.41 control-steal class).█[Showing lines 4249-4318 of 6063. Use :4319 to continue]\u000f█</out>","textHead":"# User ¶█Complete the assignment below, thoroughly:█# Target█Broker PTY control/attach/detach/reattach implementation and tests. Non-goal: daemon update mechanics except their interaction boundary.█# Change█Trace controller and viewer stream ownership from `spt rc` attach through broker session tasks. Explain what state produces a frozen terminal that recovers only after intentional detach and a new `spt rc`. Search for cancellation, half-open stream, connection epoch, displaced controller, and broker client reconnection handling.█# Acceptance█Return exact symbols, invariants, existing hazards/tests, and 3-5 falsifiable hypotheses with a safe reproducible harness proposal. No writes or runtime mutation.█# Assistant ¶█_**Planning skill diagnosis and context reading**_█# Tool call ¶█//Loading diagnosis workflow█read(path=\"skill://diagnose\", selector=null)█<out>█\u000e---█name: diagnose█description: 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.█---█# Diagnose█A discipline for hard bugs. Skip phases only when explicitly justified.█When 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.█## Phase 1 - Build a feedback loop█**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.█Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**█### Ways to construct one - try them in roughly this order█1. **Failing test** at whatever seam reaches the bug - unit, integration, e2e.█2. **Curl / HTTP script** against a running dev server [...5116ch elided...] # Phase 6 - Cleanup + post-mortem█Required before declaring done:█- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)█- [ ] Regression test passes (or absence of seam is documented)█- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)█- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)█- [ ] The hypothesis that turned out correct is stated in the commit / PR message - so the next debugger learns█**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.█\u000f█</out>█# Tool call ¶█//Mapping broker PTY files█glob(path=\"**/*{broker,pty,session,attach,control}*\", hidden=true, gitignore=true, limit=300)█<out>█\u000e# crates/█## spt/tests/█resume_no_control_steal_e2e.rs█attach_wedge_e2e.rs█run_no_dup_session_e2e.rs█## spt-daemon/█### src/█broker.rs█attach.rs█### tests/█inject_control_wedge.rs█netbroker.rs█broker.rs█attach.rs█control_stamp_lifetime.rs█## spt-store/src/█sessions.rs█attachment.rs█## spt-term/src/█pty.rs█## spt-net/src/net/█attach.rs█# docs/█## adr/█0027-unbound-endpoint-state-and-attach-on-session.md█0009-endpoint-access-control-model.md█0008-live-activity-buffer-pty-digest.md█0019-digest-adapter-declared-extractor-session-spanning.md█0018-broker-brain-process-isolation-restoration.md█0004-single-daemon-broker-brain-split-and-self-update.md█## spikes/█SPIKE-04-forkpty-parity.md█SPIKE-01-broker-handoff.md\u000f█</out>█# Tool call ¶█//Locating required domain docs█glob(path=\"{CONTEXT.md,docs/KNOWN-HAZARDS.md,docs/DEBUG-ROLLOUT.md,docs/adr/**,docs/ADR/**}\", hidden=true, gitignore=true, limit=200)█<out>█\u000eCONTEXT.md█# docs/█KNOWN-HAZARDS.md█DEBUG-ROLLOUT.md█## adr/█0037-materialized-project-index.md█0036-private-fork-distribution.md█0005-totp-seeded-spake2-node-pairing.md█0031-server-side-screen-grid-render-repaint.md█0034-per-minter-op-id-namespacing.md█0033-wake-resume-via-reconcile-intent.md█0025-live-daemon-coordinated-adapter-update.md█0022-spt-hosted-idle-delivery-translation-binary.md█0032-boundary-rotation-credential.md█0030-robust-wan-subnet-join.md█0029-adapter-update-arc-resolve-not-execute.md█0028-activity-gated-delivery-and-send-modifier-axes.md█0027-unbound-endpoint-state-and-attach-on-session.md█0026-multi-subnet-home-at-endpoint-run-creation.md█0024-multi-platform-adapter-spt-packaging.md█0023-always-on-endpoints-resident-supervised-binary-sigil-addressing.md█0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md█0020-event-envelope-sole-arriving-format-reply-to-removed.md█0012-psyche-trust-boundary-stdout-event-outbound.md█0009-endpoint-access-control-model.md█0008-live-activity-buffer-pty-digest.md█0019-digest-adapter-declared-extractor-session-spanning.md█0018-broker-brain-process-isolation-restoration.md█0016-platform-targeted-update-sets.md█0017-subnet-mesh-membership-seed-proof.md█0014-publish-target-repo-and-lapse-proof-canonical-url.md█0015-release-signing-two-key-trust-anchor.md█0013-psyche-reconciled-context-merge.md█0010-immutable-home-subnet-fork-not-rehome.md█0011-branchstore-git-kv-state-substrate.md█0006-multi-subnet-membership-model.md█0007-subnet-notification-primitive.md█0003-multi-instance-node-anchored-identity-model.md█0004-single-daemon-broker-brain-split-and-self-update.md█0002-bake-networking-into-core-collapse-spt-node.md█0001-clean-room-fork-from-claude-skill-owl.md\u000f█</out>█# Tool call ¶█//Finding attach control symbols█grep(pattern=\"\\\\b(attach|detach|reattach|controller|viewer|displaced|epoch|half[-_ ]open|cancel|CancellationToken|reconnect)\\\\b\", path=\"src;crates;packages;tests;test\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## access.rs█41|//! message traffic.█*42|//! Control surfaces (terminal attach, file transfer) are never replies; their█43|//! serves always gate as [`InboundClass::Unsolicited`].█44|█45|use spt_store::access::{AccessStore, RecentOutbound, REPLY_WINDOW_MS};█## applyhost.rs█320| // brain-restart trigger rides after the swap (ADR-0018 D3-3); it does not█*321| // attach to any session (the broker holds every hosted endpoint across the█322| // brain cycle - that is the whole point).█323| let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;█324|█...█451|/// the planned brain-restart trigger (ADR-0018 D3-3) rides after the binary█*452|/// swap. It does NOT attach to a session: the broker holds every hosted endpoint█453|/// across the brain cycle, so apply never touches one. `Err` means the broker█454|/// never answered (a dead broker fails the apply with the seat untouched).█455|fn connect_retry(broker_name: &str) -> io::Result<Brain> {█## attach.rs█*1|//! Remote terminal attach (M4-D5b, REQ-INST-8) - serve a byte-stream viewport█2|//! onto a local session for an operator on another node.█3|//!█4|//! [...42308ch elided...] emit `{\"commit\":true}`. The broker's inject worker must hit its█*102| // commit deadline, flush the buffered controller input, release the█103| // floor, and FAULT this binary (after which idle inbound SPOOLS - raw█104| // inject was removed as a delivery path, ADR-0022 amendment).█105| //█...█143|/// truncation and many settle-chunks wide, yet it fits inside a 24-row screen grid so a█*144|/// cold-attach repaint (ADR-0031 = current screen) still carries the HEAD (a taller█145|/// payload scrolls HEADSTART off the visible grid).█146|fn big_multiline_payload() -> String {█147| let mut s = String::from(\"HEADSTART \");█Showing files 1-20 of 161+. Use skip=20 for the next page, or narrow paths/pattern.█Skipped missing paths: packages, test\u000f█</out>█# Assistant ¶█_**Planning targeted code and doc exploration**_█# Tool call ¶█//Reading project domain model█read(path=\"CONTEXT.md\", selector=\"1-280\")█<out>█\u000e# spt-core█**Platform scope:** Windows + Linux for v1. macOS is out (no test machine available) but kept structurally easy - `portable-pty` and Iroh both support it, so macOS is a later test/CI-budget decision, not a re-architecture.█**Legacy migration:** it should be possible - ideally *automatic* - for a user to migrate an existing `claude_skill_owl` (modern SPT) install to spt-core (identity, agents, tracked Psyche context). Exact mechanism deferred to design; the commitment is that migration is a first-class supported path, not a manual rebuild.█Harness-independent core for the SPT ecosystem. Provides inter-agent messaging, live-agent lifecycle, terminal wrapping, self-update, and networking primitives - as both a Rust library workspace and a canonical reference binary. Designed so any agent runtime (Claude Code, Codex, Cursor, headless, future harnesses) can interface with the SPT ecosystem either by shelling out to the binary or by linking the crates directly.█Successor to `claude_skill_owl` (today's \"modern SPT\"), which is being rebuilt as `spt-core` to untether the system from Claude Code and lift it to a general-purpose agent-ecosystem core.█## Language█**spt-core**: [...55832ch elided...] mmand that lists endpoints **grouped by locality, most-recently-used within each group**: `on-node / current-project -> on-node / other-project -> off-node`, mirroring the *resolution policy*'s local-first preference. Selection **chains conditionally**: a **running** instance -> attach/tap-in (no adapter step - already live under one); a **non-running** endpoint -> into the **adapter selector** (*adapter selection*: history head = default -> prior adapters -> \"choose a different adapter\") -> *home subnet* / other creation prompts as needed -> launch; a **\"+ new endpoint\"** entry -> the full creation flow. Off-node picks respect the reach + consent gates (remote-drive of your own running instance is ungated; a co...█[Showing lines 1-283 of 883. Use :284 to continue. Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading PTY known hazards█read(path=\"docs/KNOWN-HAZARDS.md\", selector=null)█<out>█\u000e# Known Hazards█Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** - the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.█**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon<->broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.█---█## 1. Race conditions & ordering█### 1.1 Phantom INIT_SIGNOFF after grace period█- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spoole [...51630ch elided...] structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.█- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.█### 6.2 Soft-cleanup preserves state, removes `ready`█- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.█- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.█- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.█- **Sister cite:** `src/owl/stop.rs`.█...█...█<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->█[Showing lines 1-300 of 736. Use :301 to continue. Some lines truncated to 768 chars]\u000f█</out>█# Tool call ¶█//Reading debug rollout guidance█read(path=\"docs/DEBUG-ROLLOUT.md\", selector=null)█<out>█\u000e# Debug rollout runbook█<!-- [doc->REQ-UPD-6] -->█Debug rollout is the maintainer-only fast path for testing a local spt-core█build across a trusted lab subnet. It uses the normal signed self-update█substrate: no raw peer file-copy, no production `spt` CLI surface, and no█embedded debug trust anchor.█Use this when debugging needs a local build to reach multiple lab nodes quickly.█Do not use it for public releases; use `docs/RELEASE-RUNBOOK.md` for that.█## Mental model█- A rollout is a signed `SignedUpdateSet`: one signed metadata record, with one█artifact digest per Rust target triple.█- Each recipient verifies the set under its node-local `release-keys.json`,█selects only its own platform artifact, stages it, and then follows the normal█consent/apply policy.█- Debug and stable are separate channels. A debug-pinned node accepts only█`channel = \"debug\"` offers; a stable-pinned node accepts only stable offers.█- Debug versions are monotonic within the debug channel. To recover from a bad█debug build, publish a higher debug version, even if the bytes are a previous█known-good binary.█- A broker-touching candidate may be staged, but apply refuses it while█broker-hel [...2707ch elided...] t triple.█- `WrongChannel` means the node is not pinned to `debug`.█- `Rollback` means the offered debug version is not greater than the node's█current debug-channel version.█- `RefusedClass(BrokerBreaking)` means the candidate touches broker-held█resources; quiesce hosted endpoints first.█## Agent checklist█1. Read `CONTEXT.md` \"debug rollout\" an","textTail":"\u000edriven.█fn poison_and_cancel(&self, cause: Option<&io::Error>) {█if !self.poisoned.swap(true, Ordering::AcqRel) {█// First poison of this conn: loud, once. The token is class-split█// (F-039 leg a): deadline -> CONN_WRITE_POISONED (the wedge█// observable), organic fast-fail -> CONN_WRITE_RETIRED.█eprintln!(█[...13751ch elided...] estGatePin {█inner: Arc::clone(&self.inner),█half: Some(half),█}█}█}█impl Drop for BrokerConn {█fn drop(&mut self) {█// Once-per-conn `transport-close` lifecycle record (leg d): the handle█// is about to close - the terminal record of this conn id's life, with█// how it ended (organic EOF vs poisoned vs deadline-poisoned).█self.inner.lifecycle(█\"transport-close\",█&format!(█\"poisoned={} timed_out={}\",█self.inner.poisoned.load(Ordering::Acquire),█self.inner.timed_out.load(Ordering::Acquire)█),█);█{█let mut d = recover(&self.inner.dog);█...█}█...█}█}█[Showing lines 269-653 of 815. Use :654 to continue]\u000f█</out>█# Tool call ¶█//Reading net send client wait█read(path=\"crates/spt-daemon/src/brain.rs\", selector=\"1380-1485\")█<out>█\u000e pub fn net_open_stream(█...█) -> io::Result<NetStreamOpened> {█self.send(█...█serde_json::to_value(NetStreamOpenReq {█conn_id,█op_id: op.map(|o| o.seq),█minter: op.map(|o| o.minter),█})█.expect(\"NetStreamOpenReq serializes\"),█)?;█let deadline = self.call_deadline();█loop {█match self.read_event_until(deadline)? {█BrokerEvent::NetStreamOpened(ev) => return Ok(ev),█BrokerEvent::Error { message } => return Err(io::Error::other(message)),█_ => continue,█}█}█}█/// Write bytes to a held stream over the broker (D4b). With `op_id` the█/// write is exactly-once and this blocks for the `net-sent` ack (returned);█/// without one it is a direct fire-and-forget write (`None` returned).█/// `finish` closes the send side after the write.█// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]█pub fn net_stream_send(█&mut self,█stream_id: u64,█bytes: &[u8],█op: Option<MintedOp>,█finish: bool,█) -> io::Result<Option<NetSent>> {█self.send [...3018ch elided...] /// the dispatcher falls back to the ring peek).█// [impl->REQ-STREAM-OPENER-DURABLE]█pub fn net_stream_opener(&mut self, stream_id: u64) -> io::Result<Option<Vec<u8>>> {█self.send(█KIND_NET_STREAM_OPENER,█serde_json::to_value(NetStreamOpenerReq { stream_id })█.expect(\"NetStreamOpenerReq serializes\"),█)?;█loop {█match self.read_event()? {█BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_OPENER_REPLY => {█let reply: NetStreamOpenerReply = serde_json::from_value(env.payload)█.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;█...█}█...█}█}█}█[Showing lines 1379-1488 of 2236. Use :1489 to continue]\u000f█</out>█# Tool call ¶█//Reading broker net send dispatch█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"4080-4205\")█<out>█\u000e /// from abandoned earlier attempts are swept here (no separate reaper - pairing█/// is rare and the map is tiny).█// [impl->REQ-JOIN-TWO-PHASE]█fn dispatch_pair_meet(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {█let req: PairMeetReq = serde_json::from_value(env.payload)█.map_err(|e| format!(\"bad pair-meet payload: {e}\"))?;█let host = self█.net█.get()█.ok_or(\"net disabled: this broker was bound without a network host\")?;█match host.pair_meet(&req.subnet, req.verbose) {█Ok(addr) => {█// The canonical lowercase-hex name (== node_id_hex everywhere),█// derived from the met address's proven id.█let member_hex = spt_proto::identity::PublicKey::from_bytes(addr.id.as_bytes())█.map(|p| p.to_hex())█.unwrap_or_default();█let session_id = self.next_id.fetch_add(1, Ordering::Relaxed);█{█let mut holds = recover(&self.pair_holds);█let now = Instant::now();█holds.retain(|_, h| h.deadline > now); // swee [...4009ch elided...] sion_id);█}█pair_fail_frame(&fail)█}█};█send_frame(send, &frame);█Ok(())█}█/// Open a bidi stream on a held connection (D4b). Journaled exactly-once█/// under the same discipline as [`Self::dispatch_net_dial`]: the minted█/// `stream_id` is recorded inside the `apply_once` closure, and a deduped█/// replay is answered with the stream the first apply opened.█// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]█fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {█let req: NetStreamOpenReq = serde_json::from_value(env.payload)█.map_err(|e| format!(\"bad net-stream-open payload: {e}\"))?;█...█}█[Showing lines 4079-4208 of 6063. Use :4209 to continue]\u000f█</out>█# Tool call ¶█//Finding dispatch failure logging█grep(pattern=\"DISPATCH:|DispatchOutcome::Failed|net_stream_send\\\\(|send_stream\\\\(\", path=\"crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/attach.rs;crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/broker.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## dispatch.rs█217| DispatchOutcome::Served(_) | DispatchOutcome::Unknown => ClaimState::Terminal,█*218| DispatchOutcome::Failed(_) => {█219| let attempts = attempts + 1;█220| if attempts >= MAX_DISPATCH_ATTEMPTS {█221| ClaimState::Terminal█...█238|/// request and await the reply on the same stream (sync.rs `request_sync` and█*239|/// propagate.rs `fetch_update` both `net_stream_send(.., finish=true)` on the█240|/// opener), so every pull that arrives while the brain is down reads█241|/// `finished=true` while genuinely PENDING - filtering it breaks context sync█242|/// across exactly the refresh window this milestone fixes.█...█353| } else {█*354| eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");█355| }█356| let mut c = claims_done.lock().unwrap();█357| c.insert(█...█467| if take_injected_transient() {█*468| return DispatchOutcome::Failed(█469| \"injected transient worker-start failure (test valve)\".into(),█470| );█471| }█...█474| Ok(None) => return DispatchOutcome::Unknown,█*47 [...6958ch elided...] send: &SharedSend) -> Result<(), String> {█4263| let req: NetStreamSendReq = serde_json::from_value(env.payload)█4264| .map_err(|e| format!(\"bad net-stream-send payload: {e}\"))?;█4265| let bytes =█...█4269| None => host█*4270| .send_stream(req.stream_id, &bytes, req.finish)█4271| .map_err(|e| e.to_string()),█4272| Some(op) => {█4273| let minter = req.minter.unwrap_or(Minter::Legacy);█...█4281| .apply_once(key, EffectKind::NetSend, || {█*4282| host.send_stream(req.stream_id, &bytes, req.finish)█4283| })█4284| .map_err(|e| format!(\"net-stream-send failed: {e}\"))?;█4285| let frame = Envelope::new(\u000f█</out>█# Assistant ¶█_**Assessing current dispatch fixes and retries**_█# Tool call ¶█//Reading current dispatcher retry lifecycle█read(path=\"crates/spt-daemon/src/dispatch.rs\", selector=\"180-390,430-610\")█<out>█\u000eenum ClaimState {█...█/// retries exhausted. Terminal outcomes never re-enter the queue.█Terminal,█/// A transient failure released the claim; eligible again at `next_at`.█Retry { attempts: u32, next_at: Instant },█}█/// Bounded retry budget per stream per dispatcher lifetime - transient setup█/// failures recover, a persistently-failing stream goes terminal loud.█const MAX_DISPATCH_ATTEMPTS: u32 = 3;█/// Backoff base for a released claim (doubles per failed attempt).█const DISPATCH_RETRY_BASE: Duration = Duration::from_millis(500);█/// The backoff before retry number `attempts` (1-based) runs.█fn retry_backoff(attempts: u32) -> Duration {█DISPATCH_RETRY_BASE * 2u32.saturating_pow(attempts.saturating_sub(1))█}█/// PURE claim decision: may this row be (re)claimed now? `Some(prior█/// failures)` = claim it; `None` = leave it (in flight, terminal, or backing█/// off). The unit-tested seam for the no-hot-loop ruling.█// [impl->REQ-DISPATCH-CLAIM-RETRY]█fn should_claim(state: Option<&ClaimState>, now: Instant) -> Option<u32> {█match state {█None => Some(0),█Some(ClaimState::Retry { attempts, next_at }) if now >= *next_at => Some(*attempts),█[...15647ch elided...] sed: unlabeled/unknown session\".into())█}█}█}█StreamFamily::Xfer => {█// The wire names whose whitelist gates (the rule set); the origin█// it is checked against stays handshake-proven. Blank refuses.█let endpoint = line█.get(\"endpoint\")█.and_then(|v| v.as_str())█.filter(|e| !e.is_empty())█.map(str::to_string);█match endpoint {█Some(endpoint) => {█let root = spt_store::xfer::transfers_dir();█let progress = paths.scratch.join(\"xfer-progress\");█match serve_xfer(█...█) {█...█}█}█...█}█}█...█}█}\u000f█</out>█# Tool call ¶█//Tracing stream retirement implementation█grep(pattern=\"KIND_NET_STREAM_RETIRE|dispatch_net_stream_retire|retire_stream|net_stream_retire|opener\", path=\"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/nethost.rs;crates/spt-daemon/src/dispatch.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## broker.rs█66| KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,█*67| KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,█68| KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,█69| KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,█70|};█...█3100| }█*3101| KIND_NET_STREAM_RETIRE => {█*3102| if let Err(msg) = self.dispatch_net_stream_retire(env, &send) {█3103| send_error(&send, &msg);█3104| }█3105| }█3106| KIND_NET_STREAM_OPENER => {█*3107| if let Err(msg) = self.dispatch_net_stream_opener(env, &send) {█3108| send_error(&send, &msg);█3109| }█3110| }█...█4319| // [impl->REQ-REDISPATCH-FINISHED-RETIRE]█*4320| fn dispatch_net_stream_retire(&self, env: Envelope, send [...8640ch elided...] // Subscribe from the ring FLOOR: a rolled ring's chunk 0 is█576| // gone, and a below-floor cursor trips the brain's gap check.█...█916|█*917| // [unit->REQ-INST-6] the rest op's opener disambiguates by shape too.█918| let rest =█919| json!({\"kind\": \"request\", \"endpoint\": \"ling\", \"event\": \"suspend\", \"op_id\": \"a:1\"});█920| assert_eq!(classify_first_line(&rest), StreamFamily::Rest);█...█946| for v in [█*947| json!({\"kind\": \"chunk\", \"sync_id\": \"s\", \"offset\": 0, \"data_b64\": \"\"}), // mid-protocol, not an opener█948| json!({\"kind\": \"request\"}), // request without any discriminant█949| json!({\"kind\": \"request\", \"endpoint\": \"ling\"}), // rest-ish but no event█950| json!({\"kind\": \"nonsense\"}),\u000f█</out>█# Tool call ¶█//Reading connection drop tail█read(path=\"crates/spt-daemon/src/conn.rs\", selector=\"630-690\")█<out>█\u000eimpl BrokerConn {█...█pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {█let mut g = recover(&self.inner.gate);█let half = g.half.take().expect(\"pin: gate half present\");█TestGatePin {█inner: Arc::clone(&self.inner),█half: Some(half),█}█}█}█impl Drop for BrokerConn {█fn drop(&mut self) {█// Once-per-conn `transport-close` lifecycle record (leg d): the handle█// is about to close - the terminal record of this conn id's life, with█// how it ended (organic EOF vs poisoned vs deadline-poisoned).█self.inner.lifecycle(█\"transport-close\",█&format!(█\"poisoned={} timed_out={}\",█self.inner.poisoned.load(Ordering::Acquire),█self.inner.timed_out.load(Ordering::Acquire)█),█);█{█let mut d = recover(&self.inner.dog);█d.shutdown = true;█self.inner.dog_cv.notify_all();█}█if let Some(dog) = self.dog.take() {█let _ = dog.join();█}█// Halves (gate or graveyard) drop AFTER the watchdog is gone - no raw█// handle use [...295ch elided...] nd::TimedOut,█\"broker conn write exceeded its bound (conn poisoned + aborted)\",█)█}█#[cfg(test)]█mod tests {█use super::*;█use crate::transport::{DaemonTransport, LocalSocketTransport};█use interprocess::local_socket::traits::Stream as _;█use interprocess::local_socket::Stream;█use std::sync::atomic::AtomicU32;█/// A real (loopback local-socket) conn pair - the send half wrapped in the█/// primitive, the client end returned so a test can hold it alive or drop█/// it to force an ORGANIC write failure.█fn make_conn() -> (BrokerConn, Stream) {█static SEQ: AtomicU32 = AtomicU32::new(0);█let name = format!(█\"spt-daemon-connattr-{}-{}.sock\",█...█);█...█}█...█}█[Showing lines 629-693 of 815. Use :694 to continue]\u000f█</out>█# Tool call ¶█//Reading broker stream send exact█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"4250-4315\")█<out>█\u000e fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {█...█match req.op_id {█...█Some(op) => {█...█let applied_now = outcome == Outcome::Applied;█send_frame(send, &build(stream_id, req.conn_id, Some(op), applied_now));█Ok(())█}█}█}█/// Write bytes to a held stream's send side (D4b). With an `op_id` the█/// write is exactly-once through the broker-owned journal (a replayed█/// chunk after a brain crash is not written twice - Spike #3 invariant D█/// on the send path) and acked with `net-sent`; without one it is a direct█/// write (no dedup, no ack), mirroring the PTY `input` contract.█// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]█fn dispatch_net_stream_send(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {█let req: NetStreamSendReq = serde_json::from_value(env.payload)█.map_err(|e| format!(\"bad net-stream-send payload: {e}\"))?;█let bytes =█decode_bytes(&req.data_b64).map_err(|e| format!(\"bad net-stream-send base64: {e}\"))?;█let host = self.net.get().ok_or(\"net disabled\")? [...1321ch elided...] n.█fn dispatch_net_streams(&self, send: &SharedSend) -> Result<(), String> {█let host = self.net.get().ok_or(\"net disabled\")?;█let frame = Envelope::new(█KIND_NET_STREAMS_REPLY,█serde_json::to_value(NetStreamsReply {█streams: host.stream_infos(),█})█.expect(\"NetStreamsReply serializes\"),█);█send_frame(send, &frame);█Ok(())█}█/// Retire a completed stream row from redispatch eligibility (ADR-0038 A):█/// the dispatcher reports a serve loop that ran to completion, and the row█/// leaves the table so no future dispatcher generation can rediscover and█/// replay it (the KNOWN-HAZARDS 7.41 control-steal class).█[Showing lines 4249-4318 of 6063. Use :4319 to continue]\u000f█</out>"}}}
{"type":"message","id":"3ba99a6e","parentId":"fdf30e2e","timestamp":"2026-07-17T00:19:45.428Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Locating ADR filename with glob**"},{"type":"toolCall","id":"call_hR3Og8Kg1KGz9FSAPEKAW5UP|fc_0996e78cf50e3160016a597520090081998d791358ec1393ab","name":"glob","arguments":{"i":"Locating ADR 0038","path":"docs/adr/*0038*","hidden":true,"gitignore":true,"limit":20}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":56377,"output":58,"cacheRead":12800,"cacheWrite":0,"totalTokens":69235,"reasoningTokens":11,"cost":{"input":0.281885,"output":0.00174,"cacheRead":0.0063999999999999994,"cacheWrite":0,"total":0.29002500000000003}},"stopReason":"toolUse","timestamp":1784247580185,"responseId":"resp_0996e78cf50e3160016a59751ce84481998066f25fd2e364a0","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a59751f611c8199b27121477512fe67","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXUg9lomwpnWmHXxURBBKO2KX-85IwNPSIWZbk5JaNSgu6RGCo6a7gL8-we_sKcV2a-kcZGo2lJ7TraHpoa8Rtol6eJu9obBxYj7zbLJ_dX8lPKWMTyBQDQESXwrqw-ffr2El2PnTi-wg1K0SKIE_IP2yzWFUpyI0w71I_5WANMHz6mDyxDjd8jZVvm-XPMj1Nd06bMxdkzcihIHVrw_EQ_oIbsJnRkeo6rjLVLudsDbuw_2FQEhQZDVEZJeFWhruCj0yfYVewqB0gV9yoShp6TKvKNpr2uOSxj6wXTM5CuEJ2QC5lAhEUm7FZcepp2wctK8JpvQVUG75xcw6AylucvdR3jjAbQ9agdo0WOoXIvoWWgfGkhql9MNs1lgcJHBh4SsMGrx373gHiyTMz7ZQx8OT9710-mxjYAn831v1SY1KCrekXn9xS4zf05t4cPaFacGbaIqvbtKIzZp10OP4u-K2nVEv04cfzx0yOWmjjUwK2cT6tFZF8ADFYU_VfAGR9XsJqJ79inNyf2ruBPcVQ_X_acskBl-gjk_AqHKj7H5FxWnDHKZHCWvjGpWm2NF5jA-N3cBL1NYPHW8F81_2wcQBgzpy5sVVr3KHfE4VMP412mABVmOrq5T3YugX0p_yorttljFuNODp86rI-0-PlROn9STioEVeIp4T9ocOpOlpkak6ScxHEOaJL5ipewpuc_YHpggBirhUGHm3UGDlZ5k1gojCRdOjREBzceIUnoGZ0oMK34OvP_bA6pFYIg1CfFeTnTlxeG0uiosATzsgj4cr-SLkoNNWANDfZ2JpsL6kKDph1HOSCN2JIiAM_VsPdvd8hyYPYT9UWyQs_mClUZjVi7_BJI7KMDAJnbHti_vCcOuiVlOgOKgEcn0nkyJq-c06bLJdod944YHvTl4YBy6ShDoMZcnw7ZHmWILeRQERt1HwhVZj-h5bZ4iC7KQScNtoXo_sUOBFLcAMXuqDXtX6A==","summary":[{"type":"summary_text","text":"**Locating ADR filename with glob**"}]},{"id":"fc_0996e78cf50e3160016a597520090081998d791358ec1393ab","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating ADR 0038\",\"path\":\"docs/adr/*0038*\",\"hidden\":true,\"gitignore\":true,\"limit\":20}","call_id":"call_hR3Og8Kg1KGz9FSAPEKAW5UP","name":"glob"}]},"duration":5242.00720000267,"ttft":3190.3278999999166,"contextSnapshot":{"promptTokens":69177,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_hR3Og8Kg1KGz9FSAPEKAW5UP|fc_0996e78cf50e3160016a597520090081998d791358ec1393ab","toolName":"glob","startedAt":"2026-07-17T00:19:45.429Z","args":{"path":"docs/adr/*0038*"},"intent":"Locating ADR 0038"},"id":"f2f4c03a","parentId":"3ba99a6e","timestamp":"2026-07-17T00:19:45.429Z"}
{"type":"message","id":"c34780b5","parentId":"f2f4c03a","timestamp":"2026-07-17T00:19:45.432Z","message":{"role":"toolResult","toolCallId":"call_hR3Og8Kg1KGz9FSAPEKAW5UP|fc_0996e78cf50e3160016a597520090081998d791358ec1393ab","toolName":"glob","content":[{"type":"text","text":"# docs/adr/\n0038-broker-stream-lifecycle-truth.md"}],"details":{"scopePath":"docs/adr","fileCount":1,"files":["docs/adr/0038-broker-stream-lifecycle-truth.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1784247585431}}
{"type":"message","id":"f0edc779","parentId":"c34780b5","timestamp":"2026-07-17T00:19:52.260Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_dicZpyHWoG1tBwuhxHSuvYao|fc_0996e78cf50e3160016a5975269af88199bf6ee595b3f4f5a5","name":"read","arguments":{"i":"Reading lifecycle truth ADR","path":"docs/adr/0038-broker-stream-lifecycle-truth.md","selector":"1-340"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":5260,"output":43,"cacheRead":64000,"cacheWrite":0,"totalTokens":69303,"cost":{"input":0.0263,"output":0.0012900000000000001,"cacheRead":0.032,"cacheWrite":0,"total":0.059590000000000004}},"stopReason":"toolUse","timestamp":1784247585466,"responseId":"resp_0996e78cf50e3160016a59752227708199815f81fa8b5c28fc","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0996e78cf50e3160016a5975269af88199bf6ee595b3f4f5a5","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading lifecycle truth ADR\",\"path\":\"docs/adr/0038-broker-stream-lifecycle-truth.md\",\"selector\":\"1-340\"}","call_id":"call_dicZpyHWoG1tBwuhxHSuvYao","name":"read"}]},"duration":6794.529100000858,"ttft":5135.536499999464,"contextSnapshot":{"promptTokens":69260,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_dicZpyHWoG1tBwuhxHSuvYao|fc_0996e78cf50e3160016a5975269af88199bf6ee595b3f4f5a5","toolName":"read","startedAt":"2026-07-17T00:19:52.261Z","args":{"path":"docs/adr/0038-broker-stream-lifecycle-truth.md"},"intent":"Reading lifecycle truth ADR"},"id":"af272c49","parentId":"f0edc779","timestamp":"2026-07-17T00:19:52.261Z"}
{"type":"message","id":"c0911ac0","parentId":"af272c49","timestamp":"2026-07-17T00:19:52.264Z","message":{"role":"toolResult","toolCallId":"call_dicZpyHWoG1tBwuhxHSuvYao|fc_0996e78cf50e3160016a5975269af88199bf6ee595b3f4f5a5","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)\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"}],"details":{"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)\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","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\adr\\0038-broker-stream-lifecycle-truth.md"}}},"isError":false,"timestamp":1784247592264}}
{"type":"message","id":"936072c3","parentId":"c0911ac0","timestamp":"2026-07-17T00:20:00.518Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting dispatch loop and nethost lifecycle**"},{"type":"toolCall","id":"call_BubQ8qg1qWkYU5hJlIpIwBs8|fc_0996e78cf50e3160016a59752fb1908199abd10a5783d81d94","name":"grep","arguments":{"i":"Finding lifecycle implementation tests","pattern":"REDISPATCH|FINISHED_RETIRE|OPENER_DURABLE|CLAIM_RETRY|finished.*retire|opener.*evict|transient.*claim|refresh.*controller","path":"crates/spt-daemon/src;crates/spt-daemon/tests;crates/spt/tests","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_fZCxFpVUBG9shaBV3MeOlikW|fc_0996e78cf50e3160016a59752fb1a4819986eb38c8db8ae93a","name":"read","arguments":{"i":"Reading stream lifecycle implementation","path":"crates/spt-daemon/src/nethost.rs","selector":"120-220,240-290,435-525,620-705,1365-1465"}},{"type":"toolCall","id":"call_yRo0UEooCPTxsAd3xUAa2J3f|fc_0996e78cf50e3160016a59752fb1ac8199b7956c5059773298","name":"read","arguments":{"i":"Reading dispatcher lifecycle implementation","path":"crates/spt-daemon/src/dispatch.rs","selector":"70-180,180-385,430-560,645-715"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":6233,"output":292,"cacheRead":64000,"cacheWrite":0,"totalTokens":70525,"reasoningTokens":69,"cost":{"input":0.031165,"output":0.00876,"cacheRead":0.032,"cacheWrite":0,"total":0.071925}},"stopReason":"toolUse","timestamp":1784247592276,"responseId":"resp_0996e78cf50e3160016a597528b3508199a21d0563bb341a58","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a59752a44dc819985937da2bd7005fb","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXUvG8rEhTRVCDcz-JEJwfQDQIqRyJDHK-lLq_dJEdx9NitH2g7CmuWchj2Rm-hmeyjaimkf7_e3sv_lNHz8f5bqUJPk5lkWu8_216s-RS8YxIFbxpP-YBb1QOZe7qWLHs9h3kwsrvrjVX0CnWbdEguUk9YD1mGGtvD-5sEf2mg-gL6AaueRurYJDlMuZy9wZJf5-dZ7yeHUuznaHTDA-_b16udWQwtOoFPb229qg6Xf-4DA3IpOiSewCCutR3w_evTee9fvBjWusWqFmNCvfxkMAimyWKQ1DDZJCusx8eYuqrm8wf2dv7F4yBybiucLPI7yxTo_xOk8q0iq0041syvLpmnHSsgKNc7owKKaRiexShcux3DreaD05owb5aFmo26atlLjmnecJWQEC0CpDNgpk70XXesC-vhHLvg5nUmSRiiMenDxJb1AT-mKWp6B9mrM0mSl4A4HMuvXc-kTJH4kd4-jMpUZF6HZRTprG78wAGTagSN5VxVfjmoPkTt9kNf4sKyn0jKq2SjOA860vzZ4J9BMDLa3yazfIRPqVc2n6qjjlRt9pCJgjLcGoUvAknb7Htsj7NvEUqF-NLEJJKuq5dbWDsHBJnBJV8UPsg-n-JDMo75puKBVT1BsIu1h3YF8_MwFNGAZGjdEWE4AsVXUzJmxEalo4UkK6fb5U3Tf1LutLqbgKWsvQ1Tyst_8uwhosn74OgtKGajsJnURGDFfyo3NO12Sv6_hGxsQoxycI-ehORmqadkRTZWgRkEEX9rbHAkB7eBA2D0oFnrcm2i24bgSaAxIvxikGxHkBM2tzP4dBX8UkFYndG8388IfhvMfcxQLA4-5-yId-5ZVEGUd_TVt1h_5gY4UE5p_dZXehKrAGiOE7fGrODl3h2hR1gT_slQ94-rKMw-jQNpxdXIkb3rFwIjVm2PaKQ4Lz2da4A2JcK3Ztbig0UltU5bAjBEs_YLE_ecFQ8dtNGt40S8k04qitFIIgbcZ3kYMi1A1oE0Le62Ywqlv3BE_DUWe_KSduHrdTQfLkSTllLOqODuJK2Nh3t4Xb6Sf4pl_mypwHVtxgiIBhCAAMh_kTA2dnxkOGgU0KiwcfY1be4lORcfbnDm_JBeoUIIPwBdhPkrWktSGZ6Q3gzb_RB6rlBalZNX0URjNiQOVYbQKvVCYigz9ufSq0PYISv1BerB8NNEYJKN3VQw_92u2fjmcwOFdBcvB37scpetiBN1Hq-d3T1MOi-UDnfK8jb2ic5mpbxKEkYHlCQqecF9D3S3CGf4TNtk4QvG03WTsyJDCFRp7r-tLpyH80vKgN3WX77yoQZ5OKG2QtzzFY6h0PC4bV-jtJfvjHpRHt_Xziz-MB_HQ_FN7LsxJrvOca2ZCglQ5OBvQXELh0XFqvyb6IRcS3g0Vul4mmudheiVZAjrTFAZdehi1mw==","summary":[{"type":"summary_text","text":"**Inspecting dispatch loop and nethost lifecycle**"}]},{"id":"fc_0996e78cf50e3160016a59752fb1908199abd10a5783d81d94","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding lifecycle implementation tests\",\"pattern\":\"REDISPATCH|FINISHED_RETIRE|OPENER_DURABLE|CLAIM_RETRY|finished.*retire|opener.*evict|transient.*claim|refresh.*controller\",\"path\":\"crates/spt-daemon/src;crates/spt-daemon/tests;crates/spt/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_BubQ8qg1qWkYU5hJlIpIwBs8","name":"grep"},{"id":"fc_0996e78cf50e3160016a59752fb1a4819986eb38c8db8ae93a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading stream lifecycle implementation\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":\"120-220,240-290,435-525,620-705,1365-1465\"}","call_id":"call_fZCxFpVUBG9shaBV3MeOlikW","name":"read"},{"id":"fc_0996e78cf50e3160016a59752fb1ac8199b7956c5059773298","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatcher lifecycle implementation\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"70-180,180-385,430-560,645-715\"}","call_id":"call_yRo0UEooCPTxsAd3xUAa2J3f","name":"read"}]},"duration":8228.399700000882,"ttft":1983.388400003314,"contextSnapshot":{"promptTokens":70233,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BubQ8qg1qWkYU5hJlIpIwBs8|fc_0996e78cf50e3160016a59752fb1908199abd10a5783d81d94","toolName":"grep","startedAt":"2026-07-17T00:20:00.519Z","args":{"path":"crates/spt-daemon/src;crates/spt-daemon/tests;crates/spt/tests"},"intent":"Finding lifecycle implementation tests"},"id":"9158e23c","parentId":"936072c3","timestamp":"2026-07-17T00:20:00.519Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_fZCxFpVUBG9shaBV3MeOlikW|fc_0996e78cf50e3160016a59752fb1a4819986eb38c8db8ae93a","toolName":"read","startedAt":"2026-07-17T00:20:00.519Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Reading stream lifecycle implementation"},"id":"08d69716","parentId":"9158e23c","timestamp":"2026-07-17T00:20:00.519Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_yRo0UEooCPTxsAd3xUAa2J3f|fc_0996e78cf50e3160016a59752fb1ac8199b7956c5059773298","toolName":"read","startedAt":"2026-07-17T00:20:00.519Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading dispatcher lifecycle implementation"},"id":"3fbc2ee4","parentId":"08d69716","timestamp":"2026-07-17T00:20:00.519Z"}
{"type":"message","id":"c46cf5b6","parentId":"3fbc2ee4","timestamp":"2026-07-17T00:20:00.545Z","message":{"role":"toolResult","toolCallId":"call_fZCxFpVUBG9shaBV3MeOlikW|fc_0996e78cf50e3160016a59752fb1a4819986eb38c8db8ae93a","toolName":"read","content":[{"type":"text","text":"const OPENER_PIN_MAX: usize = 64 * 1024;\n\n/// Restart-durable opener capture (ADR-0038 B, REQ-STREAM-OPENER-DURABLE):\n/// the stream's complete first NDJSON line, pinned OUTSIDE the evictable data\n/// ring and held until the stream row goes away. Recovery classifies from\n/// this fact, never from ring seq 0 (which a bounded ring evicts under\n/// traffic — the abandoned-active-stream leg of the hertz RCA).\nenum OpenerCapture {\n    /// Still hunting the first `\\n` — holds every byte seen so far.\n    Buffering(Vec<u8>),\n    /// The complete first line (newline excluded), immutable from here on.\n    Pinned(Vec<u8>),\n    /// No newline within [`OPENER_PIN_MAX`] — not an NDJSON opener; give up\n    /// bounded (classification reads this as \"no opener\").\n    Oversize,\n}\n\n/// Transport backing a held connection. A real `Quic` connection (the only\n/// kind an inbound handshake can produce), or a broker-minted `Loopback` —\n/// an in-process connection with NO iroh handshake whose identity IS this node,\n/// established BY CONSTRUCTION. A loopback conn can ONLY be minted broker-side\n/// via [`NetHost::dial_loopback`]; it can never arise from an inbound handshake\n/// (a remote always presents its own key and cannot claim to be us). This is\n/// what lets a LOCAL `spt rc` ride the byte-identical `serve_attach` pump (M12\n/// W1.5, B1) — QUIC self-dial is refused by iroh (\"Connecting to ourself is not\n/// supported\"), so local attach rides a broker loopback conn instead.\nenum ConnKind {\n    /// A real QUIC connection — kept alive by ownership here; dropping it closes\n    /// the connection. Cloned (cheap handle) for stream opens.\n    Quic(Connection),\n    /// A broker-minted in-process loopback (no iroh handle, no handshake). Its\n    /// streams are minted eagerly in cross-wired pairs by [`NetHost::open_stream`];\n    /// there is no accept loop and no closed-watcher (it lives for the host).\n    Loopback,\n}\n\n/// One live connection the broker holds (QUIC or loopback).\nstruct ConnEntry {\n    /// The transport backing this connection.\n    kind: ConnKind,\n    /// The remote node's stable name (hex pubkey). For QUIC, proven by the\n    /// handshake; for loopback, THIS node's own id (the broker-minted identity).\n    remote_id_hex: String,\n    /// The subnets the peer proved current-epoch seed-knowledge of at connect\n    /// (Mesh-D2, REQ-MESH-1). Empty when seed-proof is disabled (`membership:\n    /// None`). The D5 gate swap reads this in place of `is_trusted(subnet, …)`;\n    /// at D2 it is cached but not yet consumed (the `is_trusted` gates still run\n    /// in parallel — belt + suspenders, no regression).\n    proven_subnets: HashSet<String>,\n}\n\n/// A bounded, sequenced read log for one QUIC stream — the PTY `OutputLog`\n/// discipline applied to the network (D4b): the stream's single read pump is\n/// the only producer; a brain connection attaches as the subscriber; every\n/// subscriber send happens under the enclosing `Mutex`, so live chunks and a\n/// replay can never interleave out of `seq` order. Chunks produced while no\n/// brain is attached sit in the ring and replay on the next subscribe — a\n/// brain restart loses nothing (Spike #3 invariant B/D).\n// [impl->REQ-DAEMON-2]\nstruct StreamLog {\n    stream_id: u64,\n    /// TRANSPORT read counter: bumped once per [`append`](Self::append) (one\n    /// read-pump `read_chunk`), INDEPENDENT of byte size. NOT a logical-chunk\n    /// count — QUIC may coalesce several sender writes into one receiver\n    /// `read_chunk` (or split one), so `next_seq` can be < the number of logical\n    /// chunks the sender issued for the SAME bytes. Correctness (byte-exact +\n    /// exactly-once) holds via these transport seqs + the recv cursor regardless;\n    /// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows\n    /// coalescing flake) and must gate on BYTES instead.\n    next_seq: u64,\n    ring: VecDeque<(u64, Vec<u8>)>,\n    cap_chunks: usize,\n    subscriber: Option<SharedSend>,\n    /// Read side finished cleanly (peer called finish / conn ended).\n    finished: bool,\n    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n    /// congestion surfaces as lag, never loss.\" So a retentive log never cap-evicts\n    /// on append (no loss), and the read pump REFUSES to pull the next chunk once\n    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the\n    /// transport's native flow-control then lags the *sender*. A chunk leaves the\n    /// ring ONLY when [`drain`](Self::drain) (the recv cursor) pops it; the pump is\n    /// woken to resume via the entry's `room` notifier. Ordinary streams keep\n    /// `retentive_cap = 0` (a slow/absent brain must not OOM the broker via an\n    /// unbounded ring) and are drained by subscribe, never the sync cursor.\n    retentive_cap: usize,\n    /// The pinned first NDJSON line (ADR-0038 B) — one bounded side fact per\n    /// stream, independent of the ring's eviction. Fed by [`append`](Self::append).\n    opener: OpenerCapture,\n}\n\nimpl StreamLog {\n    fn new(stream_id: u64, cap_chunks: usize) -> Self {\n        StreamLog {\n            stream_id,\n            next_seq: 0,\n            ring: VecDeque::new(),\n            cap_chunks,\n            subscriber: None,\n            finished: false,\n…\n        }\n    }\n…\n    fn floor_seq(&self) -> u64 {\n…\n    }\n\n    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\n    fn set_retentive(&mut self, cap: usize) {\n        self.retentive_cap = cap;\n    }\n\n    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\n    /// live-send to the current subscriber. Read-pump only. A non-retentive log\n    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\n    /// never evicts — bytes leave only via [`drain`](Self::drain).\n    fn append(&mut self, bytes: &[u8]) {\n        // Opener capture (ADR-0038 B): pin the first NDJSON line as it streams\n        // past, before ring bounds can ever evict it. One-shot: `Pinned` and\n        // `Oversize` are terminal states.\n        // [impl->REQ-STREAM-OPENER-DURABLE]\n        if let OpenerCapture::Buffering(buf) = &mut self.opener {\n            match bytes.iter().position(|&b| b == b'\\n') {\n                Some(nl) => {\n                    buf.extend_from_slice(&bytes[..nl]);\n                    let line = std::mem::take(buf);\n                    self.opener = if line.len() > OPENER_PIN_MAX {\n                        OpenerCapture::Oversize\n                    } else {\n                        OpenerCapture::Pinned(line)\n                    };\n                }\n                None => {\n                    buf.extend_from_slice(bytes);\n                    if buf.len() > OPENER_PIN_MAX {\n                        self.opener = OpenerCapture::Oversize;\n                    }\n                }\n            }\n        }\n        let seq = self.next_seq;\n        self.next_seq += 1;\n        self.ring.push_back((seq, bytes.to_vec()));\n        if self.retentive_cap == 0 {\n            while self.ring.len() > self.cap_chunks {\n                self.ring.pop_front();\n            }\n        }\n        if let Some(sub) = &self.subscriber {\n            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n…\n}\n…\nimpl PresenceLog {\n…\n    fn push(&mut self, mut ev: NetPresenceEvent) {\n…\n        self.next_seq += 1;\n        self.ring.push_back(ev.clone());\n        while self.ring.len() > self.cap_events {\n            self.ring.pop_front();\n        }\n        if let Some(sub) = &self.subscriber {\n            let frame = net_presence_event_envelope(&ev);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Attach `sub` as the subscriber, replaying every retained event with\n    /// `seq >= from_seq` before live streaming (the gapless-resume attach).\n    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n        // Attribution (F-039 legs b+d): presence-subscriber role fact + attach\n        // record — the OTHER RCA-pinned shared carrier (the per-dial-failure\n        // presence push rides this conn). [impl->REQ-CONN-POISON-ATTRIBUTION]\n        sub.describe(\"presence-subscriber\");\n        sub.lifecycle_event(\"presence-sub-attach\", &format!(\"from_seq={from_seq}\"));\n        self.subscriber = Some(Arc::clone(&sub));\n        for ev in self.ring.iter().filter(|e| e.seq >= from_seq) {\n            let frame = net_presence_event_envelope(ev);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n    /// not unsubscribe a newer brain).\n    fn detach_if(&mut self, sub: &SharedSend) {\n        if let Some(cur) = &self.subscriber {\n            if Arc::ptr_eq(cur, sub) {\n                self.subscriber = None;\n            }\n        }\n    }\n}\n\n/// A stream's send half — the same two transports as [`ConnKind`]. The\n/// `serve_attach`/operator pump and the `StreamLog` read discipline are\n/// IDENTICAL across both; only this leaf write differs (QUIC stream vs the\n/// write end of an in-process duplex). That is the M12 W1.5 one-pump invariant\n/// made structural: a loopback attach and a cross-node attach diverge at this\n/// leaf and nowhere above it.\nenum SendHalf {\n    Quic(SendStream),\n    Loopback(WriteHalf<DuplexStream>),\n}\n\nimpl SendHalf {\n    /// Write all `bytes` (no-op on empty). Runs on the host runtime.\n    async fn write_all(&mut self, bytes: &[u8]) -> io::Result<()> {\n        match self {\n            SendHalf::Quic(s) => s\n                .write_all(bytes)\n                .await\n                .map_err(|e| io::Error::other(e.to_string())),\n            SendHalf::Loopback(w) => w.write_all(bytes).await,\n        }\n    }\n\n    /// Close the send side so the peer's read pump sees a clean end (QUIC\n    /// `finish` / duplex `shutdown`).\n    async fn finish(&mut self) -> io::Result<()> {\n        match self {\n            SendHalf::Quic(s) => s.finish().map_err(|e| io::Error::other(e.to_string())),\n            SendHalf::Loopback(w) => w.shutdown().await,\n        }\n    }\n}\n\n/// A stream's read half — fed into the single read pump in [`register_stream`].\n/// QUIC reads via quinn's chunk API; loopback via the read end of the duplex.\n/// Both append to the same [`StreamLog`], so the subscriber (and everything\n/// above it) cannot tell the transports apart.\nenum RecvHalf {\n    Quic(RecvStream),\n    Loopback(ReadHalf<DuplexStream>),\n}\n\n/// One live stream the broker holds (QUIC or loopback): the send half (brain\n/// writes ride it over IPC) + the sequenced read log.\nstruct StreamEntry {\n    conn_id: u64,\n    remote_id_hex: String,\n    /// Whether THIS node opened the stream (`open_stream`) vs the peer\n…\n}\n…\nimpl DialPlan {\n…\n    /// `bounded_block_on`; the submit via its own `timeout`). `stage` names the\n    /// pipeline stage the dial is in (ADR-0039 Decision 5) — a stage-local\n    /// error and the caller's outer timeout both read it, so the failure token\n    /// says WHERE the dial died instead of collapsing six stages into one.\n    // [impl->REQ-PUMP-STAGE-TRUTH]\n    async fn connect(self, addr: EndpointAddr, stage: &DialStage) -> io::Result<(u64, String)> {\n        stage.enter(spt_seedproof_stage::STAGE_QUIC_CONNECT);\n        let conn = self.endpoint.connect(addr).await.map_err(|e| {\n            let msg = e.to_string();\n            if msg.to_ascii_lowercase().contains(\"alpn\") {\n                stage.enter(spt_seedproof_stage::STAGE_ALPN);\n            }\n            io::Error::other(msg)\n        })?;\n        let proven = match self.membership {\n            Some(src) => {\n                let creds = src();\n                prove_membership(\n                    &conn,\n                    self.local_id,\n                    ProofRole::Dialer,\n                    &creds,\n                    self.roster_rx.as_ref(),\n                    &self.self_addr,\n                    stage,\n                )\n                .await\n                .ok_or_else(|| {\n                    io::Error::other(\"seed-proof failed: peer is not a subnet member\")\n                })?\n            }\n            None => HashSet::new(),\n        };\n        Ok(register_conn(&self.shared, conn, proven))\n    }\n}\n\n// Local alias so the stage constants read namespaced at the use sites without\n// a wildcard import.\nuse crate::seedproofx as spt_seedproof_stage;\nuse crate::seedproofx::DialStage;\n\nfn register_conn(\n    shared: &Arc<NetShared>,\n    conn: Connection,\n    proven_subnets: HashSet<String>,\n) -> (u64, String) {\n    let remote_id_hex = PublicKey::from_bytes(conn.remote_id().as_bytes())\n        .expect(\"an iroh EndpointId is always a valid ed25519 public key\")\n        .to_hex();\n    let id = shared.next_conn_id.fetch_add(1, Ordering::Relaxed);\n    shared.conns.lock().unwrap().insert(\n        id,\n        ConnEntry {\n            kind: ConnKind::Quic(conn.clone()),\n            remote_id_hex: remote_id_hex.clone(),\n            proven_subnets,\n        },\n    );\n\n    // Presence: announce the row BEFORE spawning the closed-watcher, so the\n    // `connected` event can never race behind its own `disconnected` (D4c). The\n    // event carries the OBSERVED remote addr (REQ-CONV-1 seed write-back) so a\n    // non-blocking submit-dial seeds `peer-addrs.json` like the blocking reply\n    // did (id-only / path-less → `null`, nothing worth seeding).\n    let observed = observed_addr_json(&conn);\n    shared\n        .presence\n        .lock()\n        .unwrap()\n        .append_connected(id, &remote_id_hex, observed);\n\n    // Closed-watcher: the row's exit path. Awaits the connection's end (clean\n    // close, conn loss, or our own endpoint close), removes the row — conns\n    // finally LEAVE the table — then appends the `disconnected` event (row\n    // gone before the event is observable).\n    // [impl->REQ-EP-4]\n    let sh_close = Arc::clone(shared);\n    let conn_close = conn.clone();\n    let hex_close = remote_id_hex.clone();\n    tokio::spawn(async move {\n        conn_close.closed().await;\n        sh_close.conns.lock().unwrap().remove(&id);\n        // Retirement (ADR-0038 A): a dead conn's stream rows are transport-dead\n        // — nothing can ever be served on them again, and leaving them makes\n        // every future dispatcher generation replay them (the control-steal\n…\n    });\n…\n}\n…\n    pub fn dial_loopback(&self) -> (u64, String) {\n…\n        let mut slot = self.loopback_conn.lock().unwrap();\n        if let Some(id) = *slot {\n            return (id, remote_id_hex);\n        }\n        let id = self.shared.next_conn_id.fetch_add(1, Ordering::Relaxed);\n        self.shared.conns.lock().unwrap().insert(\n            id,\n            ConnEntry {\n                kind: ConnKind::Loopback,\n                remote_id_hex: remote_id_hex.clone(),\n                proven_subnets: HashSet::new(),\n            },\n        );\n        *slot = Some(id);\n        (id, remote_id_hex)\n    }\n\n    /// Open a bidi stream on a held connection, registering it (with its read\n    /// pump) in the stream table (D4b). Note QUIC streams are lazy: the peer\n    /// sees the stream on its first write.\n    ///\n    /// On a [`ConnKind::Loopback`] conn there is no peer to accept the stream,\n    /// so the open mints a CROSS-WIRED PAIR in-process: an operator row\n    /// (`initiated_locally = true`, the returned id) and a peer row\n    /// (`initiated_locally = false`) the inbound dispatcher claims and serves via\n    /// `serve_attach` — exactly as an `accept_bi` would surface a remote-opened\n    /// stream. One `tokio::io::duplex` wires both directions; each row's read\n    /// pump and `StreamLog` are the SAME machinery the QUIC path uses (the W1.5\n    /// one-pump invariant). The peer row's `remote_id_hex` is this node's own id,\n    /// so its serve gates same-node.\n    // [impl->REQ-DAEMON-2]\n    // [impl->REQ-RC-1]\n    pub fn open_stream(&self, conn_id: u64) -> io::Result<u64> {\n        enum Backing {\n            Quic(Connection),\n            Loopback,\n        }\n        let (backing, remote_id_hex) = {\n            let conns = self.shared.conns.lock().unwrap();\n            let e = conns.get(&conn_id).ok_or_else(|| {\n                io::Error::new(io::ErrorKind::NotFound, format!(\"no such conn {conn_id}\"))\n            })?;\n            let backing = match &e.kind {\n                ConnKind::Quic(c) => Backing::Quic(c.clone()),\n                ConnKind::Loopback => Backing::Loopback,\n            };\n            (backing, e.remote_id_hex.clone())\n        };\n        let shared = Arc::clone(&self.shared);\n        match backing {\n            // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): `open_bi` on a black-holed\n            // peer can block on stream flow-control credit the dead peer never grants.\n            Backing::Quic(conn) => self.bounded_block_on(async move {\n                let (send, recv) = conn\n                    .open_bi()\n                    .await\n                    .map_err(|e| io::Error::other(e.to_string()))?;\n                Ok(register_stream(\n                    &shared,\n                    conn_id,\n                    &remote_id_hex,\n                    SendHalf::Quic(send),\n                    RecvHalf::Quic(recv),\n                    true,\n                ))\n            }),\n            // Bounded like the QUIC arm (REQ-BROKER-ATTACH-JOURNAL-RESILIENT, bug\n            // #16): the loopback open is all in-memory and normally returns\n            // instantly, but a SICK NetHost runtime (saturated/wedged after a\n            // self-update brain-respawn) would otherwise hang this journaled op with\n            // no deadline — killing the per-conn reply thread so every attach\n            // silently deadlines (\"brain IPC read deadline elapsed\") while\n            // non-journaled ops keep working. `bounded_block_on` turns that into a\n            // prompt ordinary error frame; the bound never bites a healthy runtime.\n            // [impl->REQ-BROKER-ATTACH-JOURNAL-RESILIENT]\n            Backing::Loopback => self.bounded_block_on(async move {\n                // One duplex carries BOTH directions: operator writes reach the\n                // peer's read pump and vice-versa. Split each end into a read +\n                // write half so each row owns exactly its leg.\n                let (op_io, peer_io) = tokio::io::duplex(STREAM_READ_CHUNK);\n                let (op_r, op_w) = tokio::io::split(op_io);\n                let (peer_r, peer_w) = tokio::io::split(peer_io);\n                // Register the peer (inbound) row first so it is in the table\n                // before the operator can write its Request onto the wire — the\n                // dispatcher must never miss the stream.\n                register_stream(\n                    &shared,\n                    conn_id,\n                    &remote_id_hex,\n                    SendHalf::Loopback(peer_w),\n                    RecvHalf::Loopback(peer_r),\n                    false,\n                );\n                Ok(register_stream(\n                    &shared,\n                    conn_id,\n                    &remote_id_hex,\n                    SendHalf::Loopback(op_w),\n                    RecvHalf::Loopback(op_r),\n                    true,\n                ))\n            }),\n        }\n    }"}],"details":{"displayContent":{"text":"const OPENER_PIN_MAX: usize = 64 * 1024;\n\n/// Restart-durable opener capture (ADR-0038 B, REQ-STREAM-OPENER-DURABLE):\n/// the stream's complete first NDJSON line, pinned OUTSIDE the evictable data\n/// ring and held until the stream row goes away. Recovery classifies from\n/// this fact, never from ring seq 0 (which a bounded ring evicts under\n/// traffic — the abandoned-active-stream leg of the hertz RCA).\nenum OpenerCapture {\n    /// Still hunting the first `\\n` — holds every byte seen so far.\n    Buffering(Vec<u8>),\n    /// The complete first line (newline excluded), immutable from here on.\n    Pinned(Vec<u8>),\n    /// No newline within [`OPENER_PIN_MAX`] — not an NDJSON opener; give up\n    /// bounded (classification reads this as \"no opener\").\n    Oversize,\n}\n\n/// Transport backing a held connection. A real `Quic` connection (the only\n/// kind an inbound handshake can produce), or a broker-minted `Loopback` —\n/// an in-process connection with NO iroh handshake whose identity IS this node,\n/// established BY CONSTRUCTION. A loopback conn can ONLY be minted broker-side\n/// via [`NetHost::dial_loopback`]; it can never arise from an inbound handshake\n/// (a remote always presents its own key and cannot claim to be us). This is\n/// what lets a LOCAL `spt rc` ride the byte-identical `serve_attach` pump (M12\n/// W1.5, B1) — QUIC self-dial is refused by iroh (\"Connecting to ourself is not\n/// supported\"), so local attach rides a broker loopback conn instead.\nenum ConnKind {\n    /// A real QUIC connection — kept alive by ownership here; dropping it closes\n    /// the connection. Cloned (cheap handle) for stream opens.\n    Quic(Connection),\n    /// A broker-minted in-process loopback (no iroh handle, no handshake). Its\n    /// streams are minted eagerly in cross-wired pairs by [`NetHost::open_stream`];\n    /// there is no accept loop and no closed-watcher (it lives for the host).\n    Loopback,\n}\n\n/// One live connection the broker holds (QUIC or loopback).\nstruct ConnEntry {\n    /// The transport backing this connection.\n    kind: ConnKind,\n    /// The remote node's stable name (hex pubkey). For QUIC, proven by the\n    /// handshake; for loopback, THIS node's own id (the broker-minted identity).\n    remote_id_hex: String,\n    /// The subnets the peer proved current-epoch seed-knowledge of at connect\n    /// (Mesh-D2, REQ-MESH-1). Empty when seed-proof is disabled (`membership:\n    /// None`). The D5 gate swap reads this in place of `is_trusted(subnet, …)`;\n    /// at D2 it is cached but not yet consumed (the `is_trusted` gates still run\n    /// in parallel — belt + suspenders, no regression).\n    proven_subnets: HashSet<String>,\n}\n\n/// A bounded, sequenced read log for one QUIC stream — the PTY `OutputLog`\n/// discipline applied to the network (D4b): the stream's single read pump is\n/// the only producer; a brain connection attaches as the subscriber; every\n/// subscriber send happens under the enclosing `Mutex`, so live chunks and a\n/// replay can never interleave out of `seq` order. Chunks produced while no\n/// brain is attached sit in the ring and replay on the next subscribe — a\n/// brain restart loses nothing (Spike #3 invariant B/D).\n// [impl->REQ-DAEMON-2]\nstruct StreamLog {\n    stream_id: u64,\n    /// TRANSPORT read counter: bumped once per [`append`](Self::append) (one\n    /// read-pump `read_chunk`), INDEPENDENT of byte size. NOT a logical-chunk\n    /// count — QUIC may coalesce several sender writes into one receiver\n    /// `read_chunk` (or split one), so `next_seq` can be < the number of logical\n    /// chunks the sender issued for the SAME bytes. Correctness (byte-exact +\n    /// exactly-once) holds via these transport seqs + the recv cursor regardless;\n    /// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows\n    /// coalescing flake) and must gate on BYTES instead.\n    next_seq: u64,\n    ring: VecDeque<(u64, Vec<u8>)>,\n    cap_chunks: usize,\n    subscriber: Option<SharedSend>,\n    /// Read side finished cleanly (peer called finish / conn ended).\n    finished: bool,\n    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n    /// congestion surfaces as lag, never loss.\" So a retentive log never cap-evicts\n    /// on append (no loss), and the read pump REFUSES to pull the next chunk once\n    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the\n    /// transport's native flow-control then lags the *sender*. A chunk leaves the\n    /// ring ONLY when [`drain`](Self::drain) (the recv cursor) pops it; the pump is\n    /// woken to resume via the entry's `room` notifier. Ordinary streams keep\n    /// `retentive_cap = 0` (a slow/absent brain must not OOM the broker via an\n    /// unbounded ring) and are drained by subscribe, never the sync cursor.\n    retentive_cap: usize,\n    /// The pinned first NDJSON line (ADR-0038 B) — one bounded side fact per\n    /// stream, independent of the ring's eviction. Fed by [`append`](Self::append).\n    opener: OpenerCapture,\n}\n\nimpl StreamLog {\n    fn new(stream_id: u64, cap_chunks: usize) -> Self {\n        StreamLog {\n            stream_id,\n            next_seq: 0,\n            ring: VecDeque::new(),\n            cap_chunks,\n            subscriber: None,\n            finished: false,\n…\n        }\n    }\n…\n    fn floor_seq(&self) -> u64 {\n…\n    }\n\n    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\n    fn set_retentive(&mut self, cap: usize) {\n        self.retentive_cap = cap;\n    }\n\n    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\n    /// live-send to the current subscriber. Read-pump only. A non-retentive log\n    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\n    /// never evicts — bytes leave only via [`drain`](Self::drain).\n    fn append(&mut self, bytes: &[u8]) {\n        // Opener capture (ADR-0038 B): pin the first NDJSON line as it streams\n        // past, before ring bounds can ever evict it. One-shot: `Pinned` and\n        // `Oversize` are terminal states.\n        // [impl->REQ-STREAM-OPENER-DURABLE]\n        if let OpenerCapture::Buffering(buf) = &mut self.opener {\n            match bytes.iter().position(|&b| b == b'\\n') {\n                Some(nl) => {\n                    buf.extend_from_slice(&bytes[..nl]);\n                    let line = std::mem::take(buf);\n                    self.opener = if line.len() > OPENER_PIN_MAX {\n                        OpenerCapture::Oversize\n                    } else {\n                        OpenerCapture::Pinned(line)\n                    };\n                }\n                None => {\n                    buf.extend_from_slice(bytes);\n                    if buf.len() > OPENER_PIN_MAX {\n                        self.opener = OpenerCapture::Oversize;\n                    }\n                }\n            }\n        }\n        let seq = self.next_seq;\n        self.next_seq += 1;\n        self.ring.push_back((seq, bytes.to_vec()));\n        if self.retentive_cap == 0 {\n            while self.ring.len() > self.cap_chunks {\n                self.ring.pop_front();\n            }\n        }\n        if let Some(sub) = &self.subscriber {\n            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n…\n}\n…\nimpl PresenceLog {\n…\n    fn push(&mut self, mut ev: NetPresenceEvent) {\n…\n        self.next_seq += 1;\n        self.ring.push_back(ev.clone());\n        while self.ring.len() > self.cap_events {\n            self.ring.pop_front();\n        }\n        if let Some(sub) = &self.subscriber {\n            let frame = net_presence_event_envelope(&ev);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Attach `sub` as the subscriber, replaying every retained event with\n    /// `seq >= from_seq` before live streaming (the gapless-resume attach).\n    fn attach(&mut self, sub: SharedSend, from_seq: u64) {\n        // Attribution (F-039 legs b+d): presence-subscriber role fact + attach\n        // record — the OTHER RCA-pinned shared carrier (the per-dial-failure\n        // presence push rides this conn). [impl->REQ-CONN-POISON-ATTRIBUTION]\n        sub.describe(\"presence-subscriber\");\n        sub.lifecycle_event(\"presence-sub-attach\", &format!(\"from_seq={from_seq}\"));\n        self.subscriber = Some(Arc::clone(&sub));\n        for ev in self.ring.iter().filter(|e| e.seq >= from_seq) {\n            let frame = net_presence_event_envelope(ev);\n            // Bounded conn write (round-4, REQ-HAZARD-SHAREDSEND-NO-BLOCKING-\n            // WRITE-UNDER-LOCK): a wedged subscriber conn poisons + retires at\n            // the deadline instead of parking this producer.\n            let _ = sub.write(&frame);\n        }\n    }\n\n    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n    /// not unsubscribe a newer brain).\n    fn detach_if(&mut self, sub: &SharedSend) {\n        if let Some(cur) = &self.subscriber {\n            if Arc::ptr_eq(cur, sub) {\n                self.subscriber = None;\n            }\n        }\n    }\n}\n\n/// A stream's send half — the same two transports as [`ConnKind`]. The\n/// `serve_attach`/operator pump and the `StreamLog` read discipline are\n/// IDENTICAL across both; only this leaf write differs (QUIC stream vs the\n/// write end of an in-process duplex). That is the M12 W1.5 one-pump invariant\n/// made structural: a loopback attach and a cross-node attach diverge at this\n/// leaf and nowhere above it.\nenum SendHalf {\n    Quic(SendStream),\n    Loopback(WriteHalf<DuplexStream>),\n}\n\nimpl SendHalf {\n    /// Write all `bytes` (no-op on empty). Runs on the host runtime.\n    async fn write_all(&mut self, bytes: &[u8]) -> io::Result<()> {\n        match self {\n            SendHalf::Quic(s) => s\n                .write_all(bytes)\n                .await\n                .map_err(|e| io::Error::other(e.to_string())),\n            SendHalf::Loopback(w) => w.write_all(bytes).await,\n        }\n    }\n\n    /// Close the send side so the peer's read pump sees a clean end (QUIC\n    /// `finish` / duplex `shutdown`).\n    async fn finish(&mut self) -> io::Result<()> {\n        match self {\n            SendHalf::Quic(s) => s.finish().map_err(|e| io::Error::other(e.to_string())),\n            SendHalf::Loopback(w) => w.shutdown().await,\n        }\n    }\n}\n\n/// A stream's read half — fed into the single read pump in [`register_stream`].\n/// QUIC reads via quinn's chunk API; loopback via the read end of the duplex.\n/// Both append to the same [`StreamLog`], so the subscriber (and everything\n/// above it) cannot tell the transports apart.\nenum RecvHalf {\n    Quic(RecvStream),\n    Loopback(ReadHalf<DuplexStream>),\n}\n\n/// One live stream the broker holds (QUIC or loopback): the send half (brain\n/// writes ride it over IPC) + the sequenced read log.\nstruct StreamEntry {\n    conn_id: u64,\n    remote_id_hex: String,\n    /// Whether THIS node opened the stream (`open_stream`) vs the peer\n…\n}\n…\nimpl DialPlan {\n…\n    /// `bounded_block_on`; the submit via its own `timeout`). `stage` names the\n    /// pipeline stage the dial is in (ADR-0039 Decision 5) — a stage-local\n    /// error and the caller's outer timeout both read it, so the failure token\n    /// says WHERE the dial died instead of collapsing six stages into one.\n    // [impl->REQ-PUMP-STAGE-TRUTH]\n    async fn connect(self, addr: EndpointAddr, stage: &DialStage) -> io::Result<(u64, String)> {\n        stage.enter(spt_seedproof_stage::STAGE_QUIC_CONNECT);\n        let conn = self.endpoint.connect(addr).await.map_err(|e| {\n            let msg = e.to_string();\n            if msg.to_ascii_lowercase().contains(\"alpn\") {\n                stage.enter(spt_seedproof_stage::STAGE_ALPN);\n            }\n            io::Error::other(msg)\n        })?;\n        let proven = match self.membership {\n            Some(src) => {\n                let creds = src();\n                prove_membership(\n                    &conn,\n                    self.local_id,\n                    ProofRole::Dialer,\n                    &creds,\n                    self.roster_rx.as_ref(),\n                    &self.self_addr,\n                    stage,\n                )\n                .await\n                .ok_or_else(|| {\n                    io::Error::other(\"seed-proof failed: peer is not a subnet member\")\n                })?\n            }\n            None => HashSet::new(),\n        };\n        Ok(register_conn(&self.shared, conn, proven))\n    }\n}\n\n// Local alias so the stage constants read namespaced at the use sites without\n// a wildcard import.\nuse crate::seedproofx as spt_seedproof_stage;\nuse crate::seedproofx::DialStage;\n\nfn register_conn(\n    shared: &Arc<NetShared>,\n    conn: Connection,\n    proven_subnets: HashSet<String>,\n) -> (u64, String) {\n    let remote_id_hex = PublicKey::from_bytes(conn.remote_id().as_bytes())\n        .expect(\"an iroh EndpointId is always a valid ed25519 public key\")\n        .to_hex();\n    let id = shared.next_conn_id.fetch_add(1, Ordering::Relaxed);\n    shared.conns.lock().unwrap().insert(\n        id,\n        ConnEntry {\n            kind: ConnKind::Quic(conn.clone()),\n            remote_id_hex: remote_id_hex.clone(),\n            proven_subnets,\n        },\n    );\n\n    // Presence: announce the row BEFORE spawning the closed-watcher, so the\n    // `connected` event can never race behind its own `disconnected` (D4c). The\n    // event carries the OBSERVED remote addr (REQ-CONV-1 seed write-back) so a\n    // non-blocking submit-dial seeds `peer-addrs.json` like the blocking reply\n    // did (id-only / path-less → `null`, nothing worth seeding).\n    let observed = observed_addr_json(&conn);\n    shared\n        .presence\n        .lock()\n        .unwrap()\n        .append_connected(id, &remote_id_hex, observed);\n\n    // Closed-watcher: the row's exit path. Awaits the connection's end (clean\n    // close, conn loss, or our own endpoint close), removes the row — conns\n    // finally LEAVE the table — then appends the `disconnected` event (row\n    // gone before the event is observable).\n    // [impl->REQ-EP-4]\n    let sh_close = Arc::clone(shared);\n    let conn_close = conn.clone();\n    let hex_close = remote_id_hex.clone();\n    tokio::spawn(async move {\n        conn_close.closed().await;\n        sh_close.conns.lock().unwrap().remove(&id);\n        // Retirement (ADR-0038 A): a dead conn's stream rows are transport-dead\n        // — nothing can ever be served on them again, and leaving them makes\n        // every future dispatcher generation replay them (the control-steal\n…\n    });\n…\n}\n…\n    pub fn dial_loopback(&self) -> (u64, String) {\n…\n        let mut slot = self.loopback_conn.lock().unwrap();\n        if let Some(id) = *slot {\n            return (id, remote_id_hex);\n        }\n        let id = self.shared.next_conn_id.fetch_add(1, Ordering::Relaxed);\n        self.shared.conns.lock().unwrap().insert(\n            id,\n            ConnEntry {\n                kind: ConnKind::Loopback,\n                remote_id_hex: remote_id_hex.clone(),\n                proven_subnets: HashSet::new(),\n            },\n        );\n        *slot = Some(id);\n        (id, remote_id_hex)\n    }\n\n    /// Open a bidi stream on a held connection, registering it (with its read\n    /// pump) in the stream table (D4b). Note QUIC streams are lazy: the peer\n    /// sees the stream on its first write.\n    ///\n    /// On a [`ConnKind::Loopback`] conn there is no peer to accept the stream,\n    /// so the open mints a CROSS-WIRED PAIR in-process: an operator row\n    /// (`initiated_locally = true`, the returned id) and a peer row\n    /// (`initiated_locally = false`) the inbound dispatcher claims and serves via\n    /// `serve_attach` — exactly as an `accept_bi` would surface a remote-opened\n    /// stream. One `tokio::io::duplex` wires both directions; each row's read\n    /// pump and `StreamLog` are the SAME machinery the QUIC path uses (the W1.5\n    /// one-pump invariant). The peer row's `remote_id_hex` is this node's own id,\n    /// so its serve gates same-node.\n    // [impl->REQ-DAEMON-2]\n    // [impl->REQ-RC-1]\n    pub fn open_stream(&self, conn_id: u64) -> io::Result<u64> {\n        enum Backing {\n            Quic(Connection),\n            Loopback,\n        }\n        let (backing, remote_id_hex) = {\n            let conns = self.shared.conns.lock().unwrap();\n            let e = conns.get(&conn_id).ok_or_else(|| {\n                io::Error::new(io::ErrorKind::NotFound, format!(\"no such conn {conn_id}\"))\n            })?;\n            let backing = match &e.kind {\n                ConnKind::Quic(c) => Backing::Quic(c.clone()),\n                ConnKind::Loopback => Backing::Loopback,\n            };\n            (backing, e.remote_id_hex.clone())\n        };\n        let shared = Arc::clone(&self.shared);\n        match backing {\n            // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): `open_bi` on a black-holed\n            // peer can block on stream flow-control credit the dead peer never grants.\n            Backing::Quic(conn) => self.bounded_block_on(async move {\n                let (send, recv) = conn\n                    .open_bi()\n                    .await\n                    .map_err(|e| io::Error::other(e.to_string()))?;\n                Ok(register_stream(\n                    &shared,\n                    conn_id,\n                    &remote_id_hex,\n                    SendHalf::Quic(send),\n                    RecvHalf::Quic(recv),\n                    true,\n                ))\n            }),\n            // Bounded like the QUIC arm (REQ-BROKER-ATTACH-JOURNAL-RESILIENT, bug\n            // #16): the loopback open is all in-memory and normally returns\n            // instantly, but a SICK NetHost runtime (saturated/wedged after a\n            // self-update brain-respawn) would otherwise hang this journaled op with\n            // no deadline — killing the per-conn reply thread so every attach\n            // silently deadlines (\"brain IPC read deadline elapsed\") while\n            // non-journaled ops keep working. `bounded_block_on` turns that into a\n            // prompt ordinary error frame; the bound never bites a healthy runtime.\n            // [impl->REQ-BROKER-ATTACH-JOURNAL-RESILIENT]\n            Backing::Loopback => self.bounded_block_on(async move {\n                // One duplex carries BOTH directions: operator writes reach the\n                // peer's read pump and vice-versa. Split each end into a read +\n                // write half so each row owns exactly its leg.\n                let (op_io, peer_io) = tokio::io::duplex(STREAM_READ_CHUNK);\n                let (op_r, op_w) = tokio::io::split(op_io);\n                let (peer_r, peer_w) = tokio::io::split(peer_io);\n                // Register the peer (inbound) row first so it is in the table\n                // before the operator can write its Request onto the wire — the\n                // dispatcher must never miss the stream.\n                register_stream(\n                    &shared,\n                    conn_id,\n                    &remote_id_hex,\n                    SendHalf::Loopback(peer_w),\n                    RecvHalf::Loopback(peer_r),\n                    false,\n                );\n                Ok(register_stream(\n                    &shared,\n                    conn_id,\n                    &remote_id_hex,\n                    SendHalf::Loopback(op_w),\n                    RecvHalf::Loopback(op_r),\n                    true,\n                ))\n            }),\n        }\n    }","startLine":120,"lineNumbers":[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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,null,223,224,null,238,null,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,null,357,null,373,null,433,null,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,null,547,null,615,null,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,null,719,null,740,null,1363,null,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs"}}},"isError":false,"timestamp":1784247600545}}
{"type":"message","id":"9ca7175d","parentId":"c46cf5b6","timestamp":"2026-07-17T00:20:00.546Z","message":{"role":"toolResult","toolCallId":"call_yRo0UEooCPTxsAd3xUAa2J3f|fc_0996e78cf50e3160016a59752fb1ac8199b7956c5059773298","toolName":"read","content":[{"type":"text","text":"use spt_store::contextstore::ContextStore;\nuse spt_store::notif::NotifStore;\nuse spt_store::perch;\nuse spt_store::roster::RosterStore;\n\nuse crate::attach::serve_attach;\nuse crate::brain::{Brain, BrokerEvent};\nuse crate::notifsync::{apply_notif_feed, NotifPolicy};\nuse crate::propagate::serve_update;\nuse crate::registryhost::{RegistryGatePolicy, RegistryHost};\nuse crate::relcache::ReleaseCache;\nuse crate::sync::{serve_sync, SyncPolicy};\nuse crate::wan::receive_wan;\nuse crate::xfer::serve_xfer;\n\n/// Poll cadence for the stream-table watch (the dispatcher's only busy-wait;\n/// workers block on their stream's event feed).\npub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n\n/// What protocol a peer-initiated stream speaks, by its first record's shape.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum StreamFamily {\n    /// A context sync pull (`SyncRecord::Request`).\n    Sync,\n    /// An update pull (`UpdRecord::Query`).\n    Update,\n    /// A notif replication feed (`NotifRecord::Row`).\n    Notif,\n    /// A WAN message feed (untagged `WanMessage` lines).\n    WanMsg,\n    /// A registry replication feed (untagged `RegistryUpdate` lines).\n    Registry,\n    /// A remote-drive attach (`AttachRecord::Request`).\n    Attach,\n    /// A file transfer (`XferRecord::Fetch`/`Push`).\n    Xfer,\n    /// A remote rest op (`RestRecord::Request` — suspend/wake from another\n    /// node, M5-D5b).\n    Rest,\n    /// A cross-node shell-link op (`ShellLinkRecord::Request` — relink/cmd\n    /// against an owner's shell on this node, M5-D8c).\n    ShellLink,\n    /// A subnet-scoped liveness probe (`ServeProbeRecord::Request` — \"serving\n    /// subnet X?\", the `--nodes` view's detach-aware refinement, REQ-SUBNET-5).\n    ServeProbe,\n    /// Unclassifiable — dropped.\n    Unknown,\n}\n\n/// Classify one decoded first line by shape (module-docs demux table).\npub fn classify_first_line(line: &serde_json::Value) -> StreamFamily {\n    match line.get(\"kind\").and_then(|k| k.as_str()) {\n        Some(\"request\") if line.get(\"sync_id\").is_some() => StreamFamily::Sync,\n        Some(\"request\") if line.get(\"session_id\").is_some() => StreamFamily::Attach,\n        Some(\"request\") if line.get(\"endpoint\").is_some() && line.get(\"event\").is_some() => {\n            StreamFamily::Rest\n        }\n        Some(\"request\") if line.get(\"owner\").is_some() && line.get(\"shell_ref\").is_some() => {\n            StreamFamily::ShellLink\n        }\n        Some(\"request\") if line.get(\"serve_probe\").is_some() => StreamFamily::ServeProbe,\n        Some(\"query\") => StreamFamily::Update,\n        // The status-only convergence query rides the update family (M8\n        // decision 19, REQ-UPD-6) — same serve loop, no artifact bytes.\n        Some(\"status_query\") => StreamFamily::Update,\n        Some(\"row\") => StreamFamily::Notif,\n        Some(\"fetch\") | Some(\"push\") => StreamFamily::Xfer,\n        Some(_) => StreamFamily::Unknown,\n        None => {\n            if line.get(\"target\").is_some() && line.get(\"op_id\").is_some() {\n                StreamFamily::WanMsg\n            } else if line.get(\"subnet\").is_some() && line.get(\"instance\").is_some() {\n                StreamFamily::Registry\n            } else if line.get(\"subnet\").is_some()\n                && line.get(\"node\").is_some()\n                && line.get(\"label\").is_some()\n            {\n                // A node-level label feed (REQ-SUBNET-3): an endpoint-less peer\n                // sends label records first (no instance rows), so the feed's\n                // first line is a NodeLabelUpdate. Same serve arm, same gate.\n                StreamFamily::Registry\n            } else {\n                StreamFamily::Unknown\n            }\n        }\n    }\n}\n\n/// Why a dispatched stream's worker ended — loud + typed, never silent.\n#[derive(Debug)]\npub enum DispatchOutcome {\n    /// The family's serve loop ran to its own outcome (rendered for the log).\n    Served(String),\n    /// The first line never arrived / didn't parse — dropped.\n    Unknown,\n    /// The worker failed (IO against broker or store).\n    Failed(String),\n}\n\n/// A claimed stream's dispatch lifecycle (ADR-0038 C,\n/// REQ-DISPATCH-CLAIM-RETRY). The pre-fix `claimed` was a one-shot `HashSet`\n/// insert performed BEFORE worker spawn and never cleared — one transient\n/// worker-setup failure permanently abandoned the stream. Claims now\n/// distinguish in-flight vs terminal vs retryable, and a transient failure\n/// requeues bounded (backoff-shaped, never a hot loop).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ClaimState {\n    /// A worker owns the stream right now (`attempts` = prior failures).\n    InFlight { attempts: u32 },\n    /// Done for this dispatcher's lifetime: served, unclassifiable, or\n    /// retries exhausted. Terminal outcomes never re-enter the queue.\n    Terminal,\n    /// A transient failure released the claim; eligible again at `next_at`.\n    Retry { attempts: u32, next_at: Instant },\n}\n\n/// Bounded retry budget per stream per dispatcher lifetime — transient setup\n/// failures recover, a persistently-failing stream goes terminal loud.\nconst MAX_DISPATCH_ATTEMPTS: u32 = 3;\n\n/// Backoff base for a released claim (doubles per failed attempt).\nconst DISPATCH_RETRY_BASE: Duration = Duration::from_millis(500);\n\n/// The backoff before retry number `attempts` (1-based) runs.\nfn retry_backoff(attempts: u32) -> Duration {\n    DISPATCH_RETRY_BASE * 2u32.saturating_pow(attempts.saturating_sub(1))\n}\n\n/// PURE claim decision: may this row be (re)claimed now? `Some(prior\n/// failures)` = claim it; `None` = leave it (in flight, terminal, or backing\n/// off). The unit-tested seam for the no-hot-loop ruling.\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\nfn should_claim(state: Option<&ClaimState>, now: Instant) -> Option<u32> {\n    match state {\n        None => Some(0),\n        Some(ClaimState::Retry { attempts, next_at }) if now >= *next_at => Some(*attempts),\n        _ => None,\n    }\n}\n\n/// PURE outcome transition: what a finished worker's claim becomes. Served\n/// and Unknown (unclassifiable) are TERMINAL — a terminal classification never\n/// hot-loops. A failure requeues with backoff until the bounded budget is\n/// spent, then goes terminal.\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\nfn outcome_transition(outcome: &DispatchOutcome, attempts: u32, now: Instant) -> ClaimState {\n    match outcome {\n        DispatchOutcome::Served(_) | DispatchOutcome::Unknown => ClaimState::Terminal,\n        DispatchOutcome::Failed(_) => {\n            let attempts = attempts + 1;\n            if attempts >= MAX_DISPATCH_ATTEMPTS {\n                ClaimState::Terminal\n            } else {\n                ClaimState::Retry {\n                    attempts,\n                    next_at: now + retry_backoff(attempts),\n                }\n            }\n        }\n    }\n}\n\n/// PURE lifecycle+family discriminator (gate round 1, ADR-0038 Decision 1's\n/// \"excluded by lifecycle state\" half): is a `finished` row TERMINAL for\n/// redispatch — retire unserved, never replay?\n///\n/// `finished` alone is NOT the answer, and a bare `s.finished` claim filter\n/// would be a regression: the request/reply families FIN their send WITH the\n/// request and await the reply on the same stream (sync.rs `request_sync` and\n/// propagate.rs `fetch_update` both `net_stream_send(.., finish=true)` on the\n/// opener), so every pull that arrives while the brain is down reads\n/// `finished=true` while genuinely PENDING — filtering it breaks context sync\n/// across exactly the refresh window this milestone fixes.\n///\n/// The steal class is ATTACH-specific: `finished` on an attach stream is the\n/// operator's clean detach — its exchange is definitionally over, and a\n/// replay takes a live controller (KNOWN-HAZARDS 7.41: the pre-existing\n/// backlog on long-lived conns, and the brain-down-detach window that\n/// Served-report retirement can never see). So: finished + Attach = terminal\n/// (retired unserved via the worker's Served report); finished + anything\n/// else = a pending request — serve it.\n// [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n// [impl->REQ-HAZARD-REDISPATCH-CONTROL-STEAL]\nfn finished_row_is_terminal(family: StreamFamily, finished: bool) -> bool {\n    finished && family == StreamFamily::Attach\n}\n\n/// TEST-RIG-ONLY (the D3 regression): `SPT_DISPATCH_INJECT_TRANSIENT_ONCE`\n/// set at dispatcher start makes exactly ONE worker fail before setup —\n/// proving a transient worker-start failure requeues and recovers through\n/// the REAL claim path. Never set in production; the latch arms once per\n/// process and disarms on first take.\nfn take_injected_transient() -> bool {\n    static ARMED: OnceLock<AtomicBool> = OnceLock::new();\n    let armed = ARMED.get_or_init(|| {\n        AtomicBool::new(std::env::var(\"SPT_DISPATCH_INJECT_TRANSIENT_ONCE\").is_ok())\n    });\n    armed.swap(false, Ordering::SeqCst)\n}\n\n/// The SPT_HOME-anchored paths the serve arms need. One value, cloned per\n/// worker (policies/stores re-load from the canonical paths per request —\n/// the access-gate stance, config edits take effect without a restart).\n#[derive(Debug, Clone)]\npub struct DispatchPaths {\n    /// The perch tree (WAN message funnel target).\n    pub owlery: PathBuf,\n    /// The staged-release cache dir ([`ReleaseCache::open`]).\n    pub releases: PathBuf,\n    /// Transient bundle/artifact scratch.\n    pub scratch: PathBuf,\n}\n\nimpl DispatchPaths {\n    /// The production layout under [`perch::spt_home`].\n    pub fn production() -> DispatchPaths {\n        let home = perch::spt_home();\n        DispatchPaths {\n            owlery: perch::owlery_dir(),\n            releases: home.join(\"releases\"),\n            scratch: home.join(\"scratch\").join(\"net\"),\n        }\n    }\n}\n\n/// Watch the broker's stream table and dispatch every **peer-initiated**\n/// stream to a family worker, until `stop` is set. Blocks the calling thread\n/// (the daemon spawns it on its own); workers detach (each ends with its\n/// stream).\npub fn run_dispatch_loop(\n    broker_name: &str,\n    paths: &DispatchPaths,\n    registry: Arc<RegistryHost>,\n    stop: &AtomicBool,\n    poll: Duration,\n) -> io::Result<()> {\n    let mut brain = connect(broker_name)?;\n    // Claim lifecycle (ADR-0038 C): in-flight / terminal / retryable —\n    // replaces the pre-fix one-shot `HashSet` whose burned claims permanently\n    // abandoned a stream on the first transient failure.\n    let claims: Arc<Mutex<HashMap<u64, ClaimState>>> = Arc::new(Mutex::new(HashMap::new()));\n\n    while !stop.load(Ordering::Relaxed) {\n        let reply = brain.net_streams()?;\n        for s in reply.streams {\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\n            // invisible until its first write anyway — gotcha #6 — but the\n            // next_seq guard also skips a row whose pump hasn't appended yet).\n            // Terminal rows never re-enter; a released (Retry) claim re-enters\n            // after its backoff (REQ-DISPATCH-CLAIM-RETRY).\n            if s.initiated_locally || s.next_seq == 0 {\n                continue;\n            }\n            let attempts = {\n                let mut c = claims.lock().unwrap();\n                match should_claim(c.get(&s.stream_id), Instant::now()) {\n                    Some(attempts) => {\n                        c.insert(s.stream_id, ClaimState::InFlight { attempts });\n                        attempts\n                    }\n                    None => continue,\n                }\n            };\n            let name = broker_name.to_string();\n            let origin = s.remote_id_hex.clone();\n            let stream_id = s.stream_id;\n            let floor_seq = s.floor_seq;\n            let finished = s.finished;\n            let p = paths.clone();\n            let reg = Arc::clone(&registry);\n            let claims_done = Arc::clone(&claims);\n            thread::spawn(move || {\n                let outcome = worker(&name, stream_id, &origin, floor_seq, finished, &p, &reg);\n                if matches!(outcome, DispatchOutcome::Served(_)) {\n                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n                    // this stream's exchange is over — take its row out of\n                    // every FUTURE dispatcher generation's enumeration (the\n                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n                    // an older broker without the verb answers error (KH-2.3)\n                    // and the row retires when the broker cycles.\n                    if let Ok(mut retire_brain) = connect(&name) {\n                        let _ = retire_brain.net_stream_retire(stream_id);\n                    }\n                } else {\n                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n                }\n                let mut c = claims_done.lock().unwrap();\n                c.insert(\n                    stream_id,\n                    outcome_transition(&outcome, attempts, Instant::now()),\n                );\n            });\n        }\n        thread::sleep(poll);\n    }\n    Ok(())\n}\n\n/// Connect a fresh brain IPC client (retrying through broker startup).\nfn connect(broker_name: &str) -> io::Result<Brain> {\n    let mut last = io::Error::other(\"broker never answered\");\n    for _ in 0..300 {\n        match Brain::cold_start(broker_name, now_ms()) {\n            Ok(b) => return Ok(b),\n            Err(e) => last = e,\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    Err(last)\n}\n\nfn now_ms() -> u64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_millis() as u64)\n        .unwrap_or(0)\n}\n…\nfn peek_first_line(\n…\n) -> io::Result<Option<serde_json::Value>> {\n…\n    brain.net_stream_subscribe(stream_id, floor_seq)?;\n    let mut buf: Vec<u8> = Vec::new();\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                buf.extend_from_slice(&bytes);\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                // Finished without a complete line: classify what we have\n                // (a line without a trailing newline before EOF still counts).\n                return Ok(serde_json::from_slice(&buf).ok());\n            }\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n            _ => {}\n        }\n    }\n}\n\n/// One claimed stream's worker: peek → classify → run the family's serve\n/// loop on a fresh brain connection.\n#[allow(clippy::too_many_arguments)]\nfn worker(\n    broker_name: &str,\n    stream_id: u64,\n    origin: &str,\n    floor_seq: u64,\n    finished: bool,\n    paths: &DispatchPaths,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    if take_injected_transient() {\n        return DispatchOutcome::Failed(\n            \"injected transient worker-start failure (test valve)\".into(),\n        );\n    }\n    let line = match first_line(broker_name, stream_id, floor_seq) {\n        Ok(Some(v)) => v,\n        Ok(None) => return DispatchOutcome::Unknown,\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    };\n    let family = classify_first_line(&line);\n\n    // Gate round 1 (retire-on-sight, KNOWN-HAZARDS 7.41): a finished ATTACH\n    // row's exchange is over — the operator detached — and replaying it would\n    // take the live controller (pre-existing backlog + brain-down-detach\n    // orderings, which the Served-report path can never see). Report Served\n    // WITHOUT serving: the loop's completion path retires the row, so the\n    // backlog also drains from the table. Finished request/reply rows fall\n    // through — their FIN is protocol, not completion (see\n    // `finished_row_is_terminal`).\n    if finished_row_is_terminal(family, finished) {\n        return DispatchOutcome::Served(\"finished attach: terminal, retired unserved\".into());\n    }\n\n    let mut brain = match connect(broker_name) {\n        Ok(b) => b,\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    };\n\n    match family {\n        StreamFamily::Sync => {\n            let cs = match ContextStore::open_or_init() {\n                Ok(cs) => cs,\n                Err(e) => return DispatchOutcome::Failed(e.to_string()),\n            };\n            let policy = SyncPolicy::load();\n            match serve_sync(\n                &mut brain,\n                stream_id,\n                origin,\n                0,\n                &cs,\n                &policy,\n                &paths.scratch,\n            ) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Update => {\n            let cache = ReleaseCache::open(&paths.releases);\n            let roster = RosterStore::load();\n            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n        StreamFamily::Attach => {\n            // The gate subject is the session's OWNING endpoint, resolved\n            // from the broker's table (the spawn's label) — never from wire\n            // bytes. An unlabeled or unknown session refuses, fail-closed.\n            //\n            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n            // cannot know our local session id, so it names the ENDPOINT. When the\n            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n            // we actually host a live session for it (D5 session-presence gate;\n            // D6: a stale registry row that named a gone endpoint resolves to no\n            // session → refuse cleanly, never a false attach). The endpoint id is\n            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\n            // `endpoint`); the origin subject stays the handshake identity. The\n            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\n            // is unchanged.\n            // `endpoint` is the gate subject; `remote_session` carries the\n            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\n            // before serve_attach subscribes — never mid-serve, which would swallow\n            // interleaved attach records) and `None` on the local path.\n            let (endpoint, remote_session) = match line\n                .get(\"endpoint_id\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n            {\n                Some(ep) => match crate::attach::resolve_local_session(&mut brain, ep) {\n                    Some(sid) => (Some(ep.to_string()), Some(sid)),\n                    None => (None, None),\n                },\n                None => {\n                    let session_id = line.get(\"session_id\").and_then(|v| v.as_u64());\n                    let endpoint = session_id.and_then(|sid| {\n                        brain.sessions().ok().and_then(|reply| {\n                            reply\n                                .sessions\n                                .iter()\n                                .find(|s| s.session_id == sid && !s.endpoint.is_empty())\n                                .map(|s| s.endpoint.clone())\n                        })\n                    });\n…\n                }\n            };\n…\n        }\n…\n        StreamFamily::ShellLink => {\n…\n        }\n        StreamFamily::ServeProbe => {\n            // No gate (serveprobe module docs): the answer is public\n            // serve-state for a named subnet, read fresh from this node's\n            // own stores — the QUIC handshake is the only subject.\n            // [impl->REQ-SUBNET-5]\n            match crate::serveprobe::serve_subnet_probe(&mut brain, stream_id) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Unknown => DispatchOutcome::Unknown,\n    }\n}\n\n/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into\n/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧\n/// origin trusted in that subnet), until the peer finishes.\n///\n/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer\n/// instance of an id observed transitioning to `Active` — its node's daemon\n/// just gave it a driver, so a *locally hosted* instance of the same id\n/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`\n/// dormant). The rest edge's transition echo is gate-armed\n/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse\n/// loop runs the bounded summarizer call, never this worker (the KH 7.4\n/// non-blocking posture: N flips in one feed are N gate files, not N LLM\n/// calls serialized here).\n// [impl->REQ-INST-7]\nfn serve_registry_feed(\n    brain: &mut Brain,\n    stream_id: u64,\n    origin: &str,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    let policy = RegistryGatePolicy::load();\n    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n        return DispatchOutcome::Failed(e.to_string());\n    }\n    let mut decoder = RegistryDecoder::new();\n    let mut merged = 0usize;\n    loop {\n        match brain.read_event() {\n            Ok(BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            }) if sid == stream_id => {\n                let records = decoder.push(&bytes);\n                if records.is_empty() {\n                    continue;\n                }\n                // Split the feed into instance rows and node-level labels\n                // (REQ-SUBNET-3) — each goes through its own gated apply path.\n                let mut updates: Vec<RegistryUpdate> = Vec::new();\n                let mut labels: Vec<NodeLabelUpdate> = Vec::new();\n                for rec in records {\n                    match rec {\n                        RegistryFeedRecord::Instance(u) => updates.push(u),\n                        RegistryFeedRecord::NodeLabel(n) => labels.push(n),\n                    }\n                }\n                if !labels.is_empty() {\n                    merged += registry.apply_node_labels(origin, &labels, &policy).len();\n                }\n                if updates.is_empty() {\n                    continue;\n                }\n                let (verdicts, flips) = registry.apply_feed_flips(origin, &updates, &policy);\n                merged += verdicts.len();\n                // Attention shift (D9-2): only ids this node actually hosts\n…\n            }\n…\n        }\n    }\n}"}],"details":{"displayContent":{"text":"use spt_store::contextstore::ContextStore;\nuse spt_store::notif::NotifStore;\nuse spt_store::perch;\nuse spt_store::roster::RosterStore;\n\nuse crate::attach::serve_attach;\nuse crate::brain::{Brain, BrokerEvent};\nuse crate::notifsync::{apply_notif_feed, NotifPolicy};\nuse crate::propagate::serve_update;\nuse crate::registryhost::{RegistryGatePolicy, RegistryHost};\nuse crate::relcache::ReleaseCache;\nuse crate::sync::{serve_sync, SyncPolicy};\nuse crate::wan::receive_wan;\nuse crate::xfer::serve_xfer;\n\n/// Poll cadence for the stream-table watch (the dispatcher's only busy-wait;\n/// workers block on their stream's event feed).\npub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n\n/// What protocol a peer-initiated stream speaks, by its first record's shape.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum StreamFamily {\n    /// A context sync pull (`SyncRecord::Request`).\n    Sync,\n    /// An update pull (`UpdRecord::Query`).\n    Update,\n    /// A notif replication feed (`NotifRecord::Row`).\n    Notif,\n    /// A WAN message feed (untagged `WanMessage` lines).\n    WanMsg,\n    /// A registry replication feed (untagged `RegistryUpdate` lines).\n    Registry,\n    /// A remote-drive attach (`AttachRecord::Request`).\n    Attach,\n    /// A file transfer (`XferRecord::Fetch`/`Push`).\n    Xfer,\n    /// A remote rest op (`RestRecord::Request` — suspend/wake from another\n    /// node, M5-D5b).\n    Rest,\n    /// A cross-node shell-link op (`ShellLinkRecord::Request` — relink/cmd\n    /// against an owner's shell on this node, M5-D8c).\n    ShellLink,\n    /// A subnet-scoped liveness probe (`ServeProbeRecord::Request` — \"serving\n    /// subnet X?\", the `--nodes` view's detach-aware refinement, REQ-SUBNET-5).\n    ServeProbe,\n    /// Unclassifiable — dropped.\n    Unknown,\n}\n\n/// Classify one decoded first line by shape (module-docs demux table).\npub fn classify_first_line(line: &serde_json::Value) -> StreamFamily {\n    match line.get(\"kind\").and_then(|k| k.as_str()) {\n        Some(\"request\") if line.get(\"sync_id\").is_some() => StreamFamily::Sync,\n        Some(\"request\") if line.get(\"session_id\").is_some() => StreamFamily::Attach,\n        Some(\"request\") if line.get(\"endpoint\").is_some() && line.get(\"event\").is_some() => {\n            StreamFamily::Rest\n        }\n        Some(\"request\") if line.get(\"owner\").is_some() && line.get(\"shell_ref\").is_some() => {\n            StreamFamily::ShellLink\n        }\n        Some(\"request\") if line.get(\"serve_probe\").is_some() => StreamFamily::ServeProbe,\n        Some(\"query\") => StreamFamily::Update,\n        // The status-only convergence query rides the update family (M8\n        // decision 19, REQ-UPD-6) — same serve loop, no artifact bytes.\n        Some(\"status_query\") => StreamFamily::Update,\n        Some(\"row\") => StreamFamily::Notif,\n        Some(\"fetch\") | Some(\"push\") => StreamFamily::Xfer,\n        Some(_) => StreamFamily::Unknown,\n        None => {\n            if line.get(\"target\").is_some() && line.get(\"op_id\").is_some() {\n                StreamFamily::WanMsg\n            } else if line.get(\"subnet\").is_some() && line.get(\"instance\").is_some() {\n                StreamFamily::Registry\n            } else if line.get(\"subnet\").is_some()\n                && line.get(\"node\").is_some()\n                && line.get(\"label\").is_some()\n            {\n                // A node-level label feed (REQ-SUBNET-3): an endpoint-less peer\n                // sends label records first (no instance rows), so the feed's\n                // first line is a NodeLabelUpdate. Same serve arm, same gate.\n                StreamFamily::Registry\n            } else {\n                StreamFamily::Unknown\n            }\n        }\n    }\n}\n\n/// Why a dispatched stream's worker ended — loud + typed, never silent.\n#[derive(Debug)]\npub enum DispatchOutcome {\n    /// The family's serve loop ran to its own outcome (rendered for the log).\n    Served(String),\n    /// The first line never arrived / didn't parse — dropped.\n    Unknown,\n    /// The worker failed (IO against broker or store).\n    Failed(String),\n}\n\n/// A claimed stream's dispatch lifecycle (ADR-0038 C,\n/// REQ-DISPATCH-CLAIM-RETRY). The pre-fix `claimed` was a one-shot `HashSet`\n/// insert performed BEFORE worker spawn and never cleared — one transient\n/// worker-setup failure permanently abandoned the stream. Claims now\n/// distinguish in-flight vs terminal vs retryable, and a transient failure\n/// requeues bounded (backoff-shaped, never a hot loop).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ClaimState {\n    /// A worker owns the stream right now (`attempts` = prior failures).\n    InFlight { attempts: u32 },\n    /// Done for this dispatcher's lifetime: served, unclassifiable, or\n    /// retries exhausted. Terminal outcomes never re-enter the queue.\n    Terminal,\n    /// A transient failure released the claim; eligible again at `next_at`.\n    Retry { attempts: u32, next_at: Instant },\n}\n\n/// Bounded retry budget per stream per dispatcher lifetime — transient setup\n/// failures recover, a persistently-failing stream goes terminal loud.\nconst MAX_DISPATCH_ATTEMPTS: u32 = 3;\n\n/// Backoff base for a released claim (doubles per failed attempt).\nconst DISPATCH_RETRY_BASE: Duration = Duration::from_millis(500);\n\n/// The backoff before retry number `attempts` (1-based) runs.\nfn retry_backoff(attempts: u32) -> Duration {\n    DISPATCH_RETRY_BASE * 2u32.saturating_pow(attempts.saturating_sub(1))\n}\n\n/// PURE claim decision: may this row be (re)claimed now? `Some(prior\n/// failures)` = claim it; `None` = leave it (in flight, terminal, or backing\n/// off). The unit-tested seam for the no-hot-loop ruling.\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\nfn should_claim(state: Option<&ClaimState>, now: Instant) -> Option<u32> {\n    match state {\n        None => Some(0),\n        Some(ClaimState::Retry { attempts, next_at }) if now >= *next_at => Some(*attempts),\n        _ => None,\n    }\n}\n\n/// PURE outcome transition: what a finished worker's claim becomes. Served\n/// and Unknown (unclassifiable) are TERMINAL — a terminal classification never\n/// hot-loops. A failure requeues with backoff until the bounded budget is\n/// spent, then goes terminal.\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\nfn outcome_transition(outcome: &DispatchOutcome, attempts: u32, now: Instant) -> ClaimState {\n    match outcome {\n        DispatchOutcome::Served(_) | DispatchOutcome::Unknown => ClaimState::Terminal,\n        DispatchOutcome::Failed(_) => {\n            let attempts = attempts + 1;\n            if attempts >= MAX_DISPATCH_ATTEMPTS {\n                ClaimState::Terminal\n            } else {\n                ClaimState::Retry {\n                    attempts,\n                    next_at: now + retry_backoff(attempts),\n                }\n            }\n        }\n    }\n}\n\n/// PURE lifecycle+family discriminator (gate round 1, ADR-0038 Decision 1's\n/// \"excluded by lifecycle state\" half): is a `finished` row TERMINAL for\n/// redispatch — retire unserved, never replay?\n///\n/// `finished` alone is NOT the answer, and a bare `s.finished` claim filter\n/// would be a regression: the request/reply families FIN their send WITH the\n/// request and await the reply on the same stream (sync.rs `request_sync` and\n/// propagate.rs `fetch_update` both `net_stream_send(.., finish=true)` on the\n/// opener), so every pull that arrives while the brain is down reads\n/// `finished=true` while genuinely PENDING — filtering it breaks context sync\n/// across exactly the refresh window this milestone fixes.\n///\n/// The steal class is ATTACH-specific: `finished` on an attach stream is the\n/// operator's clean detach — its exchange is definitionally over, and a\n/// replay takes a live controller (KNOWN-HAZARDS 7.41: the pre-existing\n/// backlog on long-lived conns, and the brain-down-detach window that\n/// Served-report retirement can never see). So: finished + Attach = terminal\n/// (retired unserved via the worker's Served report); finished + anything\n/// else = a pending request — serve it.\n// [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n// [impl->REQ-HAZARD-REDISPATCH-CONTROL-STEAL]\nfn finished_row_is_terminal(family: StreamFamily, finished: bool) -> bool {\n    finished && family == StreamFamily::Attach\n}\n\n/// TEST-RIG-ONLY (the D3 regression): `SPT_DISPATCH_INJECT_TRANSIENT_ONCE`\n/// set at dispatcher start makes exactly ONE worker fail before setup —\n/// proving a transient worker-start failure requeues and recovers through\n/// the REAL claim path. Never set in production; the latch arms once per\n/// process and disarms on first take.\nfn take_injected_transient() -> bool {\n    static ARMED: OnceLock<AtomicBool> = OnceLock::new();\n    let armed = ARMED.get_or_init(|| {\n        AtomicBool::new(std::env::var(\"SPT_DISPATCH_INJECT_TRANSIENT_ONCE\").is_ok())\n    });\n    armed.swap(false, Ordering::SeqCst)\n}\n\n/// The SPT_HOME-anchored paths the serve arms need. One value, cloned per\n/// worker (policies/stores re-load from the canonical paths per request —\n/// the access-gate stance, config edits take effect without a restart).\n#[derive(Debug, Clone)]\npub struct DispatchPaths {\n    /// The perch tree (WAN message funnel target).\n    pub owlery: PathBuf,\n    /// The staged-release cache dir ([`ReleaseCache::open`]).\n    pub releases: PathBuf,\n    /// Transient bundle/artifact scratch.\n    pub scratch: PathBuf,\n}\n\nimpl DispatchPaths {\n    /// The production layout under [`perch::spt_home`].\n    pub fn production() -> DispatchPaths {\n        let home = perch::spt_home();\n        DispatchPaths {\n            owlery: perch::owlery_dir(),\n            releases: home.join(\"releases\"),\n            scratch: home.join(\"scratch\").join(\"net\"),\n        }\n    }\n}\n\n/// Watch the broker's stream table and dispatch every **peer-initiated**\n/// stream to a family worker, until `stop` is set. Blocks the calling thread\n/// (the daemon spawns it on its own); workers detach (each ends with its\n/// stream).\npub fn run_dispatch_loop(\n    broker_name: &str,\n    paths: &DispatchPaths,\n    registry: Arc<RegistryHost>,\n    stop: &AtomicBool,\n    poll: Duration,\n) -> io::Result<()> {\n    let mut brain = connect(broker_name)?;\n    // Claim lifecycle (ADR-0038 C): in-flight / terminal / retryable —\n    // replaces the pre-fix one-shot `HashSet` whose burned claims permanently\n    // abandoned a stream on the first transient failure.\n    let claims: Arc<Mutex<HashMap<u64, ClaimState>>> = Arc::new(Mutex::new(HashMap::new()));\n\n    while !stop.load(Ordering::Relaxed) {\n        let reply = brain.net_streams()?;\n        for s in reply.streams {\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\n            // invisible until its first write anyway — gotcha #6 — but the\n            // next_seq guard also skips a row whose pump hasn't appended yet).\n            // Terminal rows never re-enter; a released (Retry) claim re-enters\n            // after its backoff (REQ-DISPATCH-CLAIM-RETRY).\n            if s.initiated_locally || s.next_seq == 0 {\n                continue;\n            }\n            let attempts = {\n                let mut c = claims.lock().unwrap();\n                match should_claim(c.get(&s.stream_id), Instant::now()) {\n                    Some(attempts) => {\n                        c.insert(s.stream_id, ClaimState::InFlight { attempts });\n                        attempts\n                    }\n                    None => continue,\n                }\n            };\n            let name = broker_name.to_string();\n            let origin = s.remote_id_hex.clone();\n            let stream_id = s.stream_id;\n            let floor_seq = s.floor_seq;\n            let finished = s.finished;\n            let p = paths.clone();\n            let reg = Arc::clone(&registry);\n            let claims_done = Arc::clone(&claims);\n            thread::spawn(move || {\n                let outcome = worker(&name, stream_id, &origin, floor_seq, finished, &p, &reg);\n                if matches!(outcome, DispatchOutcome::Served(_)) {\n                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n                    // this stream's exchange is over — take its row out of\n                    // every FUTURE dispatcher generation's enumeration (the\n                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n                    // an older broker without the verb answers error (KH-2.3)\n                    // and the row retires when the broker cycles.\n                    if let Ok(mut retire_brain) = connect(&name) {\n                        let _ = retire_brain.net_stream_retire(stream_id);\n                    }\n                } else {\n                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n                }\n                let mut c = claims_done.lock().unwrap();\n                c.insert(\n                    stream_id,\n                    outcome_transition(&outcome, attempts, Instant::now()),\n                );\n            });\n        }\n        thread::sleep(poll);\n    }\n    Ok(())\n}\n\n/// Connect a fresh brain IPC client (retrying through broker startup).\nfn connect(broker_name: &str) -> io::Result<Brain> {\n    let mut last = io::Error::other(\"broker never answered\");\n    for _ in 0..300 {\n        match Brain::cold_start(broker_name, now_ms()) {\n            Ok(b) => return Ok(b),\n            Err(e) => last = e,\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    Err(last)\n}\n\nfn now_ms() -> u64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_millis() as u64)\n        .unwrap_or(0)\n}\n…\nfn peek_first_line(\n…\n) -> io::Result<Option<serde_json::Value>> {\n…\n    brain.net_stream_subscribe(stream_id, floor_seq)?;\n    let mut buf: Vec<u8> = Vec::new();\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                buf.extend_from_slice(&bytes);\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                // Finished without a complete line: classify what we have\n                // (a line without a trailing newline before EOF still counts).\n                return Ok(serde_json::from_slice(&buf).ok());\n            }\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n            _ => {}\n        }\n    }\n}\n\n/// One claimed stream's worker: peek → classify → run the family's serve\n/// loop on a fresh brain connection.\n#[allow(clippy::too_many_arguments)]\nfn worker(\n    broker_name: &str,\n    stream_id: u64,\n    origin: &str,\n    floor_seq: u64,\n    finished: bool,\n    paths: &DispatchPaths,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    if take_injected_transient() {\n        return DispatchOutcome::Failed(\n            \"injected transient worker-start failure (test valve)\".into(),\n        );\n    }\n    let line = match first_line(broker_name, stream_id, floor_seq) {\n        Ok(Some(v)) => v,\n        Ok(None) => return DispatchOutcome::Unknown,\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    };\n    let family = classify_first_line(&line);\n\n    // Gate round 1 (retire-on-sight, KNOWN-HAZARDS 7.41): a finished ATTACH\n    // row's exchange is over — the operator detached — and replaying it would\n    // take the live controller (pre-existing backlog + brain-down-detach\n    // orderings, which the Served-report path can never see). Report Served\n    // WITHOUT serving: the loop's completion path retires the row, so the\n    // backlog also drains from the table. Finished request/reply rows fall\n    // through — their FIN is protocol, not completion (see\n    // `finished_row_is_terminal`).\n    if finished_row_is_terminal(family, finished) {\n        return DispatchOutcome::Served(\"finished attach: terminal, retired unserved\".into());\n    }\n\n    let mut brain = match connect(broker_name) {\n        Ok(b) => b,\n        Err(e) => return DispatchOutcome::Failed(e.to_string()),\n    };\n\n    match family {\n        StreamFamily::Sync => {\n            let cs = match ContextStore::open_or_init() {\n                Ok(cs) => cs,\n                Err(e) => return DispatchOutcome::Failed(e.to_string()),\n            };\n            let policy = SyncPolicy::load();\n            match serve_sync(\n                &mut brain,\n                stream_id,\n                origin,\n                0,\n                &cs,\n                &policy,\n                &paths.scratch,\n            ) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Update => {\n            let cache = ReleaseCache::open(&paths.releases);\n            let roster = RosterStore::load();\n            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n        StreamFamily::Attach => {\n            // The gate subject is the session's OWNING endpoint, resolved\n            // from the broker's table (the spawn's label) — never from wire\n            // bytes. An unlabeled or unknown session refuses, fail-closed.\n            //\n            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n            // cannot know our local session id, so it names the ENDPOINT. When the\n            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n            // we actually host a live session for it (D5 session-presence gate;\n            // D6: a stale registry row that named a gone endpoint resolves to no\n            // session → refuse cleanly, never a false attach). The endpoint id is\n            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\n            // `endpoint`); the origin subject stays the handshake identity. The\n            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\n            // is unchanged.\n            // `endpoint` is the gate subject; `remote_session` carries the\n            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\n            // before serve_attach subscribes — never mid-serve, which would swallow\n            // interleaved attach records) and `None` on the local path.\n            let (endpoint, remote_session) = match line\n                .get(\"endpoint_id\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n            {\n                Some(ep) => match crate::attach::resolve_local_session(&mut brain, ep) {\n                    Some(sid) => (Some(ep.to_string()), Some(sid)),\n                    None => (None, None),\n                },\n                None => {\n                    let session_id = line.get(\"session_id\").and_then(|v| v.as_u64());\n                    let endpoint = session_id.and_then(|sid| {\n                        brain.sessions().ok().and_then(|reply| {\n                            reply\n                                .sessions\n                                .iter()\n                                .find(|s| s.session_id == sid && !s.endpoint.is_empty())\n                                .map(|s| s.endpoint.clone())\n                        })\n                    });\n…\n                }\n            };\n…\n        }\n…\n        StreamFamily::ShellLink => {\n…\n        }\n        StreamFamily::ServeProbe => {\n            // No gate (serveprobe module docs): the answer is public\n            // serve-state for a named subnet, read fresh from this node's\n            // own stores — the QUIC handshake is the only subject.\n            // [impl->REQ-SUBNET-5]\n            match crate::serveprobe::serve_subnet_probe(&mut brain, stream_id) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Unknown => DispatchOutcome::Unknown,\n    }\n}\n\n/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into\n/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧\n/// origin trusted in that subnet), until the peer finishes.\n///\n/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer\n/// instance of an id observed transitioning to `Active` — its node's daemon\n/// just gave it a driver, so a *locally hosted* instance of the same id\n/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`\n/// dormant). The rest edge's transition echo is gate-armed\n/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse\n/// loop runs the bounded summarizer call, never this worker (the KH 7.4\n/// non-blocking posture: N flips in one feed are N gate files, not N LLM\n/// calls serialized here).\n// [impl->REQ-INST-7]\nfn serve_registry_feed(\n    brain: &mut Brain,\n    stream_id: u64,\n    origin: &str,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    let policy = RegistryGatePolicy::load();\n    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n        return DispatchOutcome::Failed(e.to_string());\n    }\n    let mut decoder = RegistryDecoder::new();\n    let mut merged = 0usize;\n    loop {\n        match brain.read_event() {\n            Ok(BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            }) if sid == stream_id => {\n                let records = decoder.push(&bytes);\n                if records.is_empty() {\n                    continue;\n                }\n                // Split the feed into instance rows and node-level labels\n                // (REQ-SUBNET-3) — each goes through its own gated apply path.\n                let mut updates: Vec<RegistryUpdate> = Vec::new();\n                let mut labels: Vec<NodeLabelUpdate> = Vec::new();\n                for rec in records {\n                    match rec {\n                        RegistryFeedRecord::Instance(u) => updates.push(u),\n                        RegistryFeedRecord::NodeLabel(n) => labels.push(n),\n                    }\n                }\n                if !labels.is_empty() {\n                    merged += registry.apply_node_labels(origin, &labels, &policy).len();\n                }\n                if updates.is_empty() {\n                    continue;\n                }\n                let (verdicts, flips) = registry.apply_feed_flips(origin, &updates, &policy);\n                merged += verdicts.len();\n                // Attention shift (D9-2): only ids this node actually hosts\n…\n            }\n…\n        }\n    }\n}","startLine":70,"lineNumbers":[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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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,null,424,null,428,null,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,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,null,567,568,null,597,null,635,null,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,null,730,null,737,738,739]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"}}},"isError":false,"timestamp":1784247600545}}
{"type":"message","id":"8b3dd978","parentId":"9ca7175d","timestamp":"2026-07-17T00:20:00.597Z","message":{"role":"toolResult","toolCallId":"call_BubQ8qg1qWkYU5hJlIpIwBs8|fc_0996e78cf50e3160016a59752fb1908199abd10a5783d81d94","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n\n## src/\n### brain.rs\n 1445|\n*1446|    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A).\n 1447|    /// The dispatcher calls this when a serve loop runs to completion, so no\n 1448|    /// future dispatcher generation rediscovers and replays the stream\n 1449|    /// (KNOWN-HAZARDS 7.41). Best-effort at the caller: an older broker\n...\n 1451|    /// broker to cycle onto the new binary).\n*1452|    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n 1453|    pub fn net_stream_retire(&mut self, stream_id: u64) -> io::Result<bool> {\n 1454|        self.send(\n 1455|            KIND_NET_STREAM_RETIRE,\n### broker.rs\n 4314|\n*4315|    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A):\n 4316|    /// the dispatcher reports a serve loop that ran to completion, and the row\n 4317|    /// leaves the table so no future dispatcher generation can rediscover and\n 4318|    /// replay it (the KNOWN-HAZARDS 7.41 control-steal class).\n*4319|    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n 4320|    fn dispatch_net_stream_retire(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4321|        let req: NetStreamRetireReq = serde_json::from_value(env.payload)\n 4322|            .map_err(|e| format!(\"bad net-stream-retire payload: {e}\"))?;\n### dispatch.rs\n 181|    Terminal,\n*182|    /// A transient failure released the claim; eligible again at `next_at`.\n 183|    Retry { attempts: u32, next_at: Instant },\n 184|}\n 185|\n...\n 233|/// \"excluded by lifecycle state\" half): is a `finished` row TERMINAL for\n*234|/// redispatch — retire unserved, never replay?\n 235|///\n 236|/// `finished` alone is NOT the answer, and a bare `s.finished` claim filter\n 237|/// would be a regression: the request/reply families FIN their send WITH the\n...\n 250|/// else = a pending request — serve it.\n*251|// [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n*252|// [impl->REQ-HAZARD-REDISPATCH-CONTROL-STEAL]\n 253|fn finished_row_is_terminal(family: StreamFamily, finished: bool) -> bool {\n 254|    finished && family == StreamFamily::Attach\n 255|}\n...\n 343|                if matches!(outcome, DispatchOutcome::Served(_)) {\n*344|                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n 345|                    // this stream's exchange is over — take its row out of\n 346|                    // every FUTURE dispatcher generation's enumeration (the\n 347|                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n...\n 391|/// broker predates the verb (KH-2.3 N-1 window). `Ok(None)` = unclassifiable\n*392|/// (terminal); `Err` = transient (the claim requeues bounded).\n 393|fn first_line(\n 394|    broker_name: &str,\n 395|    stream_id: u64,\n...\n 487|    if finished_row_is_terminal(family, finished) {\n*488|        return DispatchOutcome::Served(\"finished attach: terminal, retired unserved\".into());\n 489|    }\n 490|\n 491|    let mut brain = match connect(broker_name) {\n...\n 960|    // [unit->REQ-DISPATCH-CLAIM-RETRY] the claim lifecycle, at its pure seam:\n*961|    // a TRANSIENT failure releases the claim and requeues it after a backoff;\n 962|    // TERMINAL outcomes (served, unclassifiable, budget spent) never re-enter\n 963|    // — a persistently-failing stream retries exactly its bounded budget and\n 964|    // stops (the no-hot-loop ruling), where the pre-fix one-shot HashSet\n...\n 1002|\n*1003|    // [unit->REQ-REDISPATCH-FINISHED-RETIRE] the lifecycle+family\n 1004|    // discriminator (gate round 1): a finished ATTACH is terminal for\n*1005|    // redispatch (the operator detached — replay = the 7.41 controller\n 1006|    // steal), while a finished REQUEST/REPLY stream is NOT — sync and update\n 1007|    // requesters FIN their send WITH the request (sync.rs request_sync /\n 1008|    // propagate.rs fetch_update) and await the reply, so a bare finished\n### msg.rs\n 152|/// the caller treats retire as best-effort).\n*153|// [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n 154|pub const KIND_NET_STREAM_RETIRE: &str = \"net-stream-retire\";\n 155|/// Broker→brain: the retire ack (`removed` = a row actually left the table).\n 156|pub const KIND_NET_STREAM_RETIRED: &str = \"net-stream-retired\";\n...\n 808|pub struct NetStreamRetireReq {\n*809|    /// The completed stream to retire from redispatch eligibility.\n 810|    pub stream_id: u64,\n 811|}\n 812|\n### nethost.rs\n 544|    /// is load-bearing; physical teardown belongs to the conn-close sweep only.\n*545|    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n 546|    retired: std::sync::atomic::AtomicBool,\n 547|}\n 548|\n...\n 707|        // else reads a dead conn's stream ring.\n*708|        // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n 709|        sh_close\n 710|            .streams\n 711|            .lock()\n...\n 1560|    /// call newly retired the row (idempotent; absent row = false).\n*1561|    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n 1562|    pub fn retire_stream(&self, stream_id: u64) -> bool {\n 1563|        let Some(entry) = self\n 1564|            .shared\n...\n 2091|\n*2092|    // [unit->REQ-REDISPATCH-FINISHED-RETIRE] retirement hides EXACTLY the\n 2093|    // named row from dispatch eligibility: the retired stream leaves the\n 2094|    // dispatch enumeration (stream_infos) while the active sibling stays\n 2095|    // claimable, a second retire is a no-op, and the pinned-opener query\n...\n 2130|\n*2131|    // [unit->REQ-REDISPATCH-FINISHED-RETIRE] retirement never severs the\n 2132|    // transport: a reply sent AFTER the row was retired still flushes to the\n 2133|    // peer. This is the field race's lost ordering made deterministic — the\n 2134|    // worker's reply send is fire-and-forget (brain returns before the broker\n...\n 2176|\n*2177|    // [unit->REQ-REDISPATCH-FINISHED-RETIRE] a dead CONNECTION retires its\n 2178|    // stream rows with it: the closed-watcher sweep leaves nothing for a\n 2179|    // future dispatcher generation to replay on a transport that can never\n 2180|    // serve again (the field bug's historical-rows-forever half).\n\n## tests/\n### redispatch.rs\n*1|//! REDISPATCH-TRUTH W1 — the three production-path regressions (ADR-0038 D,\n 2|//! KNOWN-HAZARDS 7.41): every leg drives the REAL\n 3|//! `dispatch::run_dispatch_loop` (as a killable `dispatch_fixture` child —\n 4|//! a dispatcher GENERATION whose death drops its workers' broker conns\n...\n 17|//!   resumes the SAME operator stream.\n*18|//! - D3: ONE injected transient worker-start failure → the claim requeues\n 19|//!   and the retry serves the stream (no permanent abandonment, no\n 20|//!   duplicate controller).\n 21|//!\n...\n 211|\n*212|// [int->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] D1 — the field bug, end to end\n 213|// through PRODUCTION rediscovery: a finished historical Attach and a current\n 214|// active Attach on the same endpoint/origin; only the dispatcher generation\n 215|// restarts. The historical stream must neither take nor clear the current\n...\n 219|// and the retired row is GONE from the broker's enumeration\n*220|// (REQ-REDISPATCH-FINISHED-RETIRE observed at the seam that matters).\n 221|#[test]\n 222|fn finished_historical_attach_neither_takes_nor_clears_current_controller() {\n 223|    init_home();\n...\n 255|    while peer_stream_ids(&mut probe).contains(&s1_a) {\n*256|        assert!(Instant::now() < end, \"the finished attach row never retired from the table\");\n 257|        thread::sleep(Duration::from_millis(50));\n 258|    }\n 259|\n...\n 302|\n*303|// [int->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] D1b (gate round 1) — the\n*304|// finished-UNRETIRED orderings the Served-report path can never see: the\n 305|// pre-existing backlog and the brain-down-detach window. s1 is attached and\n 306|// driven under gen1, gen1 is KILLED mid-serve (no Served report), THEN the\n 307|// operator detaches (the row finishes with no dispatcher alive; the conn\n 308|// stays open, so the conn-close sweep does not fire either). gen2 starts and\n*309|// serves a fresh current attach: the finished s1 row must be retired\n 310|// UNSERVED (the family-aware terminal check — pre-fix, the pinned opener\n 311|// made this replay PERFECTLY: seeded attach_as steals the current\n 312|// controller, replayed input re-types, replayed EOF clears). The current\n...\n 315|// ever being served (retire-on-sight drains the backlog).\n*316|// [int->REQ-REDISPATCH-FINISHED-RETIRE]\n 317|#[test]\n*318|fn finished_unretired_attach_is_skipped_and_drained_not_replayed() {\n 319|    init_home();\n 320|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 321|    let (name_a, name_b) = (unique_name(), unique_name());\n...\n 399|        while peer_stream_ids(&mut probe).contains(&s1_a) {\n*400|            assert!(Instant::now() < end, \"the finished-unretired row never drained from the table\");\n 401|            thread::sleep(Duration::from_millis(50));\n 402|        }\n 403|    }\n...\n 406|        !gen2_log.contains(&format!(\"DISPATCH:{s1_a}:\")),\n*407|        \"gen2 replayed the finished-unretired stream {s1_a}: {gen2_log}\"\n 408|    );\n 409|\n 410|    gen2.kill();\n...\n 413|\n*414|// [int->REQ-REDISPATCH-FINISHED-RETIRE] the GUARD leg for the family-aware\n 415|// discriminator (gate round 1 counter-check): a request/reply stream reads\n 416|// `finished=true` the moment its requester FINs with the request — sync's\n 417|// `request_sync` does exactly that — so a finished row must NOT be blanket-\n...\n 495|#[test]\n*496|fn opener_evicted_active_stream_resumes_after_dispatcher_restart() {\n 497|    init_home();\n 498|    // Shrink the ring BEFORE the broker starts (read once at NetShared::new).\n 499|    // nextest = one process per test: no cross-test env bleed.\n...\n 577|#[test]\n*578|fn transient_worker_start_failure_recovers_via_claim_retry() {\n 579|    init_home();\n 580|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 581|    let (name_a, name_b) = (unique_name(), unique_name());\n\n### fixtures/\n#### dispatch_fixture.rs\n*1|//! Test-only dispatcher-generation fixture (REDISPATCH-TRUTH D-regressions,\n*2|//! `tests/redispatch.rs`): runs the REAL `dispatch::run_dispatch_loop` against\n 3|//! the named broker, as a child process the test can KILL — a killed\n 4|//! generation drops its workers' broker connections exactly like a production\n 5|//! brain death, which no in-process thread can simulate (a parked serve-loop"}],"details":{"scopePath":"crates/spt-daemon/src, crates/spt-daemon/tests, crates/spt/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":39,"fileCount":7,"files":["crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/tests/fixtures/dispatch_fixture.rs","crates/spt-daemon/tests/redispatch.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/brain.rs","count":2},{"path":"crates/spt-daemon/src/broker.rs","count":2},{"path":"crates/spt-daemon/src/dispatch.rs","count":10},{"path":"crates/spt-daemon/src/msg.rs","count":2},{"path":"crates/spt-daemon/src/nethost.rs","count":6},{"path":"crates/spt-daemon/tests/fixtures/dispatch_fixture.rs","count":2},{"path":"crates/spt-daemon/tests/redispatch.rs","count":15}],"truncated":false,"displayContent":"# crates/spt-daemon/\n\n## src/\n### brain.rs\n 1445│\n*1446│    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A).\n 1447│    /// The dispatcher calls this when a serve loop runs to completion, so no\n 1448│    /// future dispatcher generation rediscovers and replays the stream\n 1449│    /// (KNOWN-HAZARDS 7.41). Best-effort at the caller: an older broker\n     │...\n 1451│    /// broker to cycle onto the new binary).\n*1452│    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n 1453│    pub fn net_stream_retire(&mut self, stream_id: u64) -> io::Result<bool> {\n 1454│        self.send(\n 1455│            KIND_NET_STREAM_RETIRE,\n### broker.rs\n 4314│\n*4315│    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A):\n 4316│    /// the dispatcher reports a serve loop that ran to completion, and the row\n 4317│    /// leaves the table so no future dispatcher generation can rediscover and\n 4318│    /// replay it (the KNOWN-HAZARDS 7.41 control-steal class).\n*4319│    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n 4320│    fn dispatch_net_stream_retire(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4321│        let req: NetStreamRetireReq = serde_json::from_value(env.payload)\n 4322│            .map_err(|e| format!(\"bad net-stream-retire payload: {e}\"))?;\n### dispatch.rs\n  181│    Terminal,\n *182│    /// A transient failure released the claim; eligible again at `next_at`.\n  183│    Retry { attempts: u32, next_at: Instant },\n  184│}\n  185│\n     │...\n  233│/// \"excluded by lifecycle state\" half): is a `finished` row TERMINAL for\n *234│/// redispatch — retire unserved, never replay?\n  235│///\n  236│/// `finished` alone is NOT the answer, and a bare `s.finished` claim filter\n  237│/// would be a regression: the request/reply families FIN their send WITH the\n     │...\n  250│/// else = a pending request — serve it.\n *251│// [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n *252│// [impl->REQ-HAZARD-REDISPATCH-CONTROL-STEAL]\n  253│fn finished_row_is_terminal(family: StreamFamily, finished: bool) -> bool {\n  254│    finished && family == StreamFamily::Attach\n  255│}\n     │...\n  343│                if matches!(outcome, DispatchOutcome::Served(_)) {\n *344│                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n  345│                    // this stream's exchange is over — take its row out of\n  346│                    // every FUTURE dispatcher generation's enumeration (the\n  347│                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n     │...\n  391│/// broker predates the verb (KH-2.3 N-1 window). `Ok(None)` = unclassifiable\n *392│/// (terminal); `Err` = transient (the claim requeues bounded).\n  393│fn first_line(\n  394│    broker_name: &str,\n  395│    stream_id: u64,\n     │...\n  487│    if finished_row_is_terminal(family, finished) {\n *488│        return DispatchOutcome::Served(\"finished attach: terminal, retired unserved\".into());\n  489│    }\n  490│\n  491│    let mut brain = match connect(broker_name) {\n     │...\n  960│    // [unit->REQ-DISPATCH-CLAIM-RETRY] the claim lifecycle, at its pure seam:\n *961│    // a TRANSIENT failure releases the claim and requeues it after a backoff;\n  962│    // TERMINAL outcomes (served, unclassifiable, budget spent) never re-enter\n  963│    // — a persistently-failing stream retries exactly its bounded budget and\n  964│    // stops (the no-hot-loop ruling), where the pre-fix one-shot HashSet\n     │...\n 1002│\n*1003│    // [unit->REQ-REDISPATCH-FINISHED-RETIRE] the lifecycle+family\n 1004│    // discriminator (gate round 1): a finished ATTACH is terminal for\n*1005│    // redispatch (the operator detached — replay = the 7.41 controller\n 1006│    // steal), while a finished REQUEST/REPLY stream is NOT — sync and update\n 1007│    // requesters FIN their send WITH the request (sync.rs request_sync /\n 1008│    // propagate.rs fetch_update) and await the reply, so a bare finished\n### msg.rs\n 152│/// the caller treats retire as best-effort).\n*153│// [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n 154│pub const KIND_NET_STREAM_RETIRE: &str = \"net-stream-retire\";\n 155│/// Broker→brain: the retire ack (`removed` = a row actually left the table).\n 156│pub const KIND_NET_STREAM_RETIRED: &str = \"net-stream-retired\";\n    │...\n 808│pub struct NetStreamRetireReq {\n*809│    /// The completed stream to retire from redispatch eligibility.\n 810│    pub stream_id: u64,\n 811│}\n 812│\n### nethost.rs\n  544│    /// is load-bearing; physical teardown belongs to the conn-close sweep only.\n *545│    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n  546│    retired: std::sync::atomic::AtomicBool,\n  547│}\n  548│\n     │...\n  707│        // else reads a dead conn's stream ring.\n *708│        // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n  709│        sh_close\n  710│            .streams\n  711│            .lock()\n     │...\n 1560│    /// call newly retired the row (idempotent; absent row = false).\n*1561│    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n 1562│    pub fn retire_stream(&self, stream_id: u64) -> bool {\n 1563│        let Some(entry) = self\n 1564│            .shared\n     │...\n 2091│\n*2092│    // [unit->REQ-REDISPATCH-FINISHED-RETIRE] retirement hides EXACTLY the\n 2093│    // named row from dispatch eligibility: the retired stream leaves the\n 2094│    // dispatch enumeration (stream_infos) while the active sibling stays\n 2095│    // claimable, a second retire is a no-op, and the pinned-opener query\n     │...\n 2130│\n*2131│    // [unit->REQ-REDISPATCH-FINISHED-RETIRE] retirement never severs the\n 2132│    // transport: a reply sent AFTER the row was retired still flushes to the\n 2133│    // peer. This is the field race's lost ordering made deterministic — the\n 2134│    // worker's reply send is fire-and-forget (brain returns before the broker\n     │...\n 2176│\n*2177│    // [unit->REQ-REDISPATCH-FINISHED-RETIRE] a dead CONNECTION retires its\n 2178│    // stream rows with it: the closed-watcher sweep leaves nothing for a\n 2179│    // future dispatcher generation to replay on a transport that can never\n 2180│    // serve again (the field bug's historical-rows-forever half).\n\n## tests/\n### redispatch.rs\n  *1│//! REDISPATCH-TRUTH W1 — the three production-path regressions (ADR-0038 D,\n   2│//! KNOWN-HAZARDS 7.41): every leg drives the REAL\n   3│//! `dispatch::run_dispatch_loop` (as a killable `dispatch_fixture` child —\n   4│//! a dispatcher GENERATION whose death drops its workers' broker conns\n    │...\n  17│//!   resumes the SAME operator stream.\n *18│//! - D3: ONE injected transient worker-start failure → the claim requeues\n  19│//!   and the retry serves the stream (no permanent abandonment, no\n  20│//!   duplicate controller).\n  21│//!\n    │...\n 211│\n*212│// [int->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] D1 — the field bug, end to end\n 213│// through PRODUCTION rediscovery: a finished historical Attach and a current\n 214│// active Attach on the same endpoint/origin; only the dispatcher generation\n 215│// restarts. The historical stream must neither take nor clear the current\n    │...\n 219│// and the retired row is GONE from the broker's enumeration\n*220│// (REQ-REDISPATCH-FINISHED-RETIRE observed at the seam that matters).\n 221│#[test]\n 222│fn finished_historical_attach_neither_takes_nor_clears_current_controller() {\n 223│    init_home();\n    │...\n 255│    while peer_stream_ids(&mut probe).contains(&s1_a) {\n*256│        assert!(Instant::now() < end, \"the finished attach row never retired from the table\");\n 257│        thread::sleep(Duration::from_millis(50));\n 258│    }\n 259│\n    │...\n 302│\n*303│// [int->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] D1b (gate round 1) — the\n*304│// finished-UNRETIRED orderings the Served-report path can never see: the\n 305│// pre-existing backlog and the brain-down-detach window. s1 is attached and\n 306│// driven under gen1, gen1 is KILLED mid-serve (no Served report), THEN the\n 307│// operator detaches (the row finishes with no dispatcher alive; the conn\n 308│// stays open, so the conn-close sweep does not fire either). gen2 starts and\n*309│// serves a fresh current attach: the finished s1 row must be retired\n 310│// UNSERVED (the family-aware terminal check — pre-fix, the pinned opener\n 311│// made this replay PERFECTLY: seeded attach_as steals the current\n 312│// controller, replayed input re-types, replayed EOF clears). The current\n    │...\n 315│// ever being served (retire-on-sight drains the backlog).\n*316│// [int->REQ-REDISPATCH-FINISHED-RETIRE]\n 317│#[test]\n*318│fn finished_unretired_attach_is_skipped_and_drained_not_replayed() {\n 319│    init_home();\n 320│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 321│    let (name_a, name_b) = (unique_name(), unique_name());\n    │...\n 399│        while peer_stream_ids(&mut probe).contains(&s1_a) {\n*400│            assert!(Instant::now() < end, \"the finished-unretired row never drained from the table\");\n 401│            thread::sleep(Duration::from_millis(50));\n 402│        }\n 403│    }\n    │...\n 406│        !gen2_log.contains(&format!(\"DISPATCH:{s1_a}:\")),\n*407│        \"gen2 replayed the finished-unretired stream {s1_a}: {gen2_log}\"\n 408│    );\n 409│\n 410│    gen2.kill();\n    │...\n 413│\n*414│// [int->REQ-REDISPATCH-FINISHED-RETIRE] the GUARD leg for the family-aware\n 415│// discriminator (gate round 1 counter-check): a request/reply stream reads\n 416│// `finished=true` the moment its requester FINs with the request — sync's\n 417│// `request_sync` does exactly that — so a finished row must NOT be blanket-\n    │...\n 495│#[test]\n*496│fn opener_evicted_active_stream_resumes_after_dispatcher_restart() {\n 497│    init_home();\n 498│    // Shrink the ring BEFORE the broker starts (read once at NetShared::new).\n 499│    // nextest = one process per test: no cross-test env bleed.\n    │...\n 577│#[test]\n*578│fn transient_worker_start_failure_recovers_via_claim_retry() {\n 579│    init_home();\n 580│    let dir = tempfile::tempdir().expect(\"tempdir\");\n 581│    let (name_a, name_b) = (unique_name(), unique_name());\n\n### fixtures/\n#### dispatch_fixture.rs\n*1│//! Test-only dispatcher-generation fixture (REDISPATCH-TRUTH D-regressions,\n*2│//! `tests/redispatch.rs`): runs the REAL `dispatch::run_dispatch_loop` against\n 3│//! the named broker, as a child process the test can KILL — a killed\n 4│//! generation drops its workers' broker connections exactly like a production\n 5│//! brain death, which no in-process thread can simulate (a parked serve-loop"},"isError":false,"timestamp":1784247600597}}
{"type":"message","id":"7c81ee56","parentId":"8b3dd978","timestamp":"2026-07-17T00:20:10.476Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning code comparison and reporting**"},{"type":"toolCall","id":"call_B5hA8WfZOgRqoEiAGf28FT2t|fc_0996e78cf50e3160016a59753991a8819980ea6f1224541606","name":"glob","arguments":{"i":"Locating redispatch RCA report","path":".claude/reports/2026-07-16-redispatch-truth/**/*","hidden":true,"gitignore":false,"limit":100}},{"type":"toolCall","id":"call_rzgAG80gSLF7nAR67AYOKBk9|fc_0996e78cf50e3160016a59753991b881999960b4675625906a","name":"read","arguments":{"i":"Reading redispatch integration coverage","path":"crates/spt-daemon/tests/redispatch.rs","selector":"1-120,200-430,480-650"}},{"type":"toolCall","id":"call_UusjlKbfL8EaFp3g8aUDoSTr|fc_0996e78cf50e3160016a59753991c8819985f002a9b3bdb9e7","name":"read","arguments":{"i":"Reading output log ownership details","path":"crates/spt-daemon/src/broker.rs","selector":"80-205,840-1220,1420-1610,2960-3025,3330-3425,3440-3515"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":18659,"output":326,"cacheRead":65024,"cacheWrite":0,"totalTokens":84009,"reasoningTokens":142,"cost":{"input":0.093295,"output":0.00978,"cacheRead":0.032512,"cacheWrite":0,"total":0.135587}},"stopReason":"toolUse","timestamp":1784247600655,"responseId":"resp_0996e78cf50e3160016a5975310434819998094fa296205b21","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a59753385fc81999e5ff5d2e5b2ea39","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXU5gOcWg3ousOUthVoj9p4a5sI1hspambXPG37gH6-PC4Nl6V8-oBl6jxOxbDYWcbUC7spDefpyrixMhMDcNmueXHMSqyqJPL_npOJoXRqgkDydB2EXR9AxaZslEJrM0d0MBnaiuGhgySBb5ElTHRh81voochbYh2b4-g4Ne1Wecs_90nerY_XQFfUTZqsNS8B3pqr-ssNa9z04V3NlASlldtGhYRkXQ-Ap7R-w6aRKNT7O4V_3zetEtn3YFzX7tCcXMSzwd6QmF8_DVbWbUujZ8rcv7iGhLxgoEMOHeDXQKwiLraCXlHDGXWYipu8PSFtM5Hoj2XXgSNS73vO5TJm7QZG-M8v7uMdbkzi5o_a3PZWI9dhjgsvf1OclrKvxs3fGr_s1wu0qGc6IWpFNWItkmKx6MvXyZEqj0K8oqiFhbcLYp4XhtyJi3O_MGXoY36jwaKnnEfcxarcpJkb4ZGi_thqat8olTWHCBQecPzJaObgUvYiONNTBkJvjEQCth_dTJfEoSmRC1aWMq94QgoL2JWfjnL_LkyfuFDQ1nS0HtcLHJat1gWck3GxNr01PE46iAT8Cl8RRtHwzToaUAZAKbON-fxx41Mu38TWKtQwYrPllGxo4h9pHeUjQZmmJNhFXYxMY-NPjxvrSoi0luSFalSVYRib6ko-yewy9pPhraO-Vt13vhB2CHIwdQX-UFqjQqAIe-RMgSYvQg9KZUU87xDrtYOxzgtM5kgTd-Hd7bA2L79FrzD3DolRL6buj8PXpQJlCjDvuRyZX86HhJtq4RD0Iy6nMTvtwWz6K6F-JlFw1DFhUwftbfg00Iiq5YybWyXl3c00LXLCUZ-cDXBnNvfCLZnrBC12kmfhrJvPCigJ_AQ0kxobHy3uyamrr4ropTNjJr2_5tRdoFv0OZ5rc3V0qWb8Bx8lORydHG4V9G_YuQzbTAZW8kjlcA-d2F8jIvpUPqnkV4erqXTmVw3l97CmO_OwS9cEs_O590l4BP9ewJKJ7K7Twn_OnF3qikTiTk4qeZFxZaBXW2FNGQ_-hQjA2L9HJkCCFRW1TAr4wHuGJ57w3Y7yAkslXjKd9JtPyTIvSvqRIygiy4l2NdGLqc-5JW_uJR7ZeDe2t3ijRu745R1CoGnyTXM_mzmGAo2e6Xr0xYklVHxXgA8TAWcJg1lR7NoMjilPyuG77m_8dbAsb1g1buyAVHZaLlwGBnW4_bdd91jAbN2LmqoGY_m_lrNCghnbXpszLwJhZbqPKuw8rVlfg1BcehBUkZTOg8N4K1SWFhSRrKBlMVTQSSJ-PLjm49XSl0lEaL2bKYIyPJbjpEz1012gCxfm45pi9biyz4ISwfEdYx_YYUH3sDXb3NxYl_iGE0lzFbKAjnCvDvY61WCtkKFclHKtj3YnIb67JUBuydmKHwAmL6GZdfDPe22zk0-JZRW_bE_JwwI6mizF0OurpnVpKocN2utiwKLegcUdkPUBK58pJYgjkK6jK7emDLDs64fMsFNUwKLGrK1oYXc96ElQwz-NCyGGnNnY1kYVRSSpDzFFo98IDrpDYF175PKKzJkk7Ob352yK6n70rtIYl5pwQpiE3jETE5XcCPviWpESVpV39R7XzX3fwCQAD9EJQ83SkFXuPWLsAJboqeaaoJqshFPAx2ElksyOg2XNPg6RPlxoeb8VqgbYb6OPFmy7ZkuH1jgxwNmMqkVe48XbKSTVV-wmwp89avOHmu8blVa_13Jo_4Yejtc5ja1VH1uHu9KeOwt1yow9xyG00cYgbuOmpK3haoVHcCd3ECxX9a2E1Jzap8QqFrx3EebEeVos0d1xkfgJoNCNPk4V90u_8LeRUR8fPUlj9CMJTS3Ry-mH_CYXqJ3_dmKj9pA==","summary":[{"type":"summary_text","text":"**Planning code comparison and reporting**"}]},{"id":"fc_0996e78cf50e3160016a59753991a8819980ea6f1224541606","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating redispatch RCA report\",\"path\":\".claude/reports/2026-07-16-redispatch-truth/**/*\",\"hidden\":true,\"gitignore\":false,\"limit\":100}","call_id":"call_B5hA8WfZOgRqoEiAGf28FT2t","name":"glob"},{"id":"fc_0996e78cf50e3160016a59753991b881999960b4675625906a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading redispatch integration coverage\",\"path\":\"crates/spt-daemon/tests/redispatch.rs\",\"selector\":\"1-120,200-430,480-650\"}","call_id":"call_rzgAG80gSLF7nAR67AYOKBk9","name":"read"},{"id":"fc_0996e78cf50e3160016a59753991c8819985f002a9b3bdb9e7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading output log ownership details\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"80-205,840-1220,1420-1610,2960-3025,3330-3425,3440-3515\"}","call_id":"call_UusjlKbfL8EaFp3g8aUDoSTr","name":"read"}]},"duration":9794.746899999678,"ttft":2885.969899997115,"contextSnapshot":{"promptTokens":83683,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_B5hA8WfZOgRqoEiAGf28FT2t|fc_0996e78cf50e3160016a59753991a8819980ea6f1224541606","toolName":"glob","startedAt":"2026-07-17T00:20:10.477Z","args":{"path":".claude/reports/2026-07-16-redispatch-truth/**/*"},"intent":"Locating redispatch RCA report"},"id":"2bc0ef0b","parentId":"7c81ee56","timestamp":"2026-07-17T00:20:10.477Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_rzgAG80gSLF7nAR67AYOKBk9|fc_0996e78cf50e3160016a59753991b881999960b4675625906a","toolName":"read","startedAt":"2026-07-17T00:20:10.477Z","args":{"path":"crates/spt-daemon/tests/redispatch.rs"},"intent":"Reading redispatch integration coverage"},"id":"c87d9c66","parentId":"2bc0ef0b","timestamp":"2026-07-17T00:20:10.477Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_UusjlKbfL8EaFp3g8aUDoSTr|fc_0996e78cf50e3160016a59753991c8819985f002a9b3bdb9e7","toolName":"read","startedAt":"2026-07-17T00:20:10.477Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading output log ownership details"},"id":"e6d2d161","parentId":"c87d9c66","timestamp":"2026-07-17T00:20:10.477Z"}
{"type":"message","id":"8ab88742","parentId":"e6d2d161","timestamp":"2026-07-17T00:20:10.525Z","message":{"role":"toolResult","toolCallId":"call_rzgAG80gSLF7nAR67AYOKBk9|fc_0996e78cf50e3160016a59753991b881999960b4675625906a","toolName":"read","content":[{"type":"text","text":"//! REDISPATCH-TRUTH W1 — the three production-path regressions (ADR-0038 D,\n//! KNOWN-HAZARDS 7.41): every leg drives the REAL\n//! `dispatch::run_dispatch_loop` (as a killable `dispatch_fixture` child —\n//! a dispatcher GENERATION whose death drops its workers' broker conns\n//! exactly like a production brain death). NO manual re-serve anywhere: the\n//! pre-fix e2e (`attach_survives_target_brain_restart_exactly_once`)\n//! hand-stepped a known stream and bypassed exactly the rediscovery/\n//! classification seam that was broken in the field.\n//!\n//! - D1: a FINISHED historical Attach + a CURRENT active Attach, same\n//!   endpoint/origin; restart the dispatcher generation only → the\n//!   historical stream neither takes nor clears the current controller\n//!   (post-restart input still renders; no EOF; no replay-storm log line;\n//!   the retired row left the broker's enumeration).\n//! - D2: an ACTIVE Attach whose opener rolled out of the bounded ring;\n//!   restart the dispatcher → durable classification (the pinned opener)\n//!   resumes the SAME operator stream.\n//! - D3: ONE injected transient worker-start failure → the claim requeues\n//!   and the retry serves the stream (no permanent abandonment, no\n//!   duplicate controller).\n//!\n//! HEAVY AT BIRTH (FLAKE-LEDGER #15): spawns real child processes + two\n//! hermetic QUIC brokers per test.\n\nuse std::process::{Child, Command, Stdio};\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::attach::{request_attach, send_attach_input};\nuse spt_daemon::brain::{Brain, BrokerEvent};\nuse spt_daemon::effect::{Minter, MintedOp};\nuse spt_daemon::msg::SpawnReq;\nuse spt_daemon::nethost::{NetConfig, NetHost};\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_term::ScreenGrid;\n\n/// Hold the temp home for the whole test binary (the attach.rs discipline).\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n\nfn init_home() {\n    HOME.get_or_init(|| {\n        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n        std::env::set_var(\"SPT_HOME\", dir.path());\n        dir\n    });\n}\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique_name() -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-rdsp-{}-{}.sock\", std::process::id(), n)\n}\n\nfn hermetic() -> NetConfig {\n    NetConfig {\n        identity: Identity::generate(),\n        relay: RelayPolicy::Disabled,\n        discovery: LocalDiscovery::Off,\n        scope: BindScope::Loopback,\n        membership: None,\n    }\n}\n\nfn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n    let host = NetHost::start(hermetic()).expect(\"net host start\");\n    let broker =\n        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n    broker\n}\n\nfn connect_retry(name: &str) -> Brain {\n    for _ in 0..300 {\n        if let Ok(b) = Brain::cold_start(name, 1) {\n            return b;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"brain could not connect\");\n}\n\nfn echo_spawn_req(endpoint: &str) -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n    #[cfg(windows)]\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n    SpawnReq {\n        program,\n        args,\n        rows: 24,\n        cols: 80,\n        endpoint: endpoint.to_string(),\n        cwd: None,\n        env: Default::default(),\n        translation_binary: None,\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…\n    }\n…\n}\n…\nfn render_until(\n…\n) -> bool {\n…\n    while !contains(buf, needle) {\n…\n        match operator.read_event().expect(\"operator read event\") {\n…\n            _ => {}\n        }\n    }\n    saw_eof\n}\n\nfn screen_of(viewport: &[u8]) -> String {\n    let mut screen = ScreenGrid::new(24, 80);\n    screen.advance(viewport);\n    String::from_utf8_lossy(&screen.render_repaint()).into_owned()\n}\n\n// [int->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] D1 — the field bug, end to end\n// through PRODUCTION rediscovery: a finished historical Attach and a current\n// active Attach on the same endpoint/origin; only the dispatcher generation\n// restarts. The historical stream must neither take nor clear the current\n// controller: post-restart input still renders (the frozen-PTY symptom is\n// the failure), the operator sees NO EOF (the replayed-EOF detach is the\n// clear), the fresh generation logs NO replay of the historical stream id,\n// and the retired row is GONE from the broker's enumeration\n// (REQ-REDISPATCH-FINISHED-RETIRE observed at the seam that matters).\n#[test]\nfn finished_historical_attach_neither_takes_nor_clears_current_controller() {\n    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(\"rd1-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    // ── The HISTORICAL attach: drive it, then detach → its exchange is over.\n    let s1 = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request s1\");\n    operator.net_stream_subscribe(s1, 0).expect(\"subscribe s1\");\n    send_attach_input(&mut operator, s1, b\"HIST-MARK\\r\", 10).expect(\"input s1\");\n    let mut dec1 = AttachDecoder::new();\n    let (mut cur1, mut view1) = (0u64, Vec::new());\n    render_until(&mut operator, s1, &mut dec1, &mut cur1, &mut view1, b\"HIST-MARK\", Duration::from_secs(20));\n    let s1_a = *peer_stream_ids(&mut probe).first().expect(\"s1 row on A\");\n    operator.net_stream_send(s1, &[], None, true).expect(\"detach s1\");\n\n    // Served-to-completion → the worker retires the row (ADR-0038 A). This is\n    // the enumeration a FRESH dispatcher would replay from — it must shrink.\n    let end = Instant::now() + Duration::from_secs(20);\n    while peer_stream_ids(&mut probe).contains(&s1_a) {\n        assert!(Instant::now() < end, \"the finished attach row never retired from the table\");\n        thread::sleep(Duration::from_millis(50));\n    }\n\n    // ── The CURRENT attach: same endpoint, same origin, actively controlled.\n    let s2 = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 3), AttachIntent::Control).expect(\"request s2\");\n    operator.net_stream_subscribe(s2, 0).expect(\"subscribe s2\");\n    send_attach_input(&mut operator, s2, b\"CUR-ONE\\r\", 11).expect(\"input s2 pre\");\n    let mut dec2 = AttachDecoder::new();\n    let (mut cur2, mut view2) = (0u64, Vec::new());\n    let eof_pre = render_until(&mut operator, s2, &mut dec2, &mut cur2, &mut view2, b\"CUR-ONE\", Duration::from_secs(20));\n    assert!(!eof_pre, \"no EOF while gen1 serves the live viewport\");\n    let s2_a = *peer_stream_ids(&mut probe).first().expect(\"s2 row on A\");\n\n    // ── The brain cycle: kill generation 1, start generation 2 — PRODUCTION\n    // rediscovery re-serves what the broker still enumerates.\n    gen1.kill();\n    let gen2 = Generation::spawn(&name_a, dir.path().join(\"gen2.log\"), &[]);\n\n    // THE regression: input typed AFTER the restart still renders — the\n    // reconstruction re-took the controller (successor re-take parity), and\n    // no historical replay stole/cleared it (pre-fix: frozen PTY here).\n    send_attach_input(&mut operator, s2, b\"CUR-TWO\\r\", 12).expect(\"input s2 post\");\n    let eof_post = render_until(&mut operator, s2, &mut dec2, &mut cur2, &mut view2, b\"CUR-TWO\", Duration::from_secs(30));\n    assert!(\n        !eof_post,\n        \"the current controller's stream saw an EOF across the restart — a replayed \\\n         historical detach cleared it (the 7.41 steal)\"\n    );\n    let painted = screen_of(&view2);\n    assert!(painted.contains(\"CUR-ONE\"), \"pre-restart output survived: {painted:?}\");\n    assert!(painted.contains(\"CUR-TWO\"), \"post-restart input/output flows: {painted:?}\");\n\n    // The fresh generation never touched the historical stream id — the\n    // DISPATCH:<id> replay-storm line (the field evidence class) must be\n    // absent for s1, while s2's re-serve is expected work, not noise.\n    let gen2_log = gen2.stderr_log();\n    assert!(\n        !gen2_log.contains(&format!(\"DISPATCH:{s1_a}:\")),\n        \"generation 2 replayed the retired historical stream {s1_a}: {gen2_log}\"\n    );\n    let _ = s2_a;\n\n    gen2.kill();\n    let _ = probe.kill_session();\n}\n\n// [int->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] D1b (gate round 1) — the\n// finished-UNRETIRED orderings the Served-report path can never see: the\n// pre-existing backlog and the brain-down-detach window. s1 is attached and\n// driven under gen1, gen1 is KILLED mid-serve (no Served report), THEN the\n// operator detaches (the row finishes with no dispatcher alive; the conn\n// stays open, so the conn-close sweep does not fire either). gen2 starts and\n// serves a fresh current attach: the finished s1 row must be retired\n// UNSERVED (the family-aware terminal check — pre-fix, the pinned opener\n// made this replay PERFECTLY: seeded attach_as steals the current\n// controller, replayed input re-types, replayed EOF clears). The current\n// viewport keeps flowing across several polls, sees no EOF, and gen2's log\n// never carries a DISPATCH:<s1> line; the s1 row leaves the table without\n// ever being served (retire-on-sight drains the backlog).\n// [int->REQ-REDISPATCH-FINISHED-RETIRE]\n#[test]\nfn finished_unretired_attach_is_skipped_and_drained_not_replayed() {\n    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(\"rd1b-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 (real serve, opener pinned, input in\n    // the ring — everything a replay needs to steal).\n    let s1 = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request s1\");\n    operator.net_stream_subscribe(s1, 0).expect(\"subscribe s1\");\n    send_attach_input(&mut operator, s1, b\"HIST-MARK\\r\", 10).expect(\"input s1\");\n    let mut dec1 = AttachDecoder::new();\n    let (mut cur1, mut view1) = (0u64, Vec::new());\n    render_until(&mut operator, s1, &mut dec1, &mut cur1, &mut view1, b\"HIST-MARK\", Duration::from_secs(20));\n    let s1_a = *peer_stream_ids(&mut probe).first().expect(\"s1 row on A\");\n\n    // The brain-down-detach window: kill gen1 FIRST (no worker can report\n    // Served), THEN detach — the row finishes unretired on a live conn.\n    gen1.kill();\n    operator.net_stream_send(s1, &[], None, true).expect(\"detach s1 while brain down\");\n    {\n        let end = Instant::now() + Duration::from_secs(10);\n        loop {\n            let finished = probe\n                .net_streams()\n                .expect(\"net-streams\")\n                .streams\n                .iter()\n                .any(|s| s.stream_id == s1_a && s.finished);\n            if finished {\n                break;\n            }\n            assert!(Instant::now() < end, \"s1 never read as finished on A\");\n            thread::sleep(Duration::from_millis(50));\n        }\n    }\n\n    // gen2 + a fresh CURRENT attach. Pre-fix, gen2's replay of s1 steals and\n    // clears this controller within its first poll cycles.\n    let gen2 = Generation::spawn(&name_a, dir.path().join(\"gen2.log\"), &[]);\n    let s2 = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 3), AttachIntent::Control).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 — the\n    // window in which a stale replay would land its steal.\n    for round in 0..3u32 {\n        let marker = format!(\"CUR-{round}\\r\");\n        send_attach_input(&mut operator, s2, marker.as_bytes(), 11 + 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!(!eof, \"round {round}: the current controller's stream saw an EOF — a stale replay cleared it\");\n        thread::sleep(Duration::from_millis(700));\n    }\n\n    // Retire-on-sight drained the finished row WITHOUT serving it.\n    {\n        let end = Instant::now() + Duration::from_secs(15);\n        while peer_stream_ids(&mut probe).contains(&s1_a) {\n            assert!(Instant::now() < end, \"the finished-unretired row never drained from the table\");\n            thread::sleep(Duration::from_millis(50));\n        }\n    }\n    let gen2_log = gen2.stderr_log();\n    assert!(\n        !gen2_log.contains(&format!(\"DISPATCH:{s1_a}:\")),\n        \"gen2 replayed the finished-unretired stream {s1_a}: {gen2_log}\"\n    );\n\n    gen2.kill();\n    let _ = probe.kill_session();\n}\n\n// [int->REQ-REDISPATCH-FINISHED-RETIRE] the GUARD leg for the family-aware\n// discriminator (gate round 1 counter-check): a request/reply stream reads\n// `finished=true` the moment its requester FINs with the request — sync's\n// `request_sync` does exactly that — so a finished row must NOT be blanket-\n// terminal. The request here arrives while NO dispatcher generation is\n// alive (the refresh window), sits finished-unserved, and the next\n// generation must still SERVE it: the pull completes with a reply. A bare\n// `s.finished` claim filter fails this test (context sync dead across every\n// brain cycle).\n#[test]\nfn finished_pending_sync_request_still_serves_across_generation_start() {\n    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…\n    assert!(\n        report.applied.is_empty(),\n        \"an unknown ref applies nothing (the completed reply itself is the assertion): {report:?}\"\n    );\n    let _ = s_a;\n\n    gen.kill();\n}\n\n// [int->REQ-STREAM-OPENER-DURABLE] D2 — the abandoned-active-stream leg: an\n// ACTIVE attach outlives its opener in the bounded ring (test-rig ring cap\n// via SPT_STREAM_RING_CHUNKS — production sizing untouched), the dispatcher\n// generation restarts, and DURABLE classification (the broker-pinned opener,\n// never ring seq 0) resumes the SAME operator stream: post-restart input\n// renders. Pre-fix this classified Unknown/Failed with the claim burned —\n// the stream was permanently abandoned.\n#[test]\nfn opener_evicted_active_stream_resumes_after_dispatcher_restart() {\n    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(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).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(&mut operator, s1, &mut dec, &mut cur, &mut view, b\"MARK-A\", Duration::from_secs(20));\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!(Instant::now() < end, \"the ring never rolled past the opener\");\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\n        .net_stream_opener(s1_a)\n        .expect(\"opener query answers\");\n    assert!(pinned.is_some(), \"the opener stays pinned across the ring roll\");\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(&mut operator, s1, &mut dec, &mut cur, &mut view, b\"MARK-B\", Duration::from_secs(30));\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.\n#[test]\nfn transient_worker_start_failure_recovers_via_claim_retry() {\n    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(\"rd3-ep\"))\n        .expect(\"spawn echo child\");\n    let a_addr = probe.net_status().expect(\"a status\").addr;\n\n    let gen1 = Generation::spawn(\n        &name_a,\n        dir.path().join(\"gen1.log\"),\n        &[(\"SPT_DISPATCH_INJECT_TRANSIENT_ONCE\", \"1\")],\n    );\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(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\n    operator.net_stream_subscribe(s1, 0).expect(\"subscribe\");\n    send_attach_input(&mut operator, s1, b\"RETRY-MARK\\r\", 10).expect(\"input\");\n\n    // Through the injected failure + the claim retry: the marker renders.\n    let mut dec = AttachDecoder::new();\n    let (mut cur, mut view) = (0u64, Vec::new());\n    render_until(&mut operator, s1, &mut dec, &mut cur, &mut view, b\"RETRY-MARK\", Duration::from_secs(30));\n\n    // The injection actually fired (this is what distinguishes \"recovered\n    // through retry\" from \"nothing ever failed\").\n    let log = gen1.stderr_log();\n    assert!(\n        log.contains(\"injected transient worker-start failure\"),\n        \"the transient-failure valve never fired — the retry path was not exercised: {log}\"\n    );\n\n    gen1.kill();\n    let _ = probe.kill_session();\n}\n"}],"details":{"displayContent":{"text":"//! REDISPATCH-TRUTH W1 — the three production-path regressions (ADR-0038 D,\n//! KNOWN-HAZARDS 7.41): every leg drives the REAL\n//! `dispatch::run_dispatch_loop` (as a killable `dispatch_fixture` child —\n//! a dispatcher GENERATION whose death drops its workers' broker conns\n//! exactly like a production brain death). NO manual re-serve anywhere: the\n//! pre-fix e2e (`attach_survives_target_brain_restart_exactly_once`)\n//! hand-stepped a known stream and bypassed exactly the rediscovery/\n//! classification seam that was broken in the field.\n//!\n//! - D1: a FINISHED historical Attach + a CURRENT active Attach, same\n//!   endpoint/origin; restart the dispatcher generation only → the\n//!   historical stream neither takes nor clears the current controller\n//!   (post-restart input still renders; no EOF; no replay-storm log line;\n//!   the retired row left the broker's enumeration).\n//! - D2: an ACTIVE Attach whose opener rolled out of the bounded ring;\n//!   restart the dispatcher → durable classification (the pinned opener)\n//!   resumes the SAME operator stream.\n//! - D3: ONE injected transient worker-start failure → the claim requeues\n//!   and the retry serves the stream (no permanent abandonment, no\n//!   duplicate controller).\n//!\n//! HEAVY AT BIRTH (FLAKE-LEDGER #15): spawns real child processes + two\n//! hermetic QUIC brokers per test.\n\nuse std::process::{Child, Command, Stdio};\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::attach::{request_attach, send_attach_input};\nuse spt_daemon::brain::{Brain, BrokerEvent};\nuse spt_daemon::effect::{Minter, MintedOp};\nuse spt_daemon::msg::SpawnReq;\nuse spt_daemon::nethost::{NetConfig, NetHost};\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_term::ScreenGrid;\n\n/// Hold the temp home for the whole test binary (the attach.rs discipline).\nstatic HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n\nfn init_home() {\n    HOME.get_or_init(|| {\n        let dir = tempfile::TempDir::new().expect(\"temp dir\");\n        std::env::set_var(\"SPT_HOME\", dir.path());\n        dir\n    });\n}\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique_name() -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-rdsp-{}-{}.sock\", std::process::id(), n)\n}\n\nfn hermetic() -> NetConfig {\n    NetConfig {\n        identity: Identity::generate(),\n        relay: RelayPolicy::Disabled,\n        discovery: LocalDiscovery::Off,\n        scope: BindScope::Loopback,\n        membership: None,\n    }\n}\n\nfn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {\n    let host = NetHost::start(hermetic()).expect(\"net host start\");\n    let broker =\n        Broker::bind_in_with_net(name, dir.join(\"effects.log\"), Some(host)).expect(\"bind broker\");\n    let serve = Arc::clone(&broker);\n    thread::spawn(move || {\n        let _ = serve.serve();\n    });\n    broker\n}\n\nfn connect_retry(name: &str) -> Brain {\n    for _ in 0..300 {\n        if let Ok(b) = Brain::cold_start(name, 1) {\n            return b;\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    panic!(\"brain could not connect\");\n}\n\nfn echo_spawn_req(endpoint: &str) -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n    #[cfg(windows)]\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n    SpawnReq {\n        program,\n        args,\n        rows: 24,\n        cols: 80,\n        endpoint: endpoint.to_string(),\n        cwd: None,\n        env: Default::default(),\n        translation_binary: None,\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…\n    }\n…\n}\n…\nfn render_until(\n…\n) -> bool {\n…\n    while !contains(buf, needle) {\n…\n        match operator.read_event().expect(\"operator read event\") {\n…\n            _ => {}\n        }\n    }\n    saw_eof\n}\n\nfn screen_of(viewport: &[u8]) -> String {\n    let mut screen = ScreenGrid::new(24, 80);\n    screen.advance(viewport);\n    String::from_utf8_lossy(&screen.render_repaint()).into_owned()\n}\n\n// [int->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] D1 — the field bug, end to end\n// through PRODUCTION rediscovery: a finished historical Attach and a current\n// active Attach on the same endpoint/origin; only the dispatcher generation\n// restarts. The historical stream must neither take nor clear the current\n// controller: post-restart input still renders (the frozen-PTY symptom is\n// the failure), the operator sees NO EOF (the replayed-EOF detach is the\n// clear), the fresh generation logs NO replay of the historical stream id,\n// and the retired row is GONE from the broker's enumeration\n// (REQ-REDISPATCH-FINISHED-RETIRE observed at the seam that matters).\n#[test]\nfn finished_historical_attach_neither_takes_nor_clears_current_controller() {\n    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(\"rd1-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    // ── The HISTORICAL attach: drive it, then detach → its exchange is over.\n    let s1 = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request s1\");\n    operator.net_stream_subscribe(s1, 0).expect(\"subscribe s1\");\n    send_attach_input(&mut operator, s1, b\"HIST-MARK\\r\", 10).expect(\"input s1\");\n    let mut dec1 = AttachDecoder::new();\n    let (mut cur1, mut view1) = (0u64, Vec::new());\n    render_until(&mut operator, s1, &mut dec1, &mut cur1, &mut view1, b\"HIST-MARK\", Duration::from_secs(20));\n    let s1_a = *peer_stream_ids(&mut probe).first().expect(\"s1 row on A\");\n    operator.net_stream_send(s1, &[], None, true).expect(\"detach s1\");\n\n    // Served-to-completion → the worker retires the row (ADR-0038 A). This is\n    // the enumeration a FRESH dispatcher would replay from — it must shrink.\n    let end = Instant::now() + Duration::from_secs(20);\n    while peer_stream_ids(&mut probe).contains(&s1_a) {\n        assert!(Instant::now() < end, \"the finished attach row never retired from the table\");\n        thread::sleep(Duration::from_millis(50));\n    }\n\n    // ── The CURRENT attach: same endpoint, same origin, actively controlled.\n    let s2 = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 3), AttachIntent::Control).expect(\"request s2\");\n    operator.net_stream_subscribe(s2, 0).expect(\"subscribe s2\");\n    send_attach_input(&mut operator, s2, b\"CUR-ONE\\r\", 11).expect(\"input s2 pre\");\n    let mut dec2 = AttachDecoder::new();\n    let (mut cur2, mut view2) = (0u64, Vec::new());\n    let eof_pre = render_until(&mut operator, s2, &mut dec2, &mut cur2, &mut view2, b\"CUR-ONE\", Duration::from_secs(20));\n    assert!(!eof_pre, \"no EOF while gen1 serves the live viewport\");\n    let s2_a = *peer_stream_ids(&mut probe).first().expect(\"s2 row on A\");\n\n    // ── The brain cycle: kill generation 1, start generation 2 — PRODUCTION\n    // rediscovery re-serves what the broker still enumerates.\n    gen1.kill();\n    let gen2 = Generation::spawn(&name_a, dir.path().join(\"gen2.log\"), &[]);\n\n    // THE regression: input typed AFTER the restart still renders — the\n    // reconstruction re-took the controller (successor re-take parity), and\n    // no historical replay stole/cleared it (pre-fix: frozen PTY here).\n    send_attach_input(&mut operator, s2, b\"CUR-TWO\\r\", 12).expect(\"input s2 post\");\n    let eof_post = render_until(&mut operator, s2, &mut dec2, &mut cur2, &mut view2, b\"CUR-TWO\", Duration::from_secs(30));\n    assert!(\n        !eof_post,\n        \"the current controller's stream saw an EOF across the restart — a replayed \\\n         historical detach cleared it (the 7.41 steal)\"\n    );\n    let painted = screen_of(&view2);\n    assert!(painted.contains(\"CUR-ONE\"), \"pre-restart output survived: {painted:?}\");\n    assert!(painted.contains(\"CUR-TWO\"), \"post-restart input/output flows: {painted:?}\");\n\n    // The fresh generation never touched the historical stream id — the\n    // DISPATCH:<id> replay-storm line (the field evidence class) must be\n    // absent for s1, while s2's re-serve is expected work, not noise.\n    let gen2_log = gen2.stderr_log();\n    assert!(\n        !gen2_log.contains(&format!(\"DISPATCH:{s1_a}:\")),\n        \"generation 2 replayed the retired historical stream {s1_a}: {gen2_log}\"\n    );\n    let _ = s2_a;\n\n    gen2.kill();\n    let _ = probe.kill_session();\n}\n\n// [int->REQ-HAZARD-REDISPATCH-CONTROL-STEAL] D1b (gate round 1) — the\n// finished-UNRETIRED orderings the Served-report path can never see: the\n// pre-existing backlog and the brain-down-detach window. s1 is attached and\n// driven under gen1, gen1 is KILLED mid-serve (no Served report), THEN the\n// operator detaches (the row finishes with no dispatcher alive; the conn\n// stays open, so the conn-close sweep does not fire either). gen2 starts and\n// serves a fresh current attach: the finished s1 row must be retired\n// UNSERVED (the family-aware terminal check — pre-fix, the pinned opener\n// made this replay PERFECTLY: seeded attach_as steals the current\n// controller, replayed input re-types, replayed EOF clears). The current\n// viewport keeps flowing across several polls, sees no EOF, and gen2's log\n// never carries a DISPATCH:<s1> line; the s1 row leaves the table without\n// ever being served (retire-on-sight drains the backlog).\n// [int->REQ-REDISPATCH-FINISHED-RETIRE]\n#[test]\nfn finished_unretired_attach_is_skipped_and_drained_not_replayed() {\n    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(\"rd1b-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 (real serve, opener pinned, input in\n    // the ring — everything a replay needs to steal).\n    let s1 = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request s1\");\n    operator.net_stream_subscribe(s1, 0).expect(\"subscribe s1\");\n    send_attach_input(&mut operator, s1, b\"HIST-MARK\\r\", 10).expect(\"input s1\");\n    let mut dec1 = AttachDecoder::new();\n    let (mut cur1, mut view1) = (0u64, Vec::new());\n    render_until(&mut operator, s1, &mut dec1, &mut cur1, &mut view1, b\"HIST-MARK\", Duration::from_secs(20));\n    let s1_a = *peer_stream_ids(&mut probe).first().expect(\"s1 row on A\");\n\n    // The brain-down-detach window: kill gen1 FIRST (no worker can report\n    // Served), THEN detach — the row finishes unretired on a live conn.\n    gen1.kill();\n    operator.net_stream_send(s1, &[], None, true).expect(\"detach s1 while brain down\");\n    {\n        let end = Instant::now() + Duration::from_secs(10);\n        loop {\n            let finished = probe\n                .net_streams()\n                .expect(\"net-streams\")\n                .streams\n                .iter()\n                .any(|s| s.stream_id == s1_a && s.finished);\n            if finished {\n                break;\n            }\n            assert!(Instant::now() < end, \"s1 never read as finished on A\");\n            thread::sleep(Duration::from_millis(50));\n        }\n    }\n\n    // gen2 + a fresh CURRENT attach. Pre-fix, gen2's replay of s1 steals and\n    // clears this controller within its first poll cycles.\n    let gen2 = Generation::spawn(&name_a, dir.path().join(\"gen2.log\"), &[]);\n    let s2 = request_attach(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 3), AttachIntent::Control).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 — the\n    // window in which a stale replay would land its steal.\n    for round in 0..3u32 {\n        let marker = format!(\"CUR-{round}\\r\");\n        send_attach_input(&mut operator, s2, marker.as_bytes(), 11 + 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!(!eof, \"round {round}: the current controller's stream saw an EOF — a stale replay cleared it\");\n        thread::sleep(Duration::from_millis(700));\n    }\n\n    // Retire-on-sight drained the finished row WITHOUT serving it.\n    {\n        let end = Instant::now() + Duration::from_secs(15);\n        while peer_stream_ids(&mut probe).contains(&s1_a) {\n            assert!(Instant::now() < end, \"the finished-unretired row never drained from the table\");\n            thread::sleep(Duration::from_millis(50));\n        }\n    }\n    let gen2_log = gen2.stderr_log();\n    assert!(\n        !gen2_log.contains(&format!(\"DISPATCH:{s1_a}:\")),\n        \"gen2 replayed the finished-unretired stream {s1_a}: {gen2_log}\"\n    );\n\n    gen2.kill();\n    let _ = probe.kill_session();\n}\n\n// [int->REQ-REDISPATCH-FINISHED-RETIRE] the GUARD leg for the family-aware\n// discriminator (gate round 1 counter-check): a request/reply stream reads\n// `finished=true` the moment its requester FINs with the request — sync's\n// `request_sync` does exactly that — so a finished row must NOT be blanket-\n// terminal. The request here arrives while NO dispatcher generation is\n// alive (the refresh window), sits finished-unserved, and the next\n// generation must still SERVE it: the pull completes with a reply. A bare\n// `s.finished` claim filter fails this test (context sync dead across every\n// brain cycle).\n#[test]\nfn finished_pending_sync_request_still_serves_across_generation_start() {\n    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…\n    assert!(\n        report.applied.is_empty(),\n        \"an unknown ref applies nothing (the completed reply itself is the assertion): {report:?}\"\n    );\n    let _ = s_a;\n\n    gen.kill();\n}\n\n// [int->REQ-STREAM-OPENER-DURABLE] D2 — the abandoned-active-stream leg: an\n// ACTIVE attach outlives its opener in the bounded ring (test-rig ring cap\n// via SPT_STREAM_RING_CHUNKS — production sizing untouched), the dispatcher\n// generation restarts, and DURABLE classification (the broker-pinned opener,\n// never ring seq 0) resumes the SAME operator stream: post-restart input\n// renders. Pre-fix this classified Unknown/Failed with the claim burned —\n// the stream was permanently abandoned.\n#[test]\nfn opener_evicted_active_stream_resumes_after_dispatcher_restart() {\n    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(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).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(&mut operator, s1, &mut dec, &mut cur, &mut view, b\"MARK-A\", Duration::from_secs(20));\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!(Instant::now() < end, \"the ring never rolled past the opener\");\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\n        .net_stream_opener(s1_a)\n        .expect(\"opener query answers\");\n    assert!(pinned.is_some(), \"the opener stays pinned across the ring roll\");\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(&mut operator, s1, &mut dec, &mut cur, &mut view, b\"MARK-B\", Duration::from_secs(30));\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.\n#[test]\nfn transient_worker_start_failure_recovers_via_claim_retry() {\n    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(\"rd3-ep\"))\n        .expect(\"spawn echo child\");\n    let a_addr = probe.net_status().expect(\"a status\").addr;\n\n    let gen1 = Generation::spawn(\n        &name_a,\n        dir.path().join(\"gen1.log\"),\n        &[(\"SPT_DISPATCH_INJECT_TRANSIENT_ONCE\", \"1\")],\n    );\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(&mut operator, conn.conn_id, sid, 0, MintedOp::new(Minter::Rc, 2), AttachIntent::Control).expect(\"request\");\n    operator.net_stream_subscribe(s1, 0).expect(\"subscribe\");\n    send_attach_input(&mut operator, s1, b\"RETRY-MARK\\r\", 10).expect(\"input\");\n\n    // Through the injected failure + the claim retry: the marker renders.\n    let mut dec = AttachDecoder::new();\n    let (mut cur, mut view) = (0u64, Vec::new());\n    render_until(&mut operator, s1, &mut dec, &mut cur, &mut view, b\"RETRY-MARK\", Duration::from_secs(30));\n\n    // The injection actually fired (this is what distinguishes \"recovered\n    // through retry\" from \"nothing ever failed\").\n    let log = gen1.stderr_log();\n    assert!(\n        log.contains(\"injected transient worker-start failure\"),\n        \"the transient-failure valve never fired — the retry path was not exercised: {log}\"\n    );\n\n    gen1.kill();\n    let _ = probe.kill_session();\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,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,null,130,null,140,null,160,null,168,null,172,null,179,null,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\redispatch.rs"}}},"isError":false,"timestamp":1784247610525}}
{"type":"message","id":"3e378fb8","parentId":"8ab88742","timestamp":"2026-07-17T00:20:10.526Z","message":{"role":"toolResult","toolCallId":"call_UusjlKbfL8EaFp3g8aUDoSTr|fc_0996e78cf50e3160016a59753991c8819985f002a9b3bdb9e7","toolName":"read","content":[{"type":"text","text":"/// `Arc<Mutex<SendHalf>>` — [`BrokerConn`] owns the half and every write is\n/// bounded + cancelable + poison-on-failure (see [`crate::conn`]). Identity\n/// matching stays `Arc::ptr_eq`.\npub(crate) type SharedSend = Arc<BrokerConn>;\n\n/// Default bounded output-log depth (chunks). Large enough that a brain restart\n/// window never evicts unreplayed output in practice; the bound is what keeps a\n/// disconnected session from buffering without limit.\nconst DEFAULT_LOG_CHUNKS: usize = 4096;\n\n/// The viewer-channel depth (frames). A bounded SPSC queue per viewer; when it\n/// OVERFLOWS the viewer is evicted — the drain `try_send`s and never blocks\n/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render\n/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n/// owns the replay batch), so a viewer attaching to a busy session is not\n/// spuriously evicted.\nconst VIEWER_CHANNEL_DEPTH: usize = 256;\n\n/// Soft cap on concurrent viewers per session (REQ-HAZARD-VIEWER-ISOLATION:\n/// bounds the writer-thread count). A `Viewer` attach beyond the cap is refused.\nconst MAX_VIEWERS: usize = 32;\n\n/// Whether a viewer's bounded-queue `try_send` result demands EVICTION\n/// (REQ-HAZARD-VIEWER-ISOLATION): a `Full` queue (the viewer can't keep up with\n/// the live stream) or a `Disconnected` one (its writer thread ended after a\n/// dead socket). The drain evicts on either and NEVER blocks on a viewer — this\n/// is the pure decision the fan-out applies under the log lock.\n// [impl->REQ-HAZARD-VIEWER-ISOLATION]\nfn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {\n    r.is_err()\n}\n\n/// One read-only viewer's isolated sink (REQ-HAZARD-VIEWER-ISOLATION). The drain\n/// thread `try_send`s output frames into `tx` (a bounded queue); a dedicated\n/// `writer` thread does the BLOCKING socket write, so a wedged viewer backs up\n/// only its own queue — never the drain. When the queue overflows the sink is\n/// dropped (the `tx` closes → the writer ends) and removed from the map.\nstruct ViewerSink {\n    /// The bounded live-frame queue to this viewer's writer thread.\n    tx: SyncSender<Envelope>,\n    /// The viewer's IPC send half — kept ONLY for identity matching on detach\n    /// (a dropped brain connection removes its viewer rows); never written here.\n    send: SharedSend,\n    /// Set TRUE when this viewer is EVICTED for overflow (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT),\n    /// distinguishing an eviction from a normal teardown (session end / conn drop).\n    /// `append` stores `true` BEFORE dropping the sink (and thus `tx`); the writer,\n    /// on its `rx.recv()` Err, reads this and — when true — emits a\n    /// [`KIND_VIEWER_EVICTED`] marker (in the writer thread, OFF the log lock) so\n    /// the serving brain skips to live instead of tearing the attach down.\n    evicted: Arc<AtomicBool>,\n    /// The writer thread handle (held for ownership; it exits when `tx` drops or\n    /// a socket write fails).\n    _writer: JoinHandle<()>,\n}\n\n/// The controller live-frame channel depth (frames). Deeper than a viewer's\n/// queue because the controller is the AUTHORITATIVE view — unlike a viewer it is\n/// NOT evicted on a SINGLE full `try_send` (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).\n/// b4: the drain does ONE non-blocking `try_send` per chunk and NEVER blocks; a\n/// merely-slow controller DROPS chunks + falls behind the ring, and only a channel\n/// that stays full for [`CONTROLLER_WRITE_DEADLINE`] (tracked via\n/// [`ControllerSink::last_ok`]) is detached. Generous so a momentary slow consumer\n/// never trips the deadline.\nconst CONTROLLER_CHANNEL_DEPTH: usize = 4096;\n\n/// How long a CONTROLLER's channel may stay FULL (no successful handoff since\n/// [`ControllerSink::last_ok`]) before the drain declares it stalled and detaches it\n/// (W1, refined by b4 / REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE). This\n/// BOUNDS the wedge: a backed-up controller falls behind for at most this long before\n/// eviction — never forever (the v0.12.x root) — and the drain NEVER blocks or sleeps\n/// (a single off-lock `try_send` per chunk; see [`OutputLog::append`]). A live but\n/// merely slow operator lands a frame before this elapses (resetting `last_ok`) and is\n/// untouched.\nconst CONTROLLER_WRITE_DEADLINE: Duration = Duration::from_secs(5);\n\n/// How long a controller's writer thread may sit BLOCKED inside a single socket\n/// write — the brain subscriber suspended / black-holed — before a take/reattach\n/// or the `KIND_SESSIONS` reap stall-evicts it (LIFECYCLE-TRUTH W2,\n/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n/// protects the fan-out against ONE per-client conn falling behind and is\n/// output-driven (the drain observes a Full channel); THIS bound is measured\n/// full-with-**zero writer progress** and covers the case the output-driven path\n/// can't — an IDLE session whose brain-subscriber conn is suspended produces no\n/// output, so `append`/`mark_controller_gone` never runs, and the stalled\n/// controller would otherwise latch `driven_by` forever (reattach refused, `--take`\n/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n/// is bursty; 15 s tolerates a normal burst-stall while still bounding the wedge.\n/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nconst BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n\n/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n/// the slot) is provable in ~1 s instead of 15 (mirrors the\n/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n/// ONLY — never relaxes an assertion.\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nfn brain_write_deadline() -> Duration {\n    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n        Ok(ms) => ms\n            .parse::<u64>()\n            .map(Duration::from_millis)\n            .unwrap_or(BRAIN_WRITE_DEADLINE),\n        Err(_) => BRAIN_WRITE_DEADLINE,\n    }\n}\n\n/// Bounded wait for a concurrent wake's session to register before a second wake for\n/// the same endpoint stops deduping and spawns itself (W4, REQ-SPAWN-COLLISION-GUARD-\n/// LIVE-DUP). A racing broker spawn registers its session in PTY-spawn time (ms), so a\n/// loser waits for the winner's session then no-op acks it (exactly one launch tree);\n/// only a spawn that OVERRAN this ceiling (presumed dead, its claim leaked) is taken\n/// over — availability over a vanishingly rare dup.\nconst WAKE_DEDUP_WAIT: Duration = Duration::from_secs(2);\n/// Poll slice while waiting on a racing wake's session to appear.\nconst WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n\n/// RAII release of a single-flight wake claim (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP):\n/// the broker holds an endpoint id in [`Broker::wake_inflight`] for the duration of a\n/// spawn so a concurrent duplicate wake stands down; the guard releases it on EVERY\n/// exit (success, an early `?` spawn/drain error, or a panic), so a failed spawn never\n/// strands the endpoint permanently claimed.\n…\n    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\n…\n        let result = if let Some(c) = self.controller.as_mut() {\n            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {\n…\n                Err(TrySendError::Full(_)) => {\n                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {\n…\n                    } else {\n                        None // drop this chunk; controller falls behind the ring\n                    }\n                }\n                Err(TrySendError::Disconnected(_)) => Some(epoch),\n            }\n        } else {\n            None\n        };\n        // Advance the render grid AFTER the fan-out — it must never sit between the\n        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\n        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\n        // snapshots it under the same short log lock only when an attach requests it.\n        self.grid.advance(bytes);\n        result\n    }\n\n    /// The current controller's identity (`Some(node)` remote / `None` local),\n    /// or `None` when undriven. The `by`-keyed ownership logic in\n    /// [`resolve_subscribe`] reads this.\n    fn controller_by(&self) -> Option<&str> {\n        self.controller.as_ref().and_then(|c| c.by.as_deref())\n    }\n\n    /// Whether ANY controller currently drives this session (the `controlled`\n    /// any-controller truth — REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). Snapshotted under\n    /// the log lock, then converged onto the perch OFF the lock.\n    fn has_controller(&self) -> bool {\n        self.controller.is_some()\n    }\n\n    /// The live viewer count (the `viewer_count` truth). Snapshotted under the log lock\n    /// for the convergence re-assert.\n    fn live_viewer_count(&self) -> u32 {\n        self.viewers.len() as u32\n    }\n\n    /// Become (or silently re-take) the interactive controller from `from_seq`.\n    ///\n    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller\n    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\n    /// inline under the log lock. The writer thread advances the authoritative\n    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,\n    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\n    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\n    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\n    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\n    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\n    /// single live writer per connection and a monotonic on-wire seq stream,\n    /// [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]). Stamps `driven_by` to the new\n    /// controller's identity.\n    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {\n        // Drop the prior controller sink first (its writer's live loop ends when\n        // tx drops), and bump the generation so (a) an in-flight deadline-evict\n        // for the old controller can't unseat this one and (b) a prior writer\n        // still flushing its initial batch returns before its next frame.\n        // Attribution (F-039 legs b+d): the replaced conn's record names its\n        // successor, and the new conn accretes the controller role fact.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        if let Some(old) = self.controller.take() {\n            old.send.lifecycle_event(\n                \"controller-replaced\",\n                &format!(\"session={} new_conn={}\", self.session_id, sub.id()),\n            );\n        }\n        sub.describe(&format!(\n            \"controller session={} endpoint={} by={}\",\n            self.session_id,\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint },\n            by.as_deref().unwrap_or(\"local\")\n        ));\n        sub.lifecycle_event(\n            \"controller-attach\",\n            &format!(\"session={} from_seq={from_seq}\", self.session_id),\n        );\n        self.controller = None;\n        let epoch = self.controller_epoch.fetch_add(1, Ordering::AcqRel) + 1;\n        // #6 (ADR-0031): a COLD attach's initial batch is a single synthesized clean\n        // repaint of the CURRENT SCREEN (at pseudo-seq `next_seq - 1`) instead of the\n        // raw ring — replaying an alt-screen TUI's raw ring into a fresh terminal is\n        // exactly the corruption #6/#7/#8 report. A resume-from-floor (`from_seq > 0`)\n        // keeps the raw-ring exactly-once re-fetch untouched (B2). Either way the\n        // batch is owned by the writer thread (never pushed through the live channel —\n        // it must not block the drain); live frames stream raw + in-order afterwards.\n        let initial: Vec<(u64, Vec<u8>)> = match self.repaint_initial(from_seq) {\n            Some(repaint) => vec![repaint],\n            None => self\n                .ring\n                .iter()\n                .filter(|(s, _)| *s >= from_seq)\n                .cloned()\n                .collect(),\n        };\n        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n        let writer_send = Arc::clone(&sub);\n        let dt = Arc::clone(&self.delivered_through);\n        let epoch_gate = Arc::clone(&self.controller_epoch);\n        let sid = self.session_id;\n        // W2 decouple: the writer publishes its in-flight-write window here so a\n        // stall-evict can distinguish a wedged writer from an idle one.\n        let write_blocked_since = Arc::new(Mutex::new(None));\n        let writer_blocked = Arc::clone(&write_blocked_since);\n        let writer = thread::spawn(move || {\n            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\n        });\n        self.controller = Some(ControllerSink {\n            tx,\n            send: sub,\n            by,\n            last_ok: Instant::now(),\n            write_blocked_since,\n            _writer: writer,\n        });\n        self.stamp_driven_by();\n    }\n\n    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared\n    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per\n    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its\n    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an\n    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\n    fn clear_controller(&mut self) {\n        if self.controller.take().is_some() {\n            self.stamp_driven_by();\n        }\n    }\n\n    /// B-2 (REQ-CONTROLLER-LIVENESS-REAP): lazily drop a controller whose WRITER\n    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\n    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\n    /// writer exits when a socket write fails (a severed / keepalive-killed controller\n    /// conn) or its `tx` drops; while it is parked on `rx.recv()` (a LIVE but idle\n    /// controller) it is NOT finished, so a live controller is never touched (the\n    /// no-false-reap guard). Reaping here makes the broker report honest truth so the\n    /// EXISTING [`converge_perch_stamps`] clears the now-stale info.json\n    /// `driven_by`/`controlled` stamp — no new detach path, no new query.\n    ///\n    /// LOCK-SAFE: this runs UNDER the log lock in the snapshot closure, so it must do\n    /// NO file I/O (the lock-across-effect discipline, KH 7.12/5.16). It drops the\n    /// sink in-memory ONLY (like [`become_controller`]'s prior-drop) and does NOT\n    /// [`stamp_driven_by`] — the OFF-lock `converge_perch_stamps` writes the honest\n    /// stamp. Returns true iff it reaped.\n    ///\n    /// SCOPE (doyle B-2 reduction): catches a writer that FAILED A WRITE (output was\n    /// flowing when the conn severed) or whose detach was dropped by a prior wedge\n    /// (the B-1 poison class). A TRULY IDLE severed controller (writer parked on recv,\n    /// no output, conn not yet EOF'd) stays `is_finished() == false` and converges\n    /// only once output resumes or the conn EOFs (→ `handle_conn` detach) — that\n    /// harder active-probe case is the reserved [`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`],\n    /// deliberately NOT built here. KH 7.15 held by construction: only ever CLEARS,\n    /// never latches `driven_by`.\n    // [impl->REQ-CONTROLLER-LIVENESS-REAP]\n    fn reap_dead_controller(&mut self) -> bool {\n        let dead = self\n            .controller\n            .as_ref()\n            .is_some_and(|c| c._writer.is_finished());\n        if dead {\n            // In-memory drop only (ends the — already-exited — writer); NO stamp here\n            // (info.json I/O under the log lock is the KH 7.12 hazard). converge does it.\n            self.controller = None;\n            return true;\n        }\n        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller whose\n        // writer is BLOCKED (not exited) on a suspended brain conn past\n        // BRAIN_WRITE_DEADLINE is also reaped here, so the honest `KIND_SESSIONS`\n        // truth + the off-lock converge_perch_stamps release its stale `driven_by`\n        // stamp even when nobody take/reattaches. Deferred stamp: the reap runs in\n        // the multi-session snapshot closure (no I/O under the shared sessions lock).\n        self.stall_evict_controller(Instant::now(), StampMode::Deferred)\n    }\n\n    /// Deadline-evict path (W1): the drain observed a fully-stalled controller and\n    /// timed out its `send_timeout`. Detach it — but ONLY if it is still the same\n    /// controller generation the drain saw (`epoch`), so a NEWER controller that\n    /// attached during the stall is never unseated.\n    fn mark_controller_gone(&mut self, epoch: u64) {\n        if self.controller_epoch.load(Ordering::Acquire) == epoch {\n            self.clear_controller();\n        }\n    }\n\n    /// Whether the current controller's writer is WEDGED — blocked inside a single\n    /// socket write for at least `deadline` (the brain-subscriber conn suspended /\n    /// black-holed). An IDLE controller (writer parked on `rx.recv()`, nothing to\n    /// write) reads NOT stalled: its [`ControllerSink::write_blocked_since`] is `None`.\n    /// Pure over the injected `now` + `deadline` so the unit tests it with no thread.\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn controller_write_stalled(&self, now: Instant, deadline: Duration) -> bool {\n        self.controller.as_ref().is_some_and(|c| {\n            c.write_blocked_since\n                .lock()\n                .ok()\n                .and_then(|g| *g)\n                .is_some_and(|since| now.saturating_duration_since(since) >= deadline)\n        })\n    }\n\n    /// 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    /// under the log lock via [`become_controller`]) or is deferred to the off-lock\n    /// `converge_perch_stamps` (the multi-session reap closure, KH 7.12 no-I/O rule).\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn stall_evict_controller(&mut self, now: Instant, stamp: StampMode) -> bool {\n        let deadline = brain_write_deadline();\n        if !self.controller_write_stalled(now, deadline) {\n            return false;\n        }\n        // Attribution (F-039 leg b): the evicted controller's stable conn id +\n        // stamps ride the record. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        let conn = self.controller.as_ref().map(|c| c.send.id()).unwrap_or(0);\n        eprintln!(\n            \"BRAIN_SUBSCRIBER_STALL_EVICT:{} conn={conn} {} — controller writer blocked > {}ms \\\n             on a suspended/black-holed brain conn; detached, driven_by released \\\n             [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\n            self.session_id,\n            crate::conn::log_stamp(),\n            deadline.as_millis()\n        );\n        record_stall_evict();\n        match stamp {\n            // Inline: drop the sink AND re-stamp the perch (become_controller's\n            // sibling — the take/reattach path is already I/O-under-log-lock).\n            StampMode::Inline => self.clear_controller(),\n            // Deferred: in-memory drop ONLY (the reap closure forbids I/O under the\n            // shared sessions lock); converge_perch_stamps writes the release off-lock.\n            StampMode::Deferred => self.controller = None,\n        }\n        true\n    }\n\n    /// Add a read-only viewer: snapshot the ring from `from_seq` into the writer\n    /// thread (NOT through the bounded live queue — replay must not trip the\n    /// overflow eviction), seed it the current size frame first, then run the\n    /// live queue. The drain thread fans live frames in via `try_send`.\n    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {\n        let vid = self.next_viewer_id;\n        self.next_viewer_id += 1;\n        // Attribution (F-039 legs b+d): viewer role fact + attach record.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        sub.describe(&format!(\n            \"viewer session={} endpoint={} vid={vid}\",\n            self.session_id,\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint }\n        ));\n        sub.lifecycle_event(\n            \"viewer-attach\",\n            &format!(\"session={} vid={vid} from_seq={from_seq}\", self.session_id),\n        );\n        // The size frame + the initial batch (owned by the thread, not the bounded\n        // queue). A COLD viewer attach (`from_seq == 0`) gets the #6 clean repaint;\n        // a skip-to-live / snap re-attach (`from_seq > 0`, incl. `u64::MAX`) keeps\n        // the raw-ring slice (`s >= from_seq`) — empty for skip-to-live, as before.\n        let mut initial: Vec<Envelope> =\n            vec![size_envelope(self.session_id, self.size.0, self.size.1)];\n        match self.repaint_initial(from_seq) {\n            Some((seq, bytes)) => initial.push(output_envelope(self.session_id, seq, &bytes)),\n            None => initial.extend(\n                self.ring\n                    .iter()\n                    .filter(|(s, _)| *s >= from_seq)\n                    .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),\n            ),\n        }\n        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);\n        let writer_send = Arc::clone(&sub);\n        let evicted = Arc::new(AtomicBool::new(false));\n        let writer_evicted = Arc::clone(&evicted);\n        let session_id = self.session_id;\n        let writer =\n            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));\n        self.viewers.insert(\n            vid,\n            ViewerSink {\n                tx,\n                send: sub,\n                evicted,\n                _writer: writer,\n            },\n        );\n    }\n\n    /// Resolve a controller/viewer subscribe under the W2.5 model\n    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs\n    /// the controller take / viewer add / loud displace as a side effect. See the\n    /// identity matrix in the body — the `by`-keyed logic is what makes a\n    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.\n    fn resolve_subscribe(\n        &mut self,\n        sub: SharedSend,\n        from_seq: u64,\n        intent: AttachIntent,\n        by: Option<String>,\n    ) -> SubscribeOutcome {\n        if intent == AttachIntent::Viewer {\n            if self.viewers.len() >= MAX_VIEWERS {\n                // Soft cap reached — refuse the viewer (bounds thread count).\n                return SubscribeOutcome::BusyControlled {\n                    by: \"viewer cap reached\".to_string(),\n                };\n            }\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        // Control or Take.\n        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller WEDGED\n        // on a suspended/black-holed brain conn past BRAIN_WRITE_DEADLINE is\n        // stall-evicted HERE — the take/reattach self-heals against broker truth\n        // without waiting for new output (the output-driven mark_controller_gone\n        // never fires on an idle session). Inline stamp: this path already writes the\n        // perch under the log lock via become_controller. After the evict the slot\n        // reads free and the normal free/take logic below runs.\n        self.stall_evict_controller(Instant::now(), StampMode::Inline);\n        if self.controller.is_none() {\n            // Free endpoint → become controller (nothing to displace).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        let same_identity = self.controller_by() == by.as_deref();\n        if same_identity {\n            // Same operator (or both local) re-serving — silent re-take. This is\n            // the gate-#7 path: a successor re-taking the slot after a brain\n            // restart NEVER fires Displaced.\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        if by.is_none() {\n            // The LOCAL brain re-attaching while a REMOTE controls — never steal;\n            // fall back to a silent viewer (gate #7 for the local-resume path).\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        if self.controller_by().is_none() {\n            // A remote takes an UNDRIVEN session (only the local spawner held the\n            // slot) — silent control, no loud displace (there is no remote driver\n            // to notify).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        // A DIFFERENT remote controls it.\n        match intent {\n            AttachIntent::Take => {\n                // The ONLY loud-displace path: notify the incumbent, then take.\n                // W1: route the Displaced notice through the OLD controller's\n                // writer thread (best-effort, off the dispatch thread) — the\n                // incumbent we're kicking may be exactly the wedged one, so an\n                // inline write here would block the new attacher's dispatch.\n                if let Some(old) = &self.controller {\n                    let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(\"\"));\n                    let _ = old.tx.try_send(CtrlMsg::Control(frame));\n                }\n                self.become_controller(sub, by, from_seq);\n                SubscribeOutcome::TookControl\n            }\n            _ => SubscribeOutcome::BusyControlled {\n                by: self.controller_by().unwrap_or_default().to_string(),\n            },\n        }\n    }\n…\nfn viewer_writer(\n…\n) {\n…\n    // before dropping the sink), emit a KIND_VIEWER_EVICTED marker — DISTINCT from\n    // a session-exit EOF — so the serving brain skips to live (re-subscribes from\n    // the ring floor) instead of dying silently. A NORMAL teardown (session end /\n    // conn drop) leaves the flag false → no marker, exit quietly. This write is in\n    // the dedicated writer thread, OFF the log lock / off the drain (the W1/b4\n    // isolation is preserved). REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT.\n    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n    if evicted.load(Ordering::Acquire) {\n        let marker = evicted_envelope(session_id);\n        let _ = send.write(&marker);\n    }\n}\n\n/// Round-4 observability bridge: a controller writer whose bounded conn write\n/// was DEADLINE-retired (`TimedOut` — the conn watchdog aborted a wedged\n/// write, or a gate-wait behind one exhausted the bound) IS the\n/// brain-subscriber stall-evict fact the W2 tally counts. Pre-round-4 that\n/// wedge aged until a logical sampler (`stall_evict_controller`) observed it;\n/// the physical bound now fires FIRST, so the tally is recorded here — same\n/// event class, same `daemon status` surface. An ORGANIC write failure (peer\n/// death, any non-TimedOut error) stays uncounted, exactly as before.\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nfn note_controller_write_retired(sid: u64, conn: u64, e: &io::Error) {\n    if e.kind() != io::ErrorKind::TimedOut {\n        return;\n    }\n    // conn id + stamps (F-039 leg b). [impl->REQ-CONN-POISON-ATTRIBUTION]\n    eprintln!(\n        \"BRAIN_SUBSCRIBER_STALL_EVICT:{sid} conn={conn} {} — controller conn write \\\n         exceeded its bound; physical conn poisoned + retired (writer released, EOF \\\n         cleanup follows) [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\n        crate::conn::log_stamp()\n    );\n    record_stall_evict();\n}\n\n/// A message to the CONTROLLER writer thread (W1). `Output` carries a sequenced\n/// chunk and advances the authoritative `delivered_through` on a successful\n/// write; `Control` is an out-of-band notice (Displaced, …) that is written but\n/// does NOT move the resume cursor.\nenum CtrlMsg {\n    Output(u64, Envelope),\n    Control(Envelope),\n}\n\n/// Advance the shared `delivered_through` cursor to `seq + 1`, monotonically\n/// (D4-1, via [`advanced_cursor`]) and CAS-safe against a brief two-writer\n/// overlap during a controller displace.\nfn advance_delivered(dt: &AtomicU64, seq: u64) {\n    let mut cur = dt.load(Ordering::Acquire);\n    loop {\n        let next = advanced_cursor(cur, seq, true);\n        if next == cur {\n            return;\n        }\n        match dt.compare_exchange_weak(cur, next, Ordering::AcqRel, Ordering::Acquire) {\n            Ok(_) => return,\n            Err(actual) => cur = actual,\n        }\n    }\n}\n\n/// Advance the controller resume cursor CONTIGUOUSLY (b4 / the (a) B2-gate): move to\n/// `seq + 1` ONLY when the cursor is EXACTLY `seq` (the next expected seq). A gap\n/// (`seq > cursor` — a frame the drain dropped while the controller channel was Full,\n/// the b4 backpressure) FREEZES the cursor at last-contiguous, so a re-attaching\n/// brain's `resume_seq` never skips a dropped chunk (a high-watermark jump PAST the\n/// gap would be a not-exactly-once / non-gapless resume = a B2 violation). An\n/// already-delivered `seq` (`seq < cursor`, a rewind re-send) is a no-op. A single CAS,\n/// no loop: it advances by exactly one, or not at all. In the NO-DROP path (every\n/// live frame is contiguous) this is identical to [`advance_delivered`].\n// [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\nfn contiguous_advance(dt: &AtomicU64, seq: u64) {\n    let _ = dt.compare_exchange(seq, seq + 1, Ordering::AcqRel, Ordering::Acquire);\n}\n\n/// The interactive CONTROLLER's dedicated writer thread (W1 /\n/// REQ-HAZARD-INJECT-CONTROL-COEXIST) — the authoritative analog of\n/// [`viewer_writer`]. Write the `initial` ring-replay batch (advancing the resume\n/// cursor on each success), then drain live [`CtrlMsg`]s with a BLOCKING socket\n/// write each — so a backed-up controller blocks ONLY this thread, never the\n/// session drain (which used to write the controller inline under the log lock).\n/// A failed write (dead operator) ends the thread; the drain's next bounded\n/// send then sees `Disconnected` and detaches the row. `Output` advances\n/// `delivered_through`; `Control` (Displaced/…) does not.\n// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\nfn controller_writer(\n    sid: u64,\n    send: SharedSend,\n    initial: Vec<(u64, Vec<u8>)>,\n    rx: Receiver<CtrlMsg>,\n    delivered_through: Arc<AtomicU64>,\n    epoch: u64,\n    controller_epoch: Arc<AtomicU64>,\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\n) {\n    // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): mark the exact window a\n    // BLOCKING socket write is in flight, so a reader can tell a WEDGED writer (brain\n    // conn suspended) from a merely idle one and stall-evict it. Set BEFORE the write,\n    // cleared AFTER — the mutex is never held across `write_frame` (that would just\n    // move the wedge onto the mutex).\n    let mark_blocked = |on: bool| {\n        if let Ok(mut g) = write_blocked_since.lock() {\n            *g = if on { Some(Instant::now()) } else { None };\n        }\n    };\n    for (seq, bytes) in &initial {\n        let frame = output_envelope(sid, *seq, bytes);\n        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\n        // epoch past ours) must write NOTHING further — otherwise its owned\n        // initial batch races the new writer on the same socket and reorders the\n        // seq stream. The gate closure runs AFTER the conn's serialized write\n        // gate is acquired and BEFORE any byte is written — the same\n        // serialization point the old epoch-check-under-`send.lock()` had: both\n        // writers serialize on the one conn gate, and `become_controller` bumps\n        // the epoch BEFORE spawning the new writer, so whichever superseded\n        // writer next holds the gate observes the bump and bails before\n        // committing — no check-then-block-then-write window. (A frame the\n        // stale writer committed BEFORE the bump is irreducible at the broker;\n        // fix #1 removes the contradictory double-take that produced it.)\n        // Round-4: the write itself is bounded + cancelable — a non-draining\n        // consumer poisons + retires the physical conn at the deadline instead\n        // of wedging this thread in the OS write forever.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        mark_blocked(true);\n        let outcome =\n            send.write_gated(&frame, || controller_epoch.load(Ordering::Acquire) == epoch);\n        mark_blocked(false);\n        match outcome {\n            Ok(ConnWrite::Done) => {}\n            Ok(ConnWrite::Superseded) => {\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=superseded\"),\n                );\n                return;\n            }\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n                );\n                return;\n            }\n        }\n        advance_delivered(&delivered_through, *seq);\n    }\n    while let Ok(msg) = rx.recv() {\n        let (frame, seq) = match &msg {\n            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),\n            CtrlMsg::Control(frame) => (frame, None),\n        };\n        // NO epoch gate on the live path (P1c): new output only ever flows to the\n        // CURRENT controller's channel (the drain clones `self.controller.tx`), so\n        // a superseded writer's channel holds only its pre-supersede output\n        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL\n        // control frames — notably the `Displaced` kick the displaced controller\n        // MUST still receive. The loop ends naturally when `tx` drops. Gating\n        // output here would suppress that `Displaced` (the cv-matrix loud-take\n        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.\n        mark_blocked(true);\n        let res = send.write(frame);\n        mark_blocked(false);\n        match res {\n            Ok(_) => {}\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n                );\n                return;\n            }\n        }\n        if let Some(seq) = seq {\n            // (a) CONTIGUOUS cursor (b4 / B2 gapless-resume — doyle's gate): with the\n            // drop-on-full handoff in `append`, the controller's live channel can SKIP\n            // a seq (dropped while Full). The frame still went to the socket (the live\n            // operator sees it), but the durable resume cursor must NOT jump past the\n            // gap — see [`contiguous_advance`].\n            contiguous_advance(&delivered_through, seq);\n        }\n    }\n…\n}\n…\n    /// The output-log high-water (`next_seq`) of a hosted session, or `None` if\n    /// absent. It advances every time the child emits output — the\n    /// **functional**-survival seam for the D7-1 process-level E2E (doyle F1):\n    /// after a brain-PROCESS swap, writing input to the broker-held PTY child and\n    /// watching THIS advance proves the path is alive end-to-end (input → child →\n    /// echo → broker ring), not merely that the handle is held. A read-only direct\n    /// peek, like [`session_pid`](Self::session_pid).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\n        self.sessions\n            .lock()\n            .unwrap()\n            .get(&id)\n            .map(|h| recover_log(&h.log).next_seq)\n    }\n\n    /// Drive one brain connection: handshake, then dispatch its command frames\n    /// until it disconnects. On disconnect, detach this connection from any\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\n    /// restart must not disturb the hosted children).\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n        struct HandlerTally<'a>(&'a AtomicUsize);\n        impl Drop for HandlerTally<'_> {\n            fn drop(&mut self) {\n                self.0.fetch_sub(1, Ordering::AcqRel);\n            }\n        }\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\n        let _tally = HandlerTally(&self.conn_handlers);\n\n        let (mut recv, send) = conn.split();\n        // Round-4: the send half is owned by the bounded/cancelable conn\n        // primitive from birth — every write on this physical connection is\n        // deadline-bounded, and a wedge poisons + retires the conn.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        send.describe(\"role=brain\");\n\n        // Version handshake: this connection must be a brain (2.3).\n        recv_hello(&mut recv, Role::Brain)?;\n\n        let mut my_subs: Vec<u64> = Vec::new();\n        let mut my_stream_subs: Vec<u64> = Vec::new();\n        let mut my_presence_sub = false;\n\n        loop {\n            let env = match read_frame(&mut recv) {\n                Ok(e) => e,\n                // Any read failure (EOF included) ends the connection.\n                Err(_) => break,\n            };\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_UNSUBSCRIBE => {\n…\n                }\n…\n            }\n        }\n…\n    }\n…\n    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n…\n        // The output log, with the spawning connection pre-attached as the\n        // controller (identity `None` = LOCAL, not a remote driver) so no chunk\n        // is missed before an explicit subscribe — and so a remote attach to a\n        // spawned-but-undriven session takes control silently (no loud displace\n        // of the local-spawner placeholder; REQ-RCVIEW-1).\n        let log = Arc::new(Mutex::new(OutputLog::new(\n            id,\n            self.log_cap_chunks,\n            req.endpoint.clone(),\n            (req.rows, req.cols),\n        )));\n        log.lock()\n            .unwrap()\n            .become_controller(Arc::clone(send), None, 0);\n\n        // Output drain → append under the log lock (fast: ring + viewer fan-out),\n        // then deliver to the controller OFF the lock with a bounded deadline\n        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no\n        // longer inline under the log lock, so a backed-up controller can never\n        // wedge the session; a fully-stalled controller is detached after the\n        // deadline (bounded wedge, never permanent).\n        let log_drain = Arc::clone(&log);\n        let drain = session\n            .drain(move |chunk| {\n                // append() does the controller handoff inline now (one non-blocking\n                // try_send — b4) and returns Some(epoch) ONLY when the controller is\n                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\n                // the first lock (a fresh lock, no self-deadlock). The drain never\n                // sleeps here, so a slow controller can't throttle the viewer fan-out.\n                let evict = recover_log(&log_drain).append(chunk);\n                if let Some(epoch) = evict {\n                    recover_log(&log_drain).mark_controller_gone(epoch);\n                }\n            })\n            .map_err(|e| format!(\"drain failed: {e}\"))?;\n\n        // Exit waiter → one `exit` frame when the child is reaped, then the dead\n        // session is removed from the table. Runs on its own thread; `wait`\n        // holds the child lock but `kill` uses a split killer.\n        let sessions_exit = Arc::clone(&self.sessions);\n        let log_exit = Arc::clone(&log);\n        let waiter = Arc::clone(&session);\n        thread::spawn(move || {\n            if let Ok(status) = waiter.wait() {\n                let ev = ExitEvent {\n                    session_id: id,\n                    code: Some(status.exit_code() as i32),\n                };\n                let frame = Envelope::new(\n                    KIND_EXIT,\n                    serde_json::to_value(ev).expect(\"ExitEvent serializes\"),\n                );\n                // Send to every attached sink — controller + all viewers (under\n                // the log lock).\n                let sinks = recover_log(&log_exit).all_sinks();\n                for sink in sinks {\n                    // Bounded conn write (round-4): a wedged sink can no longer\n                    // park the exit-waiter thread indefinitely.\n                    let _ = sink.write(&frame);\n                }\n            }\n            // Reap-time unlatch (REQ-HAZARD-CONTROL-STAMP-LIFETIME): the child\n            // died, not the controller conn, so no detach path clears the perch\n            // — do it here so ONLINE+CONTROLLED / driven_by / viewer_count cannot\n            // outlive the session in info.json (or gossip a stale controller).\n            // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n            recover_log(&log_exit).stamp_reaped();\n            // Reap the dead session from the table.\n            recover(&sessions_exit).remove(&id);\n        });\n\n        // Layer C: if the spawn carried a translation binary, bring it up with the\n        // session — spawn the pure stdin→stdout filter + the inject worker that\n        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to SPOOLING\n        // (logged, `None` → idle inbound spools, never raw-injected) rather than\n        // aborting the session — a missing binary costs idle delivery, never the\n        // operator their agent.\n        // C-1: the shared bounded-respawn give-up counter (starts at 0; the worker\n        // resets it on a healthy commit, the dispatch respawn path increments it).\n        let translation_respawns = Arc::new(AtomicU32::new(0));\n        let translation = req\n            .translation_binary\n            .as_deref()\n            .and_then(|argv| {\n                build_translation(\n                    argv,\n                    &req.endpoint,\n                    &input,\n                    Arc::clone(&translation_respawns),\n                    &log,\n                )\n            });\n\n        recover(&self.sessions).insert(\n            id,\n            HostedSession {\n…\n            },\n        );\n…\n        let resp = Spawned {\n            session_id: id,\n            pid,\n        };\n        let frame = Envelope::new(\n            KIND_SPAWNED,\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\n        );\n        send_frame(send, &frame);\n        Ok(id)\n    }\n\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\n    /// self-kick guard.\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n        let req: SubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\n        // Clone the log Arc out from under the sessions lock, then resolve under\n        // the log lock — never hold both locks at once.\n        //\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\n        // the per-session exit-waiter holds for the WHOLE life of a live child\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\n        // post-removal subscribe gets a prompt \"no such session\" error — both\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\n        // the rest.\n        let log = {\n            let sessions = recover(&self.sessions);\n            let h = sessions\n                .get(&req.session_id)\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\n            Arc::clone(&h.log)\n        };\n        let outcome = recover_log(&log).resolve_subscribe(\n            Arc::clone(send),\n            req.from_seq,\n            req.intent,\n            req.by,\n        );\n        send_frame(send, &subscribed_envelope(req.session_id, outcome));\n        Ok(req.session_id)\n    }\n\n    /// Release this connection's controller/viewer role on a session\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\n    /// a no-op if a newer controller already took over, or the session is gone.\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\n        let log = {\n            let sessions = recover(&self.sessions);\n            match sessions.get(&req.session_id) {\n                Some(h) => Arc::clone(&h.log),\n                None => {\n                    eprintln!(\"DBG unsubscribe: no session {}\", req.session_id);\n                    return Ok(());\n                }\n            }\n        };\n        recover_log(&log).detach_if(send);\n        Ok(())\n    }\n\n    /// Inject input bytes into a session's PTY.\n    ///"}],"details":{"displayContent":{"text":"/// `Arc<Mutex<SendHalf>>` — [`BrokerConn`] owns the half and every write is\n/// bounded + cancelable + poison-on-failure (see [`crate::conn`]). Identity\n/// matching stays `Arc::ptr_eq`.\npub(crate) type SharedSend = Arc<BrokerConn>;\n\n/// Default bounded output-log depth (chunks). Large enough that a brain restart\n/// window never evicts unreplayed output in practice; the bound is what keeps a\n/// disconnected session from buffering without limit.\nconst DEFAULT_LOG_CHUNKS: usize = 4096;\n\n/// The viewer-channel depth (frames). A bounded SPSC queue per viewer; when it\n/// OVERFLOWS the viewer is evicted — the drain `try_send`s and never blocks\n/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render\n/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n/// owns the replay batch), so a viewer attaching to a busy session is not\n/// spuriously evicted.\nconst VIEWER_CHANNEL_DEPTH: usize = 256;\n\n/// Soft cap on concurrent viewers per session (REQ-HAZARD-VIEWER-ISOLATION:\n/// bounds the writer-thread count). A `Viewer` attach beyond the cap is refused.\nconst MAX_VIEWERS: usize = 32;\n\n/// Whether a viewer's bounded-queue `try_send` result demands EVICTION\n/// (REQ-HAZARD-VIEWER-ISOLATION): a `Full` queue (the viewer can't keep up with\n/// the live stream) or a `Disconnected` one (its writer thread ended after a\n/// dead socket). The drain evicts on either and NEVER blocks on a viewer — this\n/// is the pure decision the fan-out applies under the log lock.\n// [impl->REQ-HAZARD-VIEWER-ISOLATION]\nfn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {\n    r.is_err()\n}\n\n/// One read-only viewer's isolated sink (REQ-HAZARD-VIEWER-ISOLATION). The drain\n/// thread `try_send`s output frames into `tx` (a bounded queue); a dedicated\n/// `writer` thread does the BLOCKING socket write, so a wedged viewer backs up\n/// only its own queue — never the drain. When the queue overflows the sink is\n/// dropped (the `tx` closes → the writer ends) and removed from the map.\nstruct ViewerSink {\n    /// The bounded live-frame queue to this viewer's writer thread.\n    tx: SyncSender<Envelope>,\n    /// The viewer's IPC send half — kept ONLY for identity matching on detach\n    /// (a dropped brain connection removes its viewer rows); never written here.\n    send: SharedSend,\n    /// Set TRUE when this viewer is EVICTED for overflow (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT),\n    /// distinguishing an eviction from a normal teardown (session end / conn drop).\n    /// `append` stores `true` BEFORE dropping the sink (and thus `tx`); the writer,\n    /// on its `rx.recv()` Err, reads this and — when true — emits a\n    /// [`KIND_VIEWER_EVICTED`] marker (in the writer thread, OFF the log lock) so\n    /// the serving brain skips to live instead of tearing the attach down.\n    evicted: Arc<AtomicBool>,\n    /// The writer thread handle (held for ownership; it exits when `tx` drops or\n    /// a socket write fails).\n    _writer: JoinHandle<()>,\n}\n\n/// The controller live-frame channel depth (frames). Deeper than a viewer's\n/// queue because the controller is the AUTHORITATIVE view — unlike a viewer it is\n/// NOT evicted on a SINGLE full `try_send` (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).\n/// b4: the drain does ONE non-blocking `try_send` per chunk and NEVER blocks; a\n/// merely-slow controller DROPS chunks + falls behind the ring, and only a channel\n/// that stays full for [`CONTROLLER_WRITE_DEADLINE`] (tracked via\n/// [`ControllerSink::last_ok`]) is detached. Generous so a momentary slow consumer\n/// never trips the deadline.\nconst CONTROLLER_CHANNEL_DEPTH: usize = 4096;\n\n/// How long a CONTROLLER's channel may stay FULL (no successful handoff since\n/// [`ControllerSink::last_ok`]) before the drain declares it stalled and detaches it\n/// (W1, refined by b4 / REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE). This\n/// BOUNDS the wedge: a backed-up controller falls behind for at most this long before\n/// eviction — never forever (the v0.12.x root) — and the drain NEVER blocks or sleeps\n/// (a single off-lock `try_send` per chunk; see [`OutputLog::append`]). A live but\n/// merely slow operator lands a frame before this elapses (resetting `last_ok`) and is\n/// untouched.\nconst CONTROLLER_WRITE_DEADLINE: Duration = Duration::from_secs(5);\n\n/// How long a controller's writer thread may sit BLOCKED inside a single socket\n/// write — the brain subscriber suspended / black-holed — before a take/reattach\n/// or the `KIND_SESSIONS` reap stall-evicts it (LIFECYCLE-TRUTH W2,\n/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n/// protects the fan-out against ONE per-client conn falling behind and is\n/// output-driven (the drain observes a Full channel); THIS bound is measured\n/// full-with-**zero writer progress** and covers the case the output-driven path\n/// can't — an IDLE session whose brain-subscriber conn is suspended produces no\n/// output, so `append`/`mark_controller_gone` never runs, and the stalled\n/// controller would otherwise latch `driven_by` forever (reattach refused, `--take`\n/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n/// is bursty; 15 s tolerates a normal burst-stall while still bounding the wedge.\n/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nconst BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n\n/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n/// the slot) is provable in ~1 s instead of 15 (mirrors the\n/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n/// ONLY — never relaxes an assertion.\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nfn brain_write_deadline() -> Duration {\n    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n        Ok(ms) => ms\n            .parse::<u64>()\n            .map(Duration::from_millis)\n            .unwrap_or(BRAIN_WRITE_DEADLINE),\n        Err(_) => BRAIN_WRITE_DEADLINE,\n    }\n}\n\n/// Bounded wait for a concurrent wake's session to register before a second wake for\n/// the same endpoint stops deduping and spawns itself (W4, REQ-SPAWN-COLLISION-GUARD-\n/// LIVE-DUP). A racing broker spawn registers its session in PTY-spawn time (ms), so a\n/// loser waits for the winner's session then no-op acks it (exactly one launch tree);\n/// only a spawn that OVERRAN this ceiling (presumed dead, its claim leaked) is taken\n/// over — availability over a vanishingly rare dup.\nconst WAKE_DEDUP_WAIT: Duration = Duration::from_secs(2);\n/// Poll slice while waiting on a racing wake's session to appear.\nconst WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n\n/// RAII release of a single-flight wake claim (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP):\n/// the broker holds an endpoint id in [`Broker::wake_inflight`] for the duration of a\n/// spawn so a concurrent duplicate wake stands down; the guard releases it on EVERY\n/// exit (success, an early `?` spawn/drain error, or a panic), so a failed spawn never\n/// strands the endpoint permanently claimed.\n…\n    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\n…\n        let result = if let Some(c) = self.controller.as_mut() {\n            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {\n…\n                Err(TrySendError::Full(_)) => {\n                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {\n…\n                    } else {\n                        None // drop this chunk; controller falls behind the ring\n                    }\n                }\n                Err(TrySendError::Disconnected(_)) => Some(epoch),\n            }\n        } else {\n            None\n        };\n        // Advance the render grid AFTER the fan-out — it must never sit between the\n        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\n        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\n        // snapshots it under the same short log lock only when an attach requests it.\n        self.grid.advance(bytes);\n        result\n    }\n\n    /// The current controller's identity (`Some(node)` remote / `None` local),\n    /// or `None` when undriven. The `by`-keyed ownership logic in\n    /// [`resolve_subscribe`] reads this.\n    fn controller_by(&self) -> Option<&str> {\n        self.controller.as_ref().and_then(|c| c.by.as_deref())\n    }\n\n    /// Whether ANY controller currently drives this session (the `controlled`\n    /// any-controller truth — REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). Snapshotted under\n    /// the log lock, then converged onto the perch OFF the lock.\n    fn has_controller(&self) -> bool {\n        self.controller.is_some()\n    }\n\n    /// The live viewer count (the `viewer_count` truth). Snapshotted under the log lock\n    /// for the convergence re-assert.\n    fn live_viewer_count(&self) -> u32 {\n        self.viewers.len() as u32\n    }\n\n    /// Become (or silently re-take) the interactive controller from `from_seq`.\n    ///\n    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller\n    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\n    /// inline under the log lock. The writer thread advances the authoritative\n    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,\n    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\n    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\n    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\n    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\n    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\n    /// single live writer per connection and a monotonic on-wire seq stream,\n    /// [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]). Stamps `driven_by` to the new\n    /// controller's identity.\n    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {\n        // Drop the prior controller sink first (its writer's live loop ends when\n        // tx drops), and bump the generation so (a) an in-flight deadline-evict\n        // for the old controller can't unseat this one and (b) a prior writer\n        // still flushing its initial batch returns before its next frame.\n        // Attribution (F-039 legs b+d): the replaced conn's record names its\n        // successor, and the new conn accretes the controller role fact.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        if let Some(old) = self.controller.take() {\n            old.send.lifecycle_event(\n                \"controller-replaced\",\n                &format!(\"session={} new_conn={}\", self.session_id, sub.id()),\n            );\n        }\n        sub.describe(&format!(\n            \"controller session={} endpoint={} by={}\",\n            self.session_id,\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint },\n            by.as_deref().unwrap_or(\"local\")\n        ));\n        sub.lifecycle_event(\n            \"controller-attach\",\n            &format!(\"session={} from_seq={from_seq}\", self.session_id),\n        );\n        self.controller = None;\n        let epoch = self.controller_epoch.fetch_add(1, Ordering::AcqRel) + 1;\n        // #6 (ADR-0031): a COLD attach's initial batch is a single synthesized clean\n        // repaint of the CURRENT SCREEN (at pseudo-seq `next_seq - 1`) instead of the\n        // raw ring — replaying an alt-screen TUI's raw ring into a fresh terminal is\n        // exactly the corruption #6/#7/#8 report. A resume-from-floor (`from_seq > 0`)\n        // keeps the raw-ring exactly-once re-fetch untouched (B2). Either way the\n        // batch is owned by the writer thread (never pushed through the live channel —\n        // it must not block the drain); live frames stream raw + in-order afterwards.\n        let initial: Vec<(u64, Vec<u8>)> = match self.repaint_initial(from_seq) {\n            Some(repaint) => vec![repaint],\n            None => self\n                .ring\n                .iter()\n                .filter(|(s, _)| *s >= from_seq)\n                .cloned()\n                .collect(),\n        };\n        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n        let writer_send = Arc::clone(&sub);\n        let dt = Arc::clone(&self.delivered_through);\n        let epoch_gate = Arc::clone(&self.controller_epoch);\n        let sid = self.session_id;\n        // W2 decouple: the writer publishes its in-flight-write window here so a\n        // stall-evict can distinguish a wedged writer from an idle one.\n        let write_blocked_since = Arc::new(Mutex::new(None));\n        let writer_blocked = Arc::clone(&write_blocked_since);\n        let writer = thread::spawn(move || {\n            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\n        });\n        self.controller = Some(ControllerSink {\n            tx,\n            send: sub,\n            by,\n            last_ok: Instant::now(),\n            write_blocked_since,\n            _writer: writer,\n        });\n        self.stamp_driven_by();\n    }\n\n    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared\n    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per\n    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its\n    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an\n    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\n    fn clear_controller(&mut self) {\n        if self.controller.take().is_some() {\n            self.stamp_driven_by();\n        }\n    }\n\n    /// B-2 (REQ-CONTROLLER-LIVENESS-REAP): lazily drop a controller whose WRITER\n    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\n    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\n    /// writer exits when a socket write fails (a severed / keepalive-killed controller\n    /// conn) or its `tx` drops; while it is parked on `rx.recv()` (a LIVE but idle\n    /// controller) it is NOT finished, so a live controller is never touched (the\n    /// no-false-reap guard). Reaping here makes the broker report honest truth so the\n    /// EXISTING [`converge_perch_stamps`] clears the now-stale info.json\n    /// `driven_by`/`controlled` stamp — no new detach path, no new query.\n    ///\n    /// LOCK-SAFE: this runs UNDER the log lock in the snapshot closure, so it must do\n    /// NO file I/O (the lock-across-effect discipline, KH 7.12/5.16). It drops the\n    /// sink in-memory ONLY (like [`become_controller`]'s prior-drop) and does NOT\n    /// [`stamp_driven_by`] — the OFF-lock `converge_perch_stamps` writes the honest\n    /// stamp. Returns true iff it reaped.\n    ///\n    /// SCOPE (doyle B-2 reduction): catches a writer that FAILED A WRITE (output was\n    /// flowing when the conn severed) or whose detach was dropped by a prior wedge\n    /// (the B-1 poison class). A TRULY IDLE severed controller (writer parked on recv,\n    /// no output, conn not yet EOF'd) stays `is_finished() == false` and converges\n    /// only once output resumes or the conn EOFs (→ `handle_conn` detach) — that\n    /// harder active-probe case is the reserved [`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`],\n    /// deliberately NOT built here. KH 7.15 held by construction: only ever CLEARS,\n    /// never latches `driven_by`.\n    // [impl->REQ-CONTROLLER-LIVENESS-REAP]\n    fn reap_dead_controller(&mut self) -> bool {\n        let dead = self\n            .controller\n            .as_ref()\n            .is_some_and(|c| c._writer.is_finished());\n        if dead {\n            // In-memory drop only (ends the — already-exited — writer); NO stamp here\n            // (info.json I/O under the log lock is the KH 7.12 hazard). converge does it.\n            self.controller = None;\n            return true;\n        }\n        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller whose\n        // writer is BLOCKED (not exited) on a suspended brain conn past\n        // BRAIN_WRITE_DEADLINE is also reaped here, so the honest `KIND_SESSIONS`\n        // truth + the off-lock converge_perch_stamps release its stale `driven_by`\n        // stamp even when nobody take/reattaches. Deferred stamp: the reap runs in\n        // the multi-session snapshot closure (no I/O under the shared sessions lock).\n        self.stall_evict_controller(Instant::now(), StampMode::Deferred)\n    }\n\n    /// Deadline-evict path (W1): the drain observed a fully-stalled controller and\n    /// timed out its `send_timeout`. Detach it — but ONLY if it is still the same\n    /// controller generation the drain saw (`epoch`), so a NEWER controller that\n    /// attached during the stall is never unseated.\n    fn mark_controller_gone(&mut self, epoch: u64) {\n        if self.controller_epoch.load(Ordering::Acquire) == epoch {\n            self.clear_controller();\n        }\n    }\n\n    /// Whether the current controller's writer is WEDGED — blocked inside a single\n    /// socket write for at least `deadline` (the brain-subscriber conn suspended /\n    /// black-holed). An IDLE controller (writer parked on `rx.recv()`, nothing to\n    /// write) reads NOT stalled: its [`ControllerSink::write_blocked_since`] is `None`.\n    /// Pure over the injected `now` + `deadline` so the unit tests it with no thread.\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn controller_write_stalled(&self, now: Instant, deadline: Duration) -> bool {\n        self.controller.as_ref().is_some_and(|c| {\n            c.write_blocked_since\n                .lock()\n                .ok()\n                .and_then(|g| *g)\n                .is_some_and(|since| now.saturating_duration_since(since) >= deadline)\n        })\n    }\n\n    /// 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    /// under the log lock via [`become_controller`]) or is deferred to the off-lock\n    /// `converge_perch_stamps` (the multi-session reap closure, KH 7.12 no-I/O rule).\n    // [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n    fn stall_evict_controller(&mut self, now: Instant, stamp: StampMode) -> bool {\n        let deadline = brain_write_deadline();\n        if !self.controller_write_stalled(now, deadline) {\n            return false;\n        }\n        // Attribution (F-039 leg b): the evicted controller's stable conn id +\n        // stamps ride the record. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        let conn = self.controller.as_ref().map(|c| c.send.id()).unwrap_or(0);\n        eprintln!(\n            \"BRAIN_SUBSCRIBER_STALL_EVICT:{} conn={conn} {} — controller writer blocked > {}ms \\\n             on a suspended/black-holed brain conn; detached, driven_by released \\\n             [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\n            self.session_id,\n            crate::conn::log_stamp(),\n            deadline.as_millis()\n        );\n        record_stall_evict();\n        match stamp {\n            // Inline: drop the sink AND re-stamp the perch (become_controller's\n            // sibling — the take/reattach path is already I/O-under-log-lock).\n            StampMode::Inline => self.clear_controller(),\n            // Deferred: in-memory drop ONLY (the reap closure forbids I/O under the\n            // shared sessions lock); converge_perch_stamps writes the release off-lock.\n            StampMode::Deferred => self.controller = None,\n        }\n        true\n    }\n\n    /// Add a read-only viewer: snapshot the ring from `from_seq` into the writer\n    /// thread (NOT through the bounded live queue — replay must not trip the\n    /// overflow eviction), seed it the current size frame first, then run the\n    /// live queue. The drain thread fans live frames in via `try_send`.\n    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {\n        let vid = self.next_viewer_id;\n        self.next_viewer_id += 1;\n        // Attribution (F-039 legs b+d): viewer role fact + attach record.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        sub.describe(&format!(\n            \"viewer session={} endpoint={} vid={vid}\",\n            self.session_id,\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint }\n        ));\n        sub.lifecycle_event(\n            \"viewer-attach\",\n            &format!(\"session={} vid={vid} from_seq={from_seq}\", self.session_id),\n        );\n        // The size frame + the initial batch (owned by the thread, not the bounded\n        // queue). A COLD viewer attach (`from_seq == 0`) gets the #6 clean repaint;\n        // a skip-to-live / snap re-attach (`from_seq > 0`, incl. `u64::MAX`) keeps\n        // the raw-ring slice (`s >= from_seq`) — empty for skip-to-live, as before.\n        let mut initial: Vec<Envelope> =\n            vec![size_envelope(self.session_id, self.size.0, self.size.1)];\n        match self.repaint_initial(from_seq) {\n            Some((seq, bytes)) => initial.push(output_envelope(self.session_id, seq, &bytes)),\n            None => initial.extend(\n                self.ring\n                    .iter()\n                    .filter(|(s, _)| *s >= from_seq)\n                    .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),\n            ),\n        }\n        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);\n        let writer_send = Arc::clone(&sub);\n        let evicted = Arc::new(AtomicBool::new(false));\n        let writer_evicted = Arc::clone(&evicted);\n        let session_id = self.session_id;\n        let writer =\n            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));\n        self.viewers.insert(\n            vid,\n            ViewerSink {\n                tx,\n                send: sub,\n                evicted,\n                _writer: writer,\n            },\n        );\n    }\n\n    /// Resolve a controller/viewer subscribe under the W2.5 model\n    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs\n    /// the controller take / viewer add / loud displace as a side effect. See the\n    /// identity matrix in the body — the `by`-keyed logic is what makes a\n    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.\n    fn resolve_subscribe(\n        &mut self,\n        sub: SharedSend,\n        from_seq: u64,\n        intent: AttachIntent,\n        by: Option<String>,\n    ) -> SubscribeOutcome {\n        if intent == AttachIntent::Viewer {\n            if self.viewers.len() >= MAX_VIEWERS {\n                // Soft cap reached — refuse the viewer (bounds thread count).\n                return SubscribeOutcome::BusyControlled {\n                    by: \"viewer cap reached\".to_string(),\n                };\n            }\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        // Control or Take.\n        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller WEDGED\n        // on a suspended/black-holed brain conn past BRAIN_WRITE_DEADLINE is\n        // stall-evicted HERE — the take/reattach self-heals against broker truth\n        // without waiting for new output (the output-driven mark_controller_gone\n        // never fires on an idle session). Inline stamp: this path already writes the\n        // perch under the log lock via become_controller. After the evict the slot\n        // reads free and the normal free/take logic below runs.\n        self.stall_evict_controller(Instant::now(), StampMode::Inline);\n        if self.controller.is_none() {\n            // Free endpoint → become controller (nothing to displace).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        let same_identity = self.controller_by() == by.as_deref();\n        if same_identity {\n            // Same operator (or both local) re-serving — silent re-take. This is\n            // the gate-#7 path: a successor re-taking the slot after a brain\n            // restart NEVER fires Displaced.\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        if by.is_none() {\n            // The LOCAL brain re-attaching while a REMOTE controls — never steal;\n            // fall back to a silent viewer (gate #7 for the local-resume path).\n            self.add_viewer(sub, from_seq);\n            self.stamp_viewer_count();\n            return SubscribeOutcome::Viewer;\n        }\n        if self.controller_by().is_none() {\n            // A remote takes an UNDRIVEN session (only the local spawner held the\n            // slot) — silent control, no loud displace (there is no remote driver\n            // to notify).\n            self.become_controller(sub, by, from_seq);\n            return SubscribeOutcome::Controller;\n        }\n        // A DIFFERENT remote controls it.\n        match intent {\n            AttachIntent::Take => {\n                // The ONLY loud-displace path: notify the incumbent, then take.\n                // W1: route the Displaced notice through the OLD controller's\n                // writer thread (best-effort, off the dispatch thread) — the\n                // incumbent we're kicking may be exactly the wedged one, so an\n                // inline write here would block the new attacher's dispatch.\n                if let Some(old) = &self.controller {\n                    let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(\"\"));\n                    let _ = old.tx.try_send(CtrlMsg::Control(frame));\n                }\n                self.become_controller(sub, by, from_seq);\n                SubscribeOutcome::TookControl\n            }\n            _ => SubscribeOutcome::BusyControlled {\n                by: self.controller_by().unwrap_or_default().to_string(),\n            },\n        }\n    }\n…\nfn viewer_writer(\n…\n) {\n…\n    // before dropping the sink), emit a KIND_VIEWER_EVICTED marker — DISTINCT from\n    // a session-exit EOF — so the serving brain skips to live (re-subscribes from\n    // the ring floor) instead of dying silently. A NORMAL teardown (session end /\n    // conn drop) leaves the flag false → no marker, exit quietly. This write is in\n    // the dedicated writer thread, OFF the log lock / off the drain (the W1/b4\n    // isolation is preserved). REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT.\n    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n    if evicted.load(Ordering::Acquire) {\n        let marker = evicted_envelope(session_id);\n        let _ = send.write(&marker);\n    }\n}\n\n/// Round-4 observability bridge: a controller writer whose bounded conn write\n/// was DEADLINE-retired (`TimedOut` — the conn watchdog aborted a wedged\n/// write, or a gate-wait behind one exhausted the bound) IS the\n/// brain-subscriber stall-evict fact the W2 tally counts. Pre-round-4 that\n/// wedge aged until a logical sampler (`stall_evict_controller`) observed it;\n/// the physical bound now fires FIRST, so the tally is recorded here — same\n/// event class, same `daemon status` surface. An ORGANIC write failure (peer\n/// death, any non-TimedOut error) stays uncounted, exactly as before.\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nfn note_controller_write_retired(sid: u64, conn: u64, e: &io::Error) {\n    if e.kind() != io::ErrorKind::TimedOut {\n        return;\n    }\n    // conn id + stamps (F-039 leg b). [impl->REQ-CONN-POISON-ATTRIBUTION]\n    eprintln!(\n        \"BRAIN_SUBSCRIBER_STALL_EVICT:{sid} conn={conn} {} — controller conn write \\\n         exceeded its bound; physical conn poisoned + retired (writer released, EOF \\\n         cleanup follows) [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\n        crate::conn::log_stamp()\n    );\n    record_stall_evict();\n}\n\n/// A message to the CONTROLLER writer thread (W1). `Output` carries a sequenced\n/// chunk and advances the authoritative `delivered_through` on a successful\n/// write; `Control` is an out-of-band notice (Displaced, …) that is written but\n/// does NOT move the resume cursor.\nenum CtrlMsg {\n    Output(u64, Envelope),\n    Control(Envelope),\n}\n\n/// Advance the shared `delivered_through` cursor to `seq + 1`, monotonically\n/// (D4-1, via [`advanced_cursor`]) and CAS-safe against a brief two-writer\n/// overlap during a controller displace.\nfn advance_delivered(dt: &AtomicU64, seq: u64) {\n    let mut cur = dt.load(Ordering::Acquire);\n    loop {\n        let next = advanced_cursor(cur, seq, true);\n        if next == cur {\n            return;\n        }\n        match dt.compare_exchange_weak(cur, next, Ordering::AcqRel, Ordering::Acquire) {\n            Ok(_) => return,\n            Err(actual) => cur = actual,\n        }\n    }\n}\n\n/// Advance the controller resume cursor CONTIGUOUSLY (b4 / the (a) B2-gate): move to\n/// `seq + 1` ONLY when the cursor is EXACTLY `seq` (the next expected seq). A gap\n/// (`seq > cursor` — a frame the drain dropped while the controller channel was Full,\n/// the b4 backpressure) FREEZES the cursor at last-contiguous, so a re-attaching\n/// brain's `resume_seq` never skips a dropped chunk (a high-watermark jump PAST the\n/// gap would be a not-exactly-once / non-gapless resume = a B2 violation). An\n/// already-delivered `seq` (`seq < cursor`, a rewind re-send) is a no-op. A single CAS,\n/// no loop: it advances by exactly one, or not at all. In the NO-DROP path (every\n/// live frame is contiguous) this is identical to [`advance_delivered`].\n// [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\nfn contiguous_advance(dt: &AtomicU64, seq: u64) {\n    let _ = dt.compare_exchange(seq, seq + 1, Ordering::AcqRel, Ordering::Acquire);\n}\n\n/// The interactive CONTROLLER's dedicated writer thread (W1 /\n/// REQ-HAZARD-INJECT-CONTROL-COEXIST) — the authoritative analog of\n/// [`viewer_writer`]. Write the `initial` ring-replay batch (advancing the resume\n/// cursor on each success), then drain live [`CtrlMsg`]s with a BLOCKING socket\n/// write each — so a backed-up controller blocks ONLY this thread, never the\n/// session drain (which used to write the controller inline under the log lock).\n/// A failed write (dead operator) ends the thread; the drain's next bounded\n/// send then sees `Disconnected` and detaches the row. `Output` advances\n/// `delivered_through`; `Control` (Displaced/…) does not.\n// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\nfn controller_writer(\n    sid: u64,\n    send: SharedSend,\n    initial: Vec<(u64, Vec<u8>)>,\n    rx: Receiver<CtrlMsg>,\n    delivered_through: Arc<AtomicU64>,\n    epoch: u64,\n    controller_epoch: Arc<AtomicU64>,\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\n) {\n    // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): mark the exact window a\n    // BLOCKING socket write is in flight, so a reader can tell a WEDGED writer (brain\n    // conn suspended) from a merely idle one and stall-evict it. Set BEFORE the write,\n    // cleared AFTER — the mutex is never held across `write_frame` (that would just\n    // move the wedge onto the mutex).\n    let mark_blocked = |on: bool| {\n        if let Ok(mut g) = write_blocked_since.lock() {\n            *g = if on { Some(Instant::now()) } else { None };\n        }\n    };\n    for (seq, bytes) in &initial {\n        let frame = output_envelope(sid, *seq, bytes);\n        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\n        // epoch past ours) must write NOTHING further — otherwise its owned\n        // initial batch races the new writer on the same socket and reorders the\n        // seq stream. The gate closure runs AFTER the conn's serialized write\n        // gate is acquired and BEFORE any byte is written — the same\n        // serialization point the old epoch-check-under-`send.lock()` had: both\n        // writers serialize on the one conn gate, and `become_controller` bumps\n        // the epoch BEFORE spawning the new writer, so whichever superseded\n        // writer next holds the gate observes the bump and bails before\n        // committing — no check-then-block-then-write window. (A frame the\n        // stale writer committed BEFORE the bump is irreducible at the broker;\n        // fix #1 removes the contradictory double-take that produced it.)\n        // Round-4: the write itself is bounded + cancelable — a non-draining\n        // consumer poisons + retires the physical conn at the deadline instead\n        // of wedging this thread in the OS write forever.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        mark_blocked(true);\n        let outcome =\n            send.write_gated(&frame, || controller_epoch.load(Ordering::Acquire) == epoch);\n        mark_blocked(false);\n        match outcome {\n            Ok(ConnWrite::Done) => {}\n            Ok(ConnWrite::Superseded) => {\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=superseded\"),\n                );\n                return;\n            }\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n                );\n                return;\n            }\n        }\n        advance_delivered(&delivered_through, *seq);\n    }\n    while let Ok(msg) = rx.recv() {\n        let (frame, seq) = match &msg {\n            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),\n            CtrlMsg::Control(frame) => (frame, None),\n        };\n        // NO epoch gate on the live path (P1c): new output only ever flows to the\n        // CURRENT controller's channel (the drain clones `self.controller.tx`), so\n        // a superseded writer's channel holds only its pre-supersede output\n        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL\n        // control frames — notably the `Displaced` kick the displaced controller\n        // MUST still receive. The loop ends naturally when `tx` drops. Gating\n        // output here would suppress that `Displaced` (the cv-matrix loud-take\n        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.\n        mark_blocked(true);\n        let res = send.write(frame);\n        mark_blocked(false);\n        match res {\n            Ok(_) => {}\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n                );\n                return;\n            }\n        }\n        if let Some(seq) = seq {\n            // (a) CONTIGUOUS cursor (b4 / B2 gapless-resume — doyle's gate): with the\n            // drop-on-full handoff in `append`, the controller's live channel can SKIP\n            // a seq (dropped while Full). The frame still went to the socket (the live\n            // operator sees it), but the durable resume cursor must NOT jump past the\n            // gap — see [`contiguous_advance`].\n            contiguous_advance(&delivered_through, seq);\n        }\n    }\n…\n}\n…\n    /// The output-log high-water (`next_seq`) of a hosted session, or `None` if\n    /// absent. It advances every time the child emits output — the\n    /// **functional**-survival seam for the D7-1 process-level E2E (doyle F1):\n    /// after a brain-PROCESS swap, writing input to the broker-held PTY child and\n    /// watching THIS advance proves the path is alive end-to-end (input → child →\n    /// echo → broker ring), not merely that the handle is held. A read-only direct\n    /// peek, like [`session_pid`](Self::session_pid).\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\n        self.sessions\n            .lock()\n            .unwrap()\n            .get(&id)\n            .map(|h| recover_log(&h.log).next_seq)\n    }\n\n    /// Drive one brain connection: handshake, then dispatch its command frames\n    /// until it disconnects. On disconnect, detach this connection from any\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\n    /// restart must not disturb the hosted children).\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n        struct HandlerTally<'a>(&'a AtomicUsize);\n        impl Drop for HandlerTally<'_> {\n            fn drop(&mut self) {\n                self.0.fetch_sub(1, Ordering::AcqRel);\n            }\n        }\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\n        let _tally = HandlerTally(&self.conn_handlers);\n\n        let (mut recv, send) = conn.split();\n        // Round-4: the send half is owned by the bounded/cancelable conn\n        // primitive from birth — every write on this physical connection is\n        // deadline-bounded, and a wedge poisons + retires the conn.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        send.describe(\"role=brain\");\n\n        // Version handshake: this connection must be a brain (2.3).\n        recv_hello(&mut recv, Role::Brain)?;\n\n        let mut my_subs: Vec<u64> = Vec::new();\n        let mut my_stream_subs: Vec<u64> = Vec::new();\n        let mut my_presence_sub = false;\n\n        loop {\n            let env = match read_frame(&mut recv) {\n                Ok(e) => e,\n                // Any read failure (EOF included) ends the connection.\n                Err(_) => break,\n            };\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_UNSUBSCRIBE => {\n…\n                }\n…\n            }\n        }\n…\n    }\n…\n    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n…\n        // The output log, with the spawning connection pre-attached as the\n        // controller (identity `None` = LOCAL, not a remote driver) so no chunk\n        // is missed before an explicit subscribe — and so a remote attach to a\n        // spawned-but-undriven session takes control silently (no loud displace\n        // of the local-spawner placeholder; REQ-RCVIEW-1).\n        let log = Arc::new(Mutex::new(OutputLog::new(\n            id,\n            self.log_cap_chunks,\n            req.endpoint.clone(),\n            (req.rows, req.cols),\n        )));\n        log.lock()\n            .unwrap()\n            .become_controller(Arc::clone(send), None, 0);\n\n        // Output drain → append under the log lock (fast: ring + viewer fan-out),\n        // then deliver to the controller OFF the lock with a bounded deadline\n        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no\n        // longer inline under the log lock, so a backed-up controller can never\n        // wedge the session; a fully-stalled controller is detached after the\n        // deadline (bounded wedge, never permanent).\n        let log_drain = Arc::clone(&log);\n        let drain = session\n            .drain(move |chunk| {\n                // append() does the controller handoff inline now (one non-blocking\n                // try_send — b4) and returns Some(epoch) ONLY when the controller is\n                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\n                // the first lock (a fresh lock, no self-deadlock). The drain never\n                // sleeps here, so a slow controller can't throttle the viewer fan-out.\n                let evict = recover_log(&log_drain).append(chunk);\n                if let Some(epoch) = evict {\n                    recover_log(&log_drain).mark_controller_gone(epoch);\n                }\n            })\n            .map_err(|e| format!(\"drain failed: {e}\"))?;\n\n        // Exit waiter → one `exit` frame when the child is reaped, then the dead\n        // session is removed from the table. Runs on its own thread; `wait`\n        // holds the child lock but `kill` uses a split killer.\n        let sessions_exit = Arc::clone(&self.sessions);\n        let log_exit = Arc::clone(&log);\n        let waiter = Arc::clone(&session);\n        thread::spawn(move || {\n            if let Ok(status) = waiter.wait() {\n                let ev = ExitEvent {\n                    session_id: id,\n                    code: Some(status.exit_code() as i32),\n                };\n                let frame = Envelope::new(\n                    KIND_EXIT,\n                    serde_json::to_value(ev).expect(\"ExitEvent serializes\"),\n                );\n                // Send to every attached sink — controller + all viewers (under\n                // the log lock).\n                let sinks = recover_log(&log_exit).all_sinks();\n                for sink in sinks {\n                    // Bounded conn write (round-4): a wedged sink can no longer\n                    // park the exit-waiter thread indefinitely.\n                    let _ = sink.write(&frame);\n                }\n            }\n            // Reap-time unlatch (REQ-HAZARD-CONTROL-STAMP-LIFETIME): the child\n            // died, not the controller conn, so no detach path clears the perch\n            // — do it here so ONLINE+CONTROLLED / driven_by / viewer_count cannot\n            // outlive the session in info.json (or gossip a stale controller).\n            // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n            recover_log(&log_exit).stamp_reaped();\n            // Reap the dead session from the table.\n            recover(&sessions_exit).remove(&id);\n        });\n\n        // Layer C: if the spawn carried a translation binary, bring it up with the\n        // session — spawn the pure stdin→stdout filter + the inject worker that\n        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to SPOOLING\n        // (logged, `None` → idle inbound spools, never raw-injected) rather than\n        // aborting the session — a missing binary costs idle delivery, never the\n        // operator their agent.\n        // C-1: the shared bounded-respawn give-up counter (starts at 0; the worker\n        // resets it on a healthy commit, the dispatch respawn path increments it).\n        let translation_respawns = Arc::new(AtomicU32::new(0));\n        let translation = req\n            .translation_binary\n            .as_deref()\n            .and_then(|argv| {\n                build_translation(\n                    argv,\n                    &req.endpoint,\n                    &input,\n                    Arc::clone(&translation_respawns),\n                    &log,\n                )\n            });\n\n        recover(&self.sessions).insert(\n            id,\n            HostedSession {\n…\n            },\n        );\n…\n        let resp = Spawned {\n            session_id: id,\n            pid,\n        };\n        let frame = Envelope::new(\n            KIND_SPAWNED,\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\n        );\n        send_frame(send, &frame);\n        Ok(id)\n    }\n\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\n    /// self-kick guard.\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n        let req: SubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\n        // Clone the log Arc out from under the sessions lock, then resolve under\n        // the log lock — never hold both locks at once.\n        //\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\n        // the per-session exit-waiter holds for the WHOLE life of a live child\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\n        // post-removal subscribe gets a prompt \"no such session\" error — both\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\n        // the rest.\n        let log = {\n            let sessions = recover(&self.sessions);\n            let h = sessions\n                .get(&req.session_id)\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\n            Arc::clone(&h.log)\n        };\n        let outcome = recover_log(&log).resolve_subscribe(\n            Arc::clone(send),\n            req.from_seq,\n            req.intent,\n            req.by,\n        );\n        send_frame(send, &subscribed_envelope(req.session_id, outcome));\n        Ok(req.session_id)\n    }\n\n    /// Release this connection's controller/viewer role on a session\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\n    /// a no-op if a newer controller already took over, or the session is gone.\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\n        let log = {\n            let sessions = recover(&self.sessions);\n            match sessions.get(&req.session_id) {\n                Some(h) => Arc::clone(&h.log),\n                None => {\n                    eprintln!(\"DBG unsubscribe: no session {}\", req.session_id);\n                    return Ok(());\n                }\n            }\n        };\n        recover_log(&log).detach_if(send);\n        Ok(())\n    }\n\n    /// Inject input bytes into a session's PTY.\n    ///","startLine":80,"lineNumbers":[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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,null,795,null,831,832,null,837,838,null,840,841,842,843,844,845,846,847,848,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,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,null,1376,null,1382,null,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,null,1617,null,2960,2961,2962,2963,2964,2965,2966,2967,2968,2969,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,null,3029,null,3204,3205,null,3226,null,3235,null,3330,3331,3332,3333,3334,3335,3336,3337,3338,3339,3340,3341,3342,3343,3344,3345,3346,3347,3348,3349,3350,3351,3352,3353,3354,3355,3356,3357,3358,3359,3360,3361,3362,3363,3364,3365,3366,3367,3368,3369,3370,3371,3372,3373,3374,3375,3376,3377,3378,3379,3380,3381,3382,3383,3384,3385,3386,3387,3388,3389,3390,3391,3392,3393,3394,3395,3396,3397,3398,3399,3400,3401,3402,3403,3404,3405,3406,3407,3408,3409,3410,3411,3412,3413,3414,3415,3416,3417,3418,3419,3420,3421,3422,3423,3424,3425,null,3436,3437,null,3439,3440,3441,3442,3443,3444,3445,3446,3447,3448,3449,3450,3451,3452,3453,3454,3455,3456,3457,3458,3459,3460,3461,3462,3463,3464,3465,3466,3467,3468,3469,3470,3471,3472,3473,3474,3475,3476,3477,3478,3479,3480,3481,3482,3483,3484,3485,3486,3487,3488,3489,3490,3491,3492,3493,3494,3495,3496,3497,3498,3499,3500,3501,3502,3503,3504,3505,3506,3507,3508,3509,3510,3511,3512,3513,3514,3515]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"}}},"isError":false,"timestamp":1784247610525}}
{"type":"message","id":"48bb3a8e","parentId":"3e378fb8","timestamp":"2026-07-17T00:20:10.527Z","message":{"role":"toolResult","toolCallId":"call_B5hA8WfZOgRqoEiAGf28FT2t|fc_0996e78cf50e3160016a59753991a8819980ea6f1224541606","toolName":"glob","content":[{"type":"text","text":"# .claude/reports/2026-07-16-redispatch-truth/\nhertz-refresh-rca.txt"}],"details":{"scopePath":".claude/reports/2026-07-16-redispatch-truth","fileCount":1,"files":[".claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1784247610527}}
{"type":"message","id":"0e73afb3","parentId":"48bb3a8e","timestamp":"2026-07-17T00:20:15.138Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_icEVA4lQIr4GhSdrwL5VSJ60|fc_0996e78cf50e3160016a59753d4fd08199abdd83876f337e52","name":"read","arguments":{"i":"Reading field RCA report","path":".claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt","selector":"1-360"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":23671,"output":53,"cacheRead":78336,"cacheWrite":0,"totalTokens":102060,"cost":{"input":0.11835500000000002,"output":0.00159,"cacheRead":0.039168,"cacheWrite":0,"total":0.159113}},"stopReason":"toolUse","timestamp":1784247610904,"responseId":"resp_0996e78cf50e3160016a59753b36008199b954f52308777d82","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0996e78cf50e3160016a59753d4fd08199abdd83876f337e52","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading field RCA report\",\"path\":\".claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt\",\"selector\":\"1-360\"}","call_id":"call_icEVA4lQIr4GhSdrwL5VSJ60","name":"read"}]},"duration":4233.659400001168,"ttft":2399.2540000006557,"contextSnapshot":{"promptTokens":102007,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_icEVA4lQIr4GhSdrwL5VSJ60|fc_0996e78cf50e3160016a59753d4fd08199abdd83876f337e52","toolName":"read","startedAt":"2026-07-17T00:20:15.138Z","args":{"path":".claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt"},"intent":"Reading field RCA report"},"id":"2b5fc1d0","parentId":"0e73afb3","timestamp":"2026-07-17T00:20:15.138Z"}
{"type":"message","id":"0f5bb160","parentId":"2b5fc1d0","timestamp":"2026-07-17T00:20:15.146Z","message":{"role":"toolResult","toolCallId":"call_icEVA4lQIr4GhSdrwL5VSJ60|fc_0996e78cf50e3160016a59753d4fd08199abdd83876f337e52","toolName":"read","content":[{"type":"text","text":"HERTZ RCA — `spt daemon refresh` / `spt update` freezes existing remote session PTYs\n\nVerdict: target-brain refresh exposes a production redispatch bug. Hosted PTYs and broker-held QUIC streams survive, but active remote attach handlers are brain-owned and must be reconstructed. The fresh dispatcher indiscriminately replays every historical peer stream still in the broker table—including finished attach streams—and can let stale workers steal and then clear current controllers. A second adjacent defect prevents recovery when an active stream's opener has rolled out of its bounded history.\n\nCausal chain:\n1. `spt daemon refresh` raises the supervisor brain-restart signal. Broker and hosted PTY child/master resources remain alive; only the brain process and its dispatcher/serve workers die. `spt update` uses the same brain-cycle path after apply.\n2. Every active cross-node `spt rc` has a peer-initiated broker-held net stream. Its target-side `serve_attach` worker owns a broker connection labelled `role=brain stream-subscriber stream=X controller session=Y`; therefore that semantic relay dies with the old brain even though the PTY and QUIC stream remain.\n3. The new brain intentionally resumes only hosted session cursors at boot. After net startup it launches a fresh `dispatch::run_dispatch_loop`, whose empty per-process `claimed` set enumerates every broker-held peer stream.\n4. Finished streams are never removed from `NetShared.streams`; `StreamLog::finish` only marks them finished. A new dispatcher therefore replays all historical attach streams, not only active streams.\n5. Each replay worker reads the old `AttachRecord::Request` and calls `attach_as(Control, same origin)`. Same-identity takeover silently calls `become_controller`, so it can supersede the legitimate newly reconstructed/current worker without a `Displaced` signal. Replayed EOF then calls `detach_session`, clearing the controller the stale worker just stole. The legitimate active serve loop stays open and believes it is Controller, but its sink is no longer selected, so the operator sees a frozen PTY.\n6. Detach + `spt rc` creates a fresh stream whose worker runs after the stale replay storm and takes control again, explaining immediate recovery without a PTY restart.\n7. Adjacent recovery defect: reconstruction classifies a stream by `peek_first_line(stream_id)` from sequence 0. `StreamLog` is a bounded 4096-transport-chunk ring and does not preserve protocol openers. An older/high-input active attach stream can lose its Request; replay then starts at Input/Resize, mid-record, or a gap/error. `claimed.insert(stream_id)` happens before spawn and is never cleared or retried on `Unknown`/`Failed`, so that active stream is permanently abandoned too.\n\nWhy 4/5, not all: the primary outcome depends on each endpoint's historical finished attach streams and worker scheduling; the adjacent outcome depends on retained history/activity. Four endpoints had a stale replay takeover and/or unrecoverable active opener, while the unaffected endpoint had neither winning condition in that refresh. On the later update, lia also hit one of them. Endpoint identity itself is not causal.\n\nField evidence on HFENDULEAM:\n- Brain generations 2 and 3 both log `BRAIN_RESUMED: re-established 7 session cursor(s)` and `BRAIN_NET_CONSUMERS_UP`.\n- During those generations the new dispatcher logs failures for persistent and historical streams: `DISPATCH:741:Failed(\"No process is on the other end of the pipe. (os error 233)\")`, `DISPATCH:3:Failed(\"failed to fill whole buffer\")`, `DISPATCH:4496:Failed(...)`, and `DISPATCH:4176:Failed(...)`. The very low and current-looking stream IDs in the same fresh generation are consistent with indiscriminate historical replay; the failures prove one-shot redispatch workers were abandoned. They are supporting evidence, not a one-log-line-per-frozen-endpoint mapping.\n- Detach + `spt rc` reattach restores each session without restarting its hosted PTY, proving the PTY itself did not freeze.\n\nExact code seams:\n- `crates/spt-daemon/src/applyhost.rs`: refresh/apply requests the same supervisor brain cycle.\n- `crates/spt-daemon/src/dispatch.rs:194-232`: fresh dispatcher; one-shot `claimed` insertion before spawn.\n- `dispatch.rs:255-305`: opener replay/classification from seq 0, then fresh family worker.\n- `dispatch.rs:335+` → `attach::serve_attach`: target attach reconstruction.\n- `crates/spt-daemon/src/nethost.rs`: `DEFAULT_STREAM_RING_CHUNKS = 4096`; bounded `StreamLog` append/eviction.\n- `NetShared.streams` has no finished-row removal path; `StreamLog::finish` marks EOF but leaves the row discoverable by every future dispatcher generation.\n- Existing `attach_survives_target_brain_restart_exactly_once` manually re-serves a known short stream, so it bypasses production rediscovery/classification and never rolls the opener.\n\nRecommended source fix:\nA. Remove/retire finished stream rows from redispatch eligibility while preserving only the lifecycle state genuinely needed after EOF. A fresh dispatcher must never re-serve a terminal Attach request.\nB. Make active-stream dispatch identity restart-durable and independent of evictable transport history. Store an immutable bounded opener/classification fact with each broker-held inbound stream (or pin the complete first NDJSON record outside the rolling data ring) until stream close. Recover family + cursor from that metadata, not ring seq 0.\nC. Make dispatcher claims retryable: remove/requeue a claim after transient worker setup failure. Do not hot-loop terminal outcomes; explicitly distinguish active/retryable, served, and finished streams.\nD. Add production-path regressions:\n   1. Create a finished historical Attach plus a current active Attach for the same endpoint/origin, restart only target brain/dispatcher, and prove the historical stream neither takes nor clears the current controller; current input/output remains exactly once without detach.\n   2. Push >4096 transport chunks on an active Attach so seq0/opener is evicted, restart dispatcher, and prove durable classification resumes the same operator stream.\n   3. Inject one transient worker-start failure and prove claim retry recovers without duplicate controller/output.\n\nThis is the source-level RCA. No live refresh/update was rerun during diagnosis; the live incident logs plus code path were used to avoid freezing active sessions again."}],"details":{"displayContent":{"text":"HERTZ RCA — `spt daemon refresh` / `spt update` freezes existing remote session PTYs\n\nVerdict: target-brain refresh exposes a production redispatch bug. Hosted PTYs and broker-held QUIC streams survive, but active remote attach handlers are brain-owned and must be reconstructed. The fresh dispatcher indiscriminately replays every historical peer stream still in the broker table—including finished attach streams—and can let stale workers steal and then clear current controllers. A second adjacent defect prevents recovery when an active stream's opener has rolled out of its bounded history.\n\nCausal chain:\n1. `spt daemon refresh` raises the supervisor brain-restart signal. Broker and hosted PTY child/master resources remain alive; only the brain process and its dispatcher/serve workers die. `spt update` uses the same brain-cycle path after apply.\n2. Every active cross-node `spt rc` has a peer-initiated broker-held net stream. Its target-side `serve_attach` worker owns a broker connection labelled `role=brain stream-subscriber stream=X controller session=Y`; therefore that semantic relay dies with the old brain even though the PTY and QUIC stream remain.\n3. The new brain intentionally resumes only hosted session cursors at boot. After net startup it launches a fresh `dispatch::run_dispatch_loop`, whose empty per-process `claimed` set enumerates every broker-held peer stream.\n4. Finished streams are never removed from `NetShared.streams`; `StreamLog::finish` only marks them finished. A new dispatcher therefore replays all historical attach streams, not only active streams.\n5. Each replay worker reads the old `AttachRecord::Request` and calls `attach_as(Control, same origin)`. Same-identity takeover silently calls `become_controller`, so it can supersede the legitimate newly reconstructed/current worker without a `Displaced` signal. Replayed EOF then calls `detach_session`, clearing the controller the stale worker just stole. The legitimate active serve loop stays open and believes it is Controller, but its sink is no longer selected, so the operator sees a frozen PTY.\n6. Detach + `spt rc` creates a fresh stream whose worker runs after the stale replay storm and takes control again, explaining immediate recovery without a PTY restart.\n7. Adjacent recovery defect: reconstruction classifies a stream by `peek_first_line(stream_id)` from sequence 0. `StreamLog` is a bounded 4096-transport-chunk ring and does not preserve protocol openers. An older/high-input active attach stream can lose its Request; replay then starts at Input/Resize, mid-record, or a gap/error. `claimed.insert(stream_id)` happens before spawn and is never cleared or retried on `Unknown`/`Failed`, so that active stream is permanently abandoned too.\n\nWhy 4/5, not all: the primary outcome depends on each endpoint's historical finished attach streams and worker scheduling; the adjacent outcome depends on retained history/activity. Four endpoints had a stale replay takeover and/or unrecoverable active opener, while the unaffected endpoint had neither winning condition in that refresh. On the later update, lia also hit one of them. Endpoint identity itself is not causal.\n\nField evidence on HFENDULEAM:\n- Brain generations 2 and 3 both log `BRAIN_RESUMED: re-established 7 session cursor(s)` and `BRAIN_NET_CONSUMERS_UP`.\n- During those generations the new dispatcher logs failures for persistent and historical streams: `DISPATCH:741:Failed(\"No process is on the other end of the pipe. (os error 233)\")`, `DISPATCH:3:Failed(\"failed to fill whole buffer\")`, `DISPATCH:4496:Failed(...)`, and `DISPATCH:4176:Failed(...)`. The very low and current-looking stream IDs in the same fresh generation are consistent with indiscriminate historical replay; the failures prove one-shot redispatch workers were abandoned. They are supporting evidence, not a one-log-line-per-frozen-endpoint mapping.\n- Detach + `spt rc` reattach restores each session without restarting its hosted PTY, proving the PTY itself did not freeze.\n\nExact code seams:\n- `crates/spt-daemon/src/applyhost.rs`: refresh/apply requests the same supervisor brain cycle.\n- `crates/spt-daemon/src/dispatch.rs:194-232`: fresh dispatcher; one-shot `claimed` insertion before spawn.\n- `dispatch.rs:255-305`: opener replay/classification from seq 0, then fresh family worker.\n- `dispatch.rs:335+` → `attach::serve_attach`: target attach reconstruction.\n- `crates/spt-daemon/src/nethost.rs`: `DEFAULT_STREAM_RING_CHUNKS = 4096`; bounded `StreamLog` append/eviction.\n- `NetShared.streams` has no finished-row removal path; `StreamLog::finish` marks EOF but leaves the row discoverable by every future dispatcher generation.\n- Existing `attach_survives_target_brain_restart_exactly_once` manually re-serves a known short stream, so it bypasses production rediscovery/classification and never rolls the opener.\n\nRecommended source fix:\nA. Remove/retire finished stream rows from redispatch eligibility while preserving only the lifecycle state genuinely needed after EOF. A fresh dispatcher must never re-serve a terminal Attach request.\nB. Make active-stream dispatch identity restart-durable and independent of evictable transport history. Store an immutable bounded opener/classification fact with each broker-held inbound stream (or pin the complete first NDJSON record outside the rolling data ring) until stream close. Recover family + cursor from that metadata, not ring seq 0.\nC. Make dispatcher claims retryable: remove/requeue a claim after transient worker setup failure. Do not hot-loop terminal outcomes; explicitly distinguish active/retryable, served, and finished streams.\nD. Add production-path regressions:\n   1. Create a finished historical Attach plus a current active Attach for the same endpoint/origin, restart only target brain/dispatcher, and prove the historical stream neither takes nor clears the current controller; current input/output remains exactly once without detach.\n   2. Push >4096 transport chunks on an active Attach so seq0/opener is evicted, restart dispatcher, and prove durable classification resumes the same operator stream.\n   3. Inject one transient worker-start failure and prove claim retry recovers without duplicate controller/output.\n\nThis is the source-level RCA. No live refresh/update was rerun during diagnosis; the live incident logs plus code path were used to avoid freezing active sessions again.","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.claude\\reports\\2026-07-16-redispatch-truth\\hertz-refresh-rca.txt"}}},"isError":false,"timestamp":1784247615146}}
{"type":"message","id":"63b5eed2","parentId":"0f5bb160","timestamp":"2026-07-17T00:20:32.831Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing poison log timing discrepancies**\n\n**Modeling stale connection persistence**\n\n**Confirming brain refresh connection behavior**"},{"type":"toolCall","id":"call_mK2Xl9C4eqTD8ZQojEdINgot|fc_0996e78cf50e3160016a59754ffc888199a5445ca6043a0fc6","name":"grep","arguments":{"i":"Finding brain conn stall tests","pattern":"BRAIN_SUBSCRIBER_STALL|stall_evict|suspend|black.hole|poison|controller.*reap|old_gen","path":"crates/spt-daemon/tests;crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/conn.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_DntVd3d0gqwJ7uR4A3G0Gjlm|fc_0996e78cf50e3160016a59754ffd6081998c561cfc5556f8d9","name":"grep","arguments":{"i":"Finding refresh resume tests","pattern":"brain restart|refresh|resume|reconnect|controller|detach","path":"crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/brain_resume_conn_deadlock.rs;crates/spt-daemon/tests/conn_blackhole_lifecycle.rs;crates/spt/tests/attach_wedge_e2e.rs;crates/spt/tests/resume_no_control_steal_e2e.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_7gebgj0apJLf0Bo0bzDlYOOO|fc_0996e78cf50e3160016a59754ffd708199bc1b9be53f3ad7a0","name":"read","arguments":{"i":"Reading old brain promotion boundary","path":"crates/spt-daemon/src/brainproc.rs","selector":"130-330"}},{"type":"toolCall","id":"call_9dCqkcf6TlbntaItvbaD2S8j|fc_0996e78cf50e3160016a59754ffd7c8199a13724f19e498710","name":"read","arguments":{"i":"Reading connection poison mechanism","path":"crates/spt-daemon/src/conn.rs","selector":"1-180,270-650"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":6659,"output":805,"cacheRead":96768,"cacheWrite":0,"totalTokens":104232,"reasoningTokens":483,"cost":{"input":0.033295000000000005,"output":0.02415,"cacheRead":0.048383999999999996,"cacheWrite":0,"total":0.105829}},"stopReason":"toolUse","timestamp":1784247615199,"responseId":"resp_0996e78cf50e3160016a59753f95388199949960a74846a1ea","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a5975414334819995bf80589dcb11de","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXVPnmvzYXXMzxkYg4HqpngsWreezetGkadgFT0ZLv096wlXDxBnc_qZ-pBquYj3K9PyMGbJFM-UchhzbaUqOK8oCdCn808RExKl-Tg0LdcYmn2s1nsgsVUI5hPnEYjARGBVSwUT_R0IT9uRTIWhyhPu1Md6z-Di_aiVMGduP7ChkhKn9c4qXj2BLNuU8eMTN-gatsyTdVD_dgAX86aO2li14h4Kk9pXpeuQVi7x92TbP0856QbZHta9XgkOpc3O0je10tbg0Cufbw1tbPhsCx-gO8Rg0MYGuiXlU55ryIoz2-KWzJ2Bcrn9o8FsJdaFkuWK9--9YSUwXOZkIPzMwr0mzLhFKhBwjLQOOLNo-6DNk_UB2K4q0GhvfWEiz8Y_gqI4xkEwbyF3lcjkLCJj-p1VjxgFF9SMMSvAC5_2ysvFCKbFvcx8_wPx4C1rHcqlJmrRv-5sbcIrmQK3iof7UJxsVp68xRdKAy0Esr7EIvcOfBzBl8SsJW3IgxWqijHIv5n8URZXXK-dC6y14gLBVOHfzUpLDUm0W6gNioImoiR-xXJyRbPwQ64k42W8mMXI6ttAgBhGVScTEjmLKw8CUos_dpUlPkcKR8GkdXw_05KsqgevBME6g8dcuwSa2rlGpcQ6F7i2wj5WwBAWMu4wvazlRqG-GXdWXPF8lUwhKyXv1vxezW43f1HPa8fWW1JhQuSQfXkoF2ty_GDihEpVDAZGggZBW2Jqi9huo5xJMm5KeJG3OF3Iw9P-3Xzj7mP9IARgBkslL_D2NdI1e_lLZ5xktnPhHXH7VtDclf3WcU-fRCPTFXNp2R5tpXpQL_ZTRLKecW8cPlJltJ8341GGS6WJdTdukBu_o2Y0QRxhK0EiRC6vdag0sdJGKDZ7bhm1z6mHtLoUS7dT8ugX_KyGXzmXZh61kKUv7cQxnT1yh925wRShq54kdg_a_y0KPP7TvHJV5p5R4cVwTel58M-x4iYokfbtDtM5qf9gAPdbMNzGPnQ0kEaTSUI3iEXCKPw-7pGrB5XUt2OJ342JzBiIBV_F_rlE_QPsbUZP_Vdv9151G1ZzfuHf1i7ToV-s4qSBp9LX75lFZaoFyaMX3rb8nhthrsTyMIfb9wN_-vrO32fYqLDqKroNHheJv_BJ2T-27EQKygDuzXNQUpNbQlhybc-iW61nrpHYFqiYapyzccaPnumcj0acjVwM28r5igXOqe9PHdfgQL-WQ1iTea4o8dHD1gkmZRl_wrPTFOfVyy-I7VJHLe8dG3JTxz57rGcLQUuO5dRBJhcUzU49b4kfUda-8HDXZSZwjByvLiQRDezdMxIfZIPks97nG9fqzOMAcQ0535M3ZW51AUzmRffr-x5tuMexxmHdC_NH99ixwNJ5-Ne6rvVaDd7F8A-hq_TiANnTzZYhpXpkb62jYtFGNJCqJBp60COeB00Wwd7HUu8kFu3oLZNH-3TqTNIe-BGRFcLPWcghmTvfGLFk366Ctq_DcLcLaJpAcL0o5vrZEtHg-DmaId4C2_IvIXUDmDzoNqoDXxB0T1cl4L2DLm0X6YSeaecOaIXIDMR-GcR8Wm924hxSiCGraGI9Z7so4MXBssG5cx1r0fYYVcDCCAF80dxRkX41YxobMh1GQcE9-OqxuuexlqoTkOVE1NEb_B0E-nDogjqitAg2AqKNxhw7MESjlJeT5nVYoHvG2svYWahUJQvze73Q7HLoopON11wFhB0qRbzPXcRVg58nbbpyhGCnlGfSRpu7iJg_zUn-V2l8CgwMvVbu3Esalgd4iwyw9nhb4BhRNSBhM5uvk1-Z8JGhvjYl4cER2JEnBc4dGySgy_rBfVQkz8zcaXSd0WnCHdItYZQnefHNgnKt6IXDcFRilRNLeuyxm6fM2uUZVr9SBipnDv5s_5-d8XAnA7-nVyBYT9OdsneHyhirRdWaeDbmP5AyS8KGXkCdLI8E7NmgM02tMBOceoI_Ayazvds6yIxUGc5YN8DXVNpcMeHdjPHnQYj_r7xQ1Wx9AqgxpfX05sqF5A4EF3wiO3U94YjAXOQniRlqXS2vDtYGwiMtpsHIo-EOgULfkN7XG8EVHeGwLncHyvK8hrm44JIoG_PRrfKilrcvKJbB8XLqz8KvDZvoGsk4w7WVX7YlYB6xx-mMJuJwNzGXKYRNz4-EF6FzMQ8QsM73DYV-CQ7QmIpNY_oP74jNn9TYbX9MrVhRQfvA92DIzK6jwpzWiTZa0rONvU_xrBvwUEb7jn-LTRHBDF7LgZiN-KvzndBQi_R5pkNdv8ZnAA0eRT3PQL2sB3gufEvQzZp9u7dBP7A-xEwb9C78hledws5UdDACWBrQuVzGZpDZanGuBiVb8E5UzUWKmIDcacqM1wrUEQYL2Y8WUv9eb798lMigFuCFixLKzDvyZwjMvKHhp3yMZ3Y9dS8iEMZftHcrqzUb-qPU5CMD3jKt6ave_6vFONLdacRnfjhiGGaorGRJPweJR20tyqdmW7r-SyQ1-WPDScJ7839Eg8q8FxMQswxPntDf5POZGFCAVlxeW0Y122m9TswzNmZj4Tf9WloPZDBFyEQOqYW16snQKSdqQjLo9h_5vpJxlHlKQxE9FJNA5VwTxCxdXf5kBY5MGk1oUJYNVgRToCBvHB6jTUAhIw-Z-izQRf3L-DF6ieHNd8F0qFzkb2zk5zVjhtedG62icW8mPtjW8vSD-PxSGGGA3HLGc9t1Hq80G1h-79qQqXmXZOaDZ3O752q2dUiXusQVdYa-rNIz2bA8ytVRYyuGsKP9JfGiU0_QnxEBpT_QKkYb51YZW3H42cdRWjQscLv3WiRZy_rdxaum2EXkM-IRuWG2WuUy6IhsmCaOz7eg7qpnFy5kUBcigny1tFAVYL5c7OuzJ246Xw5C1aN8u7Xvv7Eebz8xwYmsMfs64tbobGa8ywyedCvpq9awrGG78pECPHhYXySyHXDsBkdlw4ijwn8PI-lWO9AeXIPBFNtxDntjjyam8uXQKynTyGGBMDPe2dCmBc7gaxqRQCL1JIfp9njR-zNBLMFBUgZs5ZuZM0-FBt-6YNBFBGOGpOyn7cnXPMFsnc3oiM5eUhVzKLcV4SAhZ11pHFiWTY_JXP_IOVoDqbmDBFMQFLONzX7D2OpnIh5Lb5gJlYAfI93l11kHXZVVgPJHDsoQrZjruVJQdo8_sZ_u18K5GF51TS8MzxT_o7I6lfRZv3rICuP-PGf418WNtKB22YrW_rc3Hnjp8Z7MtlQ0T5wf6WApNXbebabxRqEfPuHtPek2SdmaeqxNkkkHWTPhsIUACU1SkjDgPiqAvPrDi8zAuAdPjjJI1-3x-MMZ3hdxF1uOZ9uAvd4qOF98YifmyJI80ocbp_krADW_xAPDbb0JPGnzcKNtpgX7XKZShM9ATGwpfuELQ6Dr4g54WIFmL1C-rj9XMuBTNIS_KrdmwPB1ZPGrj3eEE6RDTfXt_E6DBlZ7F4p0fs0ioVlOXnVi99PPU2GVf-TjUbmA2ShzqRcdrR_-2vWtEbc1UIoWzUbadFRCgjzZbibobuKsXn46A-gcHWw9qrKGx3pvI2J3C54fwF34HxRfVarkWUnH0aC9EVTx5f2-rj3WqVSzkI5D1T6EQxo2SC4SMPzosW-dR9Izg60n8MFsZMnTuUeixpCWzoRiEKNoSi5OVWib4zq206qlFyYG5tBiAZm3m1-mgnkrcZXanzcLEP14yLtNAAJITbN9z55-F246Cir6iLaY67WkyLsIgNwYjC-t9QfRon7A8UcYXQ6uEGFSZpLo80pilbRhnWLS0aJvUTHanqXksP1AI4kqbT2lfwHXqGItR2div35Ov8ASFqebMcEjMgrK3B_ich-j6_6R09FB48Arw9CrmRL4uFZB64-x5nbIrJ3QPySljpxFIFnZh2ZXGwv9ehFBHIElqYRUvWZadYVs7OTPfSwvCnM=","summary":[{"type":"summary_text","text":"**Analyzing poison log timing discrepancies**"},{"type":"summary_text","text":"**Modeling stale connection persistence**"},{"type":"summary_text","text":"**Confirming brain refresh connection behavior**"}]},{"id":"fc_0996e78cf50e3160016a59754ffc888199a5445ca6043a0fc6","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding brain conn stall tests\",\"pattern\":\"BRAIN_SUBSCRIBER_STALL|stall_evict|suspend|black.hole|poison|controller.*reap|old_gen\",\"path\":\"crates/spt-daemon/tests;crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/conn.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_mK2Xl9C4eqTD8ZQojEdINgot","name":"grep"},{"id":"fc_0996e78cf50e3160016a59754ffd6081998c561cfc5556f8d9","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding refresh resume tests\",\"pattern\":\"brain restart|refresh|resume|reconnect|controller|detach\",\"path\":\"crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/brain_resume_conn_deadlock.rs;crates/spt-daemon/tests/conn_blackhole_lifecycle.rs;crates/spt/tests/attach_wedge_e2e.rs;crates/spt/tests/resume_no_control_steal_e2e.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_DntVd3d0gqwJ7uR4A3G0Gjlm","name":"grep"},{"id":"fc_0996e78cf50e3160016a59754ffd708199bc1b9be53f3ad7a0","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading old brain promotion boundary\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"130-330\"}","call_id":"call_7gebgj0apJLf0Bo0bzDlYOOO","name":"read"},{"id":"fc_0996e78cf50e3160016a59754ffd7c8199a13724f19e498710","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading connection poison mechanism\",\"path\":\"crates/spt-daemon/src/conn.rs\",\"selector\":\"1-180,270-650\"}","call_id":"call_9dCqkcf6TlbntaItvbaD2S8j","name":"read"}]},"duration":17621.835100002587,"ttft":2076.620300002396,"contextSnapshot":{"promptTokens":103427,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_mK2Xl9C4eqTD8ZQojEdINgot|fc_0996e78cf50e3160016a59754ffc888199a5445ca6043a0fc6","toolName":"grep","startedAt":"2026-07-17T00:20:32.832Z","args":{"path":"crates/spt-daemon/tests;crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/conn.rs"},"intent":"Finding brain conn stall tests"},"id":"2bdc7996","parentId":"63b5eed2","timestamp":"2026-07-17T00:20:32.832Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_DntVd3d0gqwJ7uR4A3G0Gjlm|fc_0996e78cf50e3160016a59754ffd6081998c561cfc5556f8d9","toolName":"grep","startedAt":"2026-07-17T00:20:32.832Z","args":{"path":"crates/spt-daemon/tests/attach.rs;crates/spt-daemon/tests/brain_resume_conn_deadlock.rs;crates/spt-daemon/tests/conn_blackhole_lifecycle.rs;crates/spt/tests/attach_wedge_e2e.rs;crates/spt/tests/resume…"},"intent":"Finding refresh resume tests"},"id":"1edd6a17","parentId":"2bdc7996","timestamp":"2026-07-17T00:20:32.832Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_7gebgj0apJLf0Bo0bzDlYOOO|fc_0996e78cf50e3160016a59754ffd708199bc1b9be53f3ad7a0","toolName":"read","startedAt":"2026-07-17T00:20:32.832Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading old brain promotion boundary"},"id":"6cf8f1fb","parentId":"1edd6a17","timestamp":"2026-07-17T00:20:32.832Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_9dCqkcf6TlbntaItvbaD2S8j|fc_0996e78cf50e3160016a59754ffd7c8199a13724f19e498710","toolName":"read","startedAt":"2026-07-17T00:20:32.832Z","args":{"path":"crates/spt-daemon/src/conn.rs"},"intent":"Reading connection poison mechanism"},"id":"7f9fc6b6","parentId":"6cf8f1fb","timestamp":"2026-07-17T00:20:32.832Z"}
{"type":"message","id":"1ec3f8bb","parentId":"7f9fc6b6","timestamp":"2026-07-17T00:20:32.842Z","message":{"role":"toolResult","toolCallId":"call_9dCqkcf6TlbntaItvbaD2S8j|fc_0996e78cf50e3160016a59754ffd7c8199a13724f19e498710","toolName":"read","content":[{"type":"text","text":"//! Bounded, cancelable, poison-on-failure framed writes for one physical\n//! broker connection (UPDATE-WEDGE round-4).\n//!\n//! ## The failure class this kills\n//!\n//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard\n//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes\n//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\n//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\n//! A connected consumer that stops reading (an `rc --take` whose process\n//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n//! logical stall-evict (`stall_evict_controller`) releases only the controller\n//! ROLE — it never cancels the in-flight pipe write or closes the physical\n//! connection, so the writer (and every write queued behind the same gate)\n//! stays wedged until the peer process dies (~128 s in the field capture, on a\n//! brain restart).\n//!\n//! ## The primitive\n//!\n//! [`BrokerConn`] owns the send half outright — it never leaves this module,\n//! so an unbounded bypass write cannot compile. Every write:\n//!\n//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE\n//!    deadline stamped at write entry covers BOTH the gate-wait and the OS\n//!    write);\n//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute\n//!    deadline, then performs the blocking [`write_frame`] OUT of every lock;\n//! 3. on success, returns the half to the gate.\n//!\n//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\n//! write returning, nor on opportunistic stall-evict sampling: it poisons the\n//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\n//! completion APC makes the wedged `SleepEx` loop return with\n//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\n//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\n//! the same abort (the stuck op it waited behind is the wedge).\n//!\n//! ## Poison / retire invariant (both OSes)\n//!\n//! On deadline, write failure, or cancellation: (1) the WHOLE physical\n//! connection is poisoned, idempotently — a length-prefixed frame may have\n//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\n//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\n//! (3) the buffer of the canceled op is released only after the op reports\n//! completion (`write_frame` returns only after the completion APC — the\n//! synchronous return IS the completion handshake); (4) the retired half is\n//! parked in a graveyard slot — never dropped while the watchdog might still\n//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\n//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\n//! pipe is then severed (`DisconnectNamedPipe` — the server end forces the\n//! client off, so the old client's next access fails, a re-issued server read\n//! fails, and the `interprocess` drop-limbo flush cannot block on unread\n//! data), followed by one more `CancelIoEx` covering a read re-issued in the\n//! cancel→disconnect window.\n//!\n//! No new output queue (the bounded per-sink queues + isolated writer threads\n//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\n//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\n//! healthy connections; only the poison/retire path is new.\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n\nuse std::io;\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\nuse std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};\nuse std::thread::{self, JoinHandle};\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n\nuse interprocess::local_socket::SendHalf;\n\nuse crate::codec::write_frame;\nuse crate::frame::Envelope;\n\n/// Stable per-physical-conn identity mint — a monotonic u64 stamped at\n/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\nstatic CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n\n/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n/// startup ([`init_log_anchor`], both broker and brain entries) so every\n/// record in one process shares one base; lazily initialized on first stamp\n/// otherwise (tests, in-process brokers) — correlation needs a COMMON base,\n/// not an absolute one.\nstatic MONO_ANCHOR: OnceLock<Instant> = OnceLock::new();\n\n/// Anchor the monotonic half of [`log_stamp`] at process start — called first\n/// thing by both daemon entries, next to `scrub_identity_env`.\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\npub(crate) fn init_log_anchor() {\n    let _ = MONO_ANCHOR.set(Instant::now());\n}\n\n/// Wall-clock + monotonic stamp for daemon-stderr correlation records\n/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\n/// stderr has no per-line timestamps and broker+brain share one file — without\n/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\n/// to field incident times (wall).\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\npub(crate) fn log_stamp() -> String {\n    let wall_ms = SystemTime::now()\n        .duration_since(UNIX_EPOCH)\n        .map(|d| d.as_millis())\n        .unwrap_or(0);\n    let mono_ms = MONO_ANCHOR.get_or_init(Instant::now).elapsed().as_millis();\n    format!(\"wall_ms={wall_ms} mono_ms={mono_ms}\")\n}\n\n/// Attribution-label cap: facts accrete per role attach (a long-lived brain\n/// conn can be controller + viewer + presence subscriber over its life), and\n/// the label must stay BOUNDED — one truncation marker, then no further growth.\nconst LABEL_CAP: usize = 240;\n\n/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\n#[derive(Debug, PartialEq, Eq)]\npub(crate) enum ConnWrite {\n    /// The frame was fully written.\n    Done,\n    /// The pre-write gate closure declined (e.g. a superseded controller\n    /// writer's epoch check) — nothing was written; the conn stays healthy.\n    Superseded,\n}\n\n/// The serialized write gate. `half: Some` = free; `None` = a write is in\n/// flight (checked out) or the conn has retired (then `poisoned` is set).\nstruct Gate {\n    half: Option<SendHalf>,\n}\n\n/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\nstruct Dog {\n    /// `Some((op_id, deadline))` while a write is in flight.\n    inflight: Option<(u64, Instant)>,\n    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n    shutdown: bool,\n}\n\n/// The state shared with the watchdog thread (no `Arc<BrokerConn>` cycle —\n/// the watchdog must not keep the conn alive).\nstruct Inner {\n    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n    id: u64,\n    /// Accreted attribution facts (subscriber role, endpoint/session where\n    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n    label: Mutex<String>,\n    /// Latch: the once-per-conn `write-start` lifecycle record fired.\n    first_write_logged: AtomicBool,\n    gate: Mutex<Gate>,\n    gate_cv: Condvar,\n    dog: Mutex<Dog>,\n    dog_cv: Condvar,\n    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\n    /// conn refuses every future write and is never re-gated.\n    poisoned: AtomicBool,\n    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\n    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\n    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\n    /// count the wedge (the W2 stall-evict tally) without conflating it with\n    /// an ordinary disconnect.\n    timed_out: AtomicBool,\n    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\n    severed: AtomicBool,\n    /// A retired (poisoned) half parks here so its OS handle stays alive\n    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\n    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\n    graveyard: Mutex<Option<SendHalf>>,\n    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\n    op_seq: AtomicU64,\n    /// The absolute per-write bound (gate-wait + OS write completion).\n    bound: Duration,\n    /// The raw OS handle/fd of the underlying stream, captured at\n    /// construction for the out-of-band abort. The recv and send halves share\n    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\n    /// aborting this handle covers a parked read too. Valid for the whole\n    /// conn lifetime: the half lives in `gate`, a writer's stack, or\n    /// `graveyard` until drop.\n    #[cfg(windows)]\n    raw: isize,\n    #[cfg(unix)]\n…\n}\n…\nimpl Inner {\n…\n    fn lifecycle(&self, event: &str, extra: &str) {\n        let sep = if extra.is_empty() { \"\" } else { \" \" };\n        eprintln!(\n            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\n            self.attribution()\n        );\n    }\n\n    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\n    /// the halves share one OS object) and latch the poison. Idempotent and\n    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\n    /// no-op, and re-cancel closes the race where a write was issued a beat\n    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\n    /// `cause` (the organic caller's write error) rides the retirement record\n    /// when the retirement is NOT deadline-driven.\n    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\n        if !self.poisoned.swap(true, Ordering::AcqRel) {\n            // First poison of this conn: loud, once. The token is class-split\n            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n            eprintln!(\n                \"{}\",\n                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n            );\n        }\n        #[cfg(windows)]\n        // SAFETY: scalar kernel32 call on a handle whose owning half is kept\n        // alive until Drop (gate / writer stack / graveyard — see `raw`).\n        unsafe {\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\n        }\n        #[cfg(unix)]\n        // SAFETY: scalar shutdown(2) on an fd alive until Drop. SHUT_RDWR\n        // fails the blocked write (EPIPE), EOFs the parked read, and fails\n        // every future op — on Unix this one call IS the whole teardown.\n        unsafe {\n            let _ = libc::shutdown(self.raw, libc::SHUT_RDWR);\n        }\n        // Wake gate-waiters so they observe the poison instead of sleeping\n        // out their full deadline, and the watchdog so it re-evaluates.\n        self.gate_cv.notify_all();\n        self.dog_cv.notify_all();\n    }\n\n    /// Sever the physical connection (exactly once), AFTER the canceled op\n    /// reported completion: on Windows `DisconnectNamedPipe` forces the\n    /// client off (its next access fails), makes a re-issued server-side\n    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\n    /// new read in after the cancel), and defuses the drop-limbo flush; the\n    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\n    /// `shutdown` in [`poison_and_cancel`] already did all of this.\n    // On Unix the cfg(windows) teardown strips out and the idempotence\n    // guard's early `return` becomes the function tail — needless_return\n    // there, load-bearing here. Restructuring (e.g. `if !swapped { … }`)\n    // would leave Unix an empty if-body (`clippy::needless_if`) instead.\n    #[cfg_attr(unix, allow(clippy::needless_return))]\n    fn sever(&self) {\n        if self.severed.swap(true, Ordering::AcqRel) {\n            return;\n        }\n        #[cfg(windows)]\n        // SAFETY: as in poison_and_cancel — handle outlives the watchdog.\n        unsafe {\n            let _ = win::DisconnectNamedPipe(self.raw);\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\n        }\n    }\n\n    /// The watchdog body: park until a write is in flight, then sleep to its\n    /// absolute deadline. If the op is still in flight at the deadline,\n    /// poison + cancel, re-canceling periodically until the op reports\n    /// completion (the writer clears `inflight` the instant `write_frame`\n    /// returns — that return IS the buffer-release handshake), then sever.\n    fn watchdog(&self) {\n        let mut d = recover(&self.dog);\n        loop {\n            // Park until armed (or shutdown).\n            let (op, deadline) = loop {\n                if d.shutdown {\n                    return;\n                }\n                if let Some(armed) = d.inflight {\n                    break armed;\n                }\n                d = self\n                    .dog_cv\n                    .wait(d)\n                    .unwrap_or_else(|p| p.into_inner());\n            };\n            // Sleep toward the deadline while THIS op stays in flight.\n            let fired = loop {\n                if d.shutdown {\n                    return;\n                }\n                match d.inflight {\n                    Some((cur, _)) if cur == op => {}\n                    // Completed (or a newer op armed) before the deadline.\n                    _ => break false,\n                }\n                let now = Instant::now();\n                if now >= deadline {\n                    break true;\n                }\n                let (g, _) = self\n                    .dog_cv\n                    .wait_timeout(d, deadline - now)\n                    .unwrap_or_else(|p| p.into_inner());\n                d = g;\n            };\n            if !fired {\n                continue;\n            }\n            // Deadline: abort out of band, then wait for the canceled op to\n            // report completion before severing (re-cancel each slice — a\n            // cancel that raced the WriteFileEx issuance reaches nothing, so\n            // one shot is not enough). Latch timed_out FIRST so the aborted\n            // writer observes its failure as a DEADLINE retirement (wedge),\n            // not an organic conn death.\n            drop(d);\n            self.timed_out.store(true, Ordering::Release);\n            self.poison_and_cancel(None);\n            d = recover(&self.dog);\n            while matches!(d.inflight, Some((cur, _)) if cur == op) && !d.shutdown {\n                let (g, _) = self\n                    .dog_cv\n                    .wait_timeout(d, Duration::from_millis(100))\n                    .unwrap_or_else(|p| p.into_inner());\n                d = g;\n                drop(d);\n                self.poison_and_cancel(None);\n                d = recover(&self.dog);\n            }\n            if d.shutdown {\n                return;\n            }\n            drop(d);\n            self.sever();\n            d = recover(&self.dog);\n        }\n    }\n}\n\n/// The shared, serialized, BOUNDED send side of one physical broker\n/// connection — what [`crate::broker::SharedSend`] now aliases. Owns the\n/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\npub(crate) struct BrokerConn {\n    inner: Arc<Inner>,\n    /// Joined on drop — after `shutdown` is raised — so the raw handle the\n    /// watchdog aborts through strictly outlives every raw use.\n    dog: Option<JoinHandle<()>>,\n}\n\nimpl BrokerConn {\n    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\n    /// deadline (gate-wait + OS write completion) — the broker passes its\n    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\n        let raw = raw_of(&half);\n        let inner = Arc::new(Inner {\n            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n            label: Mutex::new(String::new()),\n            first_write_logged: AtomicBool::new(false),\n            gate: Mutex::new(Gate { half: Some(half) }),\n            gate_cv: Condvar::new(),\n            dog: Mutex::new(Dog {\n                inflight: None,\n                shutdown: false,\n            }),\n            dog_cv: Condvar::new(),\n            poisoned: AtomicBool::new(false),\n            timed_out: AtomicBool::new(false),\n            severed: AtomicBool::new(false),\n            graveyard: Mutex::new(None),\n            op_seq: AtomicU64::new(0),\n            bound,\n            raw,\n        });\n        let dog_inner = Arc::clone(&inner);\n        let dog = thread::Builder::new()\n            .name(\"conn-watchdog\".into())\n            .spawn(move || dog_inner.watchdog())\n            .ok();\n        BrokerConn { inner, dog }\n    }\n\n    /// The stable per-physical-conn id — what lifecycle records key on\n    /// (`Arc::ptr_eq` identity does not survive a log line).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn id(&self) -> u64 {\n        self.inner.id\n    }\n\n    /// Accrete an attribution fact onto this conn's label (subscriber role,\n    /// endpoint/session where known — e.g. `role=brain`,\n    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn describe(&self, fact: &str) {\n        let mut l = recover(&self.inner.label);\n        if l.ends_with('…') {\n            return;\n        }\n        if l.len() + fact.len() + 1 > LABEL_CAP {\n            l.push('…');\n            return;\n        }\n        if !l.is_empty() {\n            l.push(' ');\n        }\n        l.push_str(fact);\n    }\n\n    /// Emit a bounded per-conn lifecycle record through this conn's shared\n    /// attribution format — the broker's sink machinery uses it for\n    /// `writer-exit`, attach/replace, and detach events (leg d).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\n        self.inner.lifecycle(event, extra);\n    }\n\n    /// Bounded framed write. See [`Self::write_gated`].\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n        self.write_gated(env, || true)\n    }\n\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\n    /// serialized gate is acquired and BEFORE any byte is written — the same\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\n    /// superseded controller writer still bails atomically with respect to\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\n    pub(crate) fn write_gated(\n        &self,\n        env: &Envelope,\n        pre: impl FnOnce() -> bool,\n    ) -> io::Result<ConnWrite> {\n        let inner = &*self.inner;\n        let deadline = Instant::now() + inner.bound;\n\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\n        let mut g = recover(&inner.gate);\n        loop {\n            if inner.poisoned.load(Ordering::Acquire) {\n                return Err(poisoned_err());\n            }\n            if g.half.is_some() {\n                break;\n            }\n            let now = Instant::now();\n            if now >= deadline {\n                // The write we queued behind is stuck past OUR whole bound:\n                // that op is the wedge. Abort it out of band and fail — the\n                // watchdog's own deadline (armed at that op's entry) is also\n                // in flight; both paths converge on the same idempotent\n                // poison/cancel.\n                drop(g);\n                inner.timed_out.store(true, Ordering::Release);\n                inner.poison_and_cancel(None);\n                return Err(timeout_err());\n            }\n            let (g2, _) = inner\n                .gate_cv\n                .wait_timeout(g, deadline - now)\n                .unwrap_or_else(|p| p.into_inner());\n            g = g2;\n        }\n\n        if !pre() {\n            return Ok(ConnWrite::Superseded);\n        }\n\n        // Once-per-conn `write-start` lifecycle record (leg d): marks the conn's\n        // first committed write, so a conn that later retires can be correlated\n        // to when it went active (fresh-carrier churn shows as open→start→retire\n        // triplets with fresh ids).\n        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\n            inner.lifecycle(\"write-start\", \"\");\n        }\n\n        // ── Check out the half + arm the watchdog, then write OUT of locks. ──\n        let mut half = g.half.take().expect(\"gate half present at checkout\");\n        let op = inner.op_seq.fetch_add(1, Ordering::Relaxed) + 1;\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = Some((op, deadline));\n            inner.dog_cv.notify_all();\n        }\n        drop(g);\n\n        let res = write_frame(&mut half, env);\n\n        // Completion handshake: `write_frame` has returned, so the OS op —\n        // canceled or not — reported completion and the frame buffer is\n        // released. Clear the arm BEFORE deciding health, so the watchdog\n        // never severs while the op is formally in flight.\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = None;\n            inner.dog_cv.notify_all();\n        }\n\n        let mut g = recover(&inner.gate);\n        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\n            g.half = Some(half);\n            drop(g);\n            inner.gate_cv.notify_all();\n            return Ok(ConnWrite::Done);\n        }\n\n        // ── Retire: poison the whole conn, abort the read side so\n        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\n        // graveyard (its handle must outlive any late raw abort; never\n        // re-gated, never reused — a timed-out length-prefixed frame may be\n        // partially written). ──\n        drop(g);\n        inner.poison_and_cancel(res.as_ref().err());\n        inner.sever();\n        *recover(&inner.graveyard) = Some(half);\n        inner.gate_cv.notify_all();\n        // A DEADLINE retirement (the watchdog aborted OUR op, or the conn was\n        // already deadline-poisoned) surfaces as TimedOut — the caller can\n        // count the wedge (W2 stall-evict tally). An organic I/O failure\n        // (peer death) keeps its own error.\n        if inner.timed_out.load(Ordering::Acquire) {\n            return Err(timeout_err());\n        }\n        Err(match res {\n            Err(e) => e,\n            Ok(()) => poisoned_err(),\n        })\n    }\n}\n\n/// TEST-ONLY determinism lever: holds the serialized write gate (the half is\n/// checked out) so every writer parks AT THE GATE until the pin drops — the\n/// round-4 analog of pinning the old `Mutex<SendHalf>` across a registration\n/// window (the P1c epoch-gate keystone unit relies on it).\n#[cfg(test)]\npub(crate) struct TestGatePin {\n    inner: Arc<Inner>,\n    half: Option<SendHalf>,\n}\n\n#[cfg(test)]\nimpl Drop for TestGatePin {\n    fn drop(&mut self) {\n        let mut g = recover(&self.inner.gate);\n        g.half = self.half.take();\n        drop(g);\n        self.inner.gate_cv.notify_all();\n    }\n}\n\n#[cfg(test)]\nimpl BrokerConn {\n    /// Check the half out of the gate without arming the watchdog — writers\n    /// queue behind the gate (bounded by their own deadlines) until the\n    /// returned pin drops. Test-only; never a production path.\n    pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {\n        let mut g = recover(&self.inner.gate);\n        let half = g.half.take().expect(\"pin: gate half present\");\n        TestGatePin {\n            inner: Arc::clone(&self.inner),\n            half: Some(half),\n        }\n    }\n}\n\nimpl Drop for BrokerConn {\n    fn drop(&mut self) {\n        // Once-per-conn `transport-close` lifecycle record (leg d): the handle\n        // is about to close — the terminal record of this conn id's life, with\n        // how it ended (organic EOF vs poisoned vs deadline-poisoned).\n        self.inner.lifecycle(\n            \"transport-close\",\n            &format!(\n                \"poisoned={} timed_out={}\",\n                self.inner.poisoned.load(Ordering::Acquire),\n                self.inner.timed_out.load(Ordering::Acquire)\n            ),\n        );\n…\n    }\n}"}],"details":{"displayContent":{"text":"//! Bounded, cancelable, poison-on-failure framed writes for one physical\n//! broker connection (UPDATE-WEDGE round-4).\n//!\n//! ## The failure class this kills\n//!\n//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard\n//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes\n//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\n//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\n//! A connected consumer that stops reading (an `rc --take` whose process\n//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n//! logical stall-evict (`stall_evict_controller`) releases only the controller\n//! ROLE — it never cancels the in-flight pipe write or closes the physical\n//! connection, so the writer (and every write queued behind the same gate)\n//! stays wedged until the peer process dies (~128 s in the field capture, on a\n//! brain restart).\n//!\n//! ## The primitive\n//!\n//! [`BrokerConn`] owns the send half outright — it never leaves this module,\n//! so an unbounded bypass write cannot compile. Every write:\n//!\n//! 1. acquires the serialized write GATE with a bounded wait (an ABSOLUTE\n//!    deadline stamped at write entry covers BOTH the gate-wait and the OS\n//!    write);\n//! 2. arms an INDEPENDENT per-connection watchdog thread with that absolute\n//!    deadline, then performs the blocking [`write_frame`] OUT of every lock;\n//! 3. on success, returns the half to the gate.\n//!\n//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\n//! write returning, nor on opportunistic stall-evict sampling: it poisons the\n//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\n//! completion APC makes the wedged `SleepEx` loop return with\n//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\n//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\n//! the same abort (the stuck op it waited behind is the wedge).\n//!\n//! ## Poison / retire invariant (both OSes)\n//!\n//! On deadline, write failure, or cancellation: (1) the WHOLE physical\n//! connection is poisoned, idempotently — a length-prefixed frame may have\n//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\n//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\n//! (3) the buffer of the canceled op is released only after the op reports\n//! completion (`write_frame` returns only after the completion APC — the\n//! synchronous return IS the completion handshake); (4) the retired half is\n//! parked in a graveyard slot — never dropped while the watchdog might still\n//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\n//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\n//! pipe is then severed (`DisconnectNamedPipe` — the server end forces the\n//! client off, so the old client's next access fails, a re-issued server read\n//! fails, and the `interprocess` drop-limbo flush cannot block on unread\n//! data), followed by one more `CancelIoEx` covering a read re-issued in the\n//! cancel→disconnect window.\n//!\n//! No new output queue (the bounded per-sink queues + isolated writer threads\n//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\n//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\n//! healthy connections; only the poison/retire path is new.\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n\nuse std::io;\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\nuse std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};\nuse std::thread::{self, JoinHandle};\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n\nuse interprocess::local_socket::SendHalf;\n\nuse crate::codec::write_frame;\nuse crate::frame::Envelope;\n\n/// Stable per-physical-conn identity mint — a monotonic u64 stamped at\n/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\nstatic CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n\n/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n/// startup ([`init_log_anchor`], both broker and brain entries) so every\n/// record in one process shares one base; lazily initialized on first stamp\n/// otherwise (tests, in-process brokers) — correlation needs a COMMON base,\n/// not an absolute one.\nstatic MONO_ANCHOR: OnceLock<Instant> = OnceLock::new();\n\n/// Anchor the monotonic half of [`log_stamp`] at process start — called first\n/// thing by both daemon entries, next to `scrub_identity_env`.\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\npub(crate) fn init_log_anchor() {\n    let _ = MONO_ANCHOR.set(Instant::now());\n}\n\n/// Wall-clock + monotonic stamp for daemon-stderr correlation records\n/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\n/// stderr has no per-line timestamps and broker+brain share one file — without\n/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\n/// to field incident times (wall).\n// [impl->REQ-CONN-POISON-ATTRIBUTION]\npub(crate) fn log_stamp() -> String {\n    let wall_ms = SystemTime::now()\n        .duration_since(UNIX_EPOCH)\n        .map(|d| d.as_millis())\n        .unwrap_or(0);\n    let mono_ms = MONO_ANCHOR.get_or_init(Instant::now).elapsed().as_millis();\n    format!(\"wall_ms={wall_ms} mono_ms={mono_ms}\")\n}\n\n/// Attribution-label cap: facts accrete per role attach (a long-lived brain\n/// conn can be controller + viewer + presence subscriber over its life), and\n/// the label must stay BOUNDED — one truncation marker, then no further growth.\nconst LABEL_CAP: usize = 240;\n\n/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\n#[derive(Debug, PartialEq, Eq)]\npub(crate) enum ConnWrite {\n    /// The frame was fully written.\n    Done,\n    /// The pre-write gate closure declined (e.g. a superseded controller\n    /// writer's epoch check) — nothing was written; the conn stays healthy.\n    Superseded,\n}\n\n/// The serialized write gate. `half: Some` = free; `None` = a write is in\n/// flight (checked out) or the conn has retired (then `poisoned` is set).\nstruct Gate {\n    half: Option<SendHalf>,\n}\n\n/// Watchdog bookkeeping: the currently in-flight op and its ABSOLUTE deadline.\nstruct Dog {\n    /// `Some((op_id, deadline))` while a write is in flight.\n    inflight: Option<(u64, Instant)>,\n    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n    shutdown: bool,\n}\n\n/// The state shared with the watchdog thread (no `Arc<BrokerConn>` cycle —\n/// the watchdog must not keep the conn alive).\nstruct Inner {\n    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n    id: u64,\n    /// Accreted attribution facts (subscriber role, endpoint/session where\n    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n    label: Mutex<String>,\n    /// Latch: the once-per-conn `write-start` lifecycle record fired.\n    first_write_logged: AtomicBool,\n    gate: Mutex<Gate>,\n    gate_cv: Condvar,\n    dog: Mutex<Dog>,\n    dog_cv: Condvar,\n    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\n    /// conn refuses every future write and is never re-gated.\n    poisoned: AtomicBool,\n    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\n    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\n    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\n    /// count the wedge (the W2 stall-evict tally) without conflating it with\n    /// an ordinary disconnect.\n    timed_out: AtomicBool,\n    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\n    severed: AtomicBool,\n    /// A retired (poisoned) half parks here so its OS handle stays alive\n    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\n    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\n    graveyard: Mutex<Option<SendHalf>>,\n    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\n    op_seq: AtomicU64,\n    /// The absolute per-write bound (gate-wait + OS write completion).\n    bound: Duration,\n    /// The raw OS handle/fd of the underlying stream, captured at\n    /// construction for the out-of-band abort. The recv and send halves share\n    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\n    /// aborting this handle covers a parked read too. Valid for the whole\n    /// conn lifetime: the half lives in `gate`, a writer's stack, or\n    /// `graveyard` until drop.\n    #[cfg(windows)]\n    raw: isize,\n    #[cfg(unix)]\n…\n}\n…\nimpl Inner {\n…\n    fn lifecycle(&self, event: &str, extra: &str) {\n        let sep = if extra.is_empty() { \"\" } else { \" \" };\n        eprintln!(\n            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\n            self.attribution()\n        );\n    }\n\n    /// Abort the connection's in-flight I/O OUT OF BAND (both directions —\n    /// the halves share one OS object) and latch the poison. Idempotent and\n    /// safe to repeat: `CancelIoEx` on a handle with nothing pending is a\n    /// no-op, and re-cancel closes the race where a write was issued a beat\n    /// AFTER a cancel (cancellation only reaches ALREADY-issued ops).\n    /// `cause` (the organic caller's write error) rides the retirement record\n    /// when the retirement is NOT deadline-driven.\n    fn poison_and_cancel(&self, cause: Option<&io::Error>) {\n        if !self.poisoned.swap(true, Ordering::AcqRel) {\n            // First poison of this conn: loud, once. The token is class-split\n            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n            eprintln!(\n                \"{}\",\n                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n            );\n        }\n        #[cfg(windows)]\n        // SAFETY: scalar kernel32 call on a handle whose owning half is kept\n        // alive until Drop (gate / writer stack / graveyard — see `raw`).\n        unsafe {\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\n        }\n        #[cfg(unix)]\n        // SAFETY: scalar shutdown(2) on an fd alive until Drop. SHUT_RDWR\n        // fails the blocked write (EPIPE), EOFs the parked read, and fails\n        // every future op — on Unix this one call IS the whole teardown.\n        unsafe {\n            let _ = libc::shutdown(self.raw, libc::SHUT_RDWR);\n        }\n        // Wake gate-waiters so they observe the poison instead of sleeping\n        // out their full deadline, and the watchdog so it re-evaluates.\n        self.gate_cv.notify_all();\n        self.dog_cv.notify_all();\n    }\n\n    /// Sever the physical connection (exactly once), AFTER the canceled op\n    /// reported completion: on Windows `DisconnectNamedPipe` forces the\n    /// client off (its next access fails), makes a re-issued server-side\n    /// read fail (so `handle_conn` reaches EOF cleanup even if it slipped a\n    /// new read in after the cancel), and defuses the drop-limbo flush; the\n    /// trailing re-cancel covers exactly that slipped-in read. On Unix the\n    /// `shutdown` in [`poison_and_cancel`] already did all of this.\n    // On Unix the cfg(windows) teardown strips out and the idempotence\n    // guard's early `return` becomes the function tail — needless_return\n    // there, load-bearing here. Restructuring (e.g. `if !swapped { … }`)\n    // would leave Unix an empty if-body (`clippy::needless_if`) instead.\n    #[cfg_attr(unix, allow(clippy::needless_return))]\n    fn sever(&self) {\n        if self.severed.swap(true, Ordering::AcqRel) {\n            return;\n        }\n        #[cfg(windows)]\n        // SAFETY: as in poison_and_cancel — handle outlives the watchdog.\n        unsafe {\n            let _ = win::DisconnectNamedPipe(self.raw);\n            let _ = win::CancelIoEx(self.raw, core::ptr::null_mut());\n        }\n    }\n\n    /// The watchdog body: park until a write is in flight, then sleep to its\n    /// absolute deadline. If the op is still in flight at the deadline,\n    /// poison + cancel, re-canceling periodically until the op reports\n    /// completion (the writer clears `inflight` the instant `write_frame`\n    /// returns — that return IS the buffer-release handshake), then sever.\n    fn watchdog(&self) {\n        let mut d = recover(&self.dog);\n        loop {\n            // Park until armed (or shutdown).\n            let (op, deadline) = loop {\n                if d.shutdown {\n                    return;\n                }\n                if let Some(armed) = d.inflight {\n                    break armed;\n                }\n                d = self\n                    .dog_cv\n                    .wait(d)\n                    .unwrap_or_else(|p| p.into_inner());\n            };\n            // Sleep toward the deadline while THIS op stays in flight.\n            let fired = loop {\n                if d.shutdown {\n                    return;\n                }\n                match d.inflight {\n                    Some((cur, _)) if cur == op => {}\n                    // Completed (or a newer op armed) before the deadline.\n                    _ => break false,\n                }\n                let now = Instant::now();\n                if now >= deadline {\n                    break true;\n                }\n                let (g, _) = self\n                    .dog_cv\n                    .wait_timeout(d, deadline - now)\n                    .unwrap_or_else(|p| p.into_inner());\n                d = g;\n            };\n            if !fired {\n                continue;\n            }\n            // Deadline: abort out of band, then wait for the canceled op to\n            // report completion before severing (re-cancel each slice — a\n            // cancel that raced the WriteFileEx issuance reaches nothing, so\n            // one shot is not enough). Latch timed_out FIRST so the aborted\n            // writer observes its failure as a DEADLINE retirement (wedge),\n            // not an organic conn death.\n            drop(d);\n            self.timed_out.store(true, Ordering::Release);\n            self.poison_and_cancel(None);\n            d = recover(&self.dog);\n            while matches!(d.inflight, Some((cur, _)) if cur == op) && !d.shutdown {\n                let (g, _) = self\n                    .dog_cv\n                    .wait_timeout(d, Duration::from_millis(100))\n                    .unwrap_or_else(|p| p.into_inner());\n                d = g;\n                drop(d);\n                self.poison_and_cancel(None);\n                d = recover(&self.dog);\n            }\n            if d.shutdown {\n                return;\n            }\n            drop(d);\n            self.sever();\n            d = recover(&self.dog);\n        }\n    }\n}\n\n/// The shared, serialized, BOUNDED send side of one physical broker\n/// connection — what [`crate::broker::SharedSend`] now aliases. Owns the\n/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\npub(crate) struct BrokerConn {\n    inner: Arc<Inner>,\n    /// Joined on drop — after `shutdown` is raised — so the raw handle the\n    /// watchdog aborts through strictly outlives every raw use.\n    dog: Option<JoinHandle<()>>,\n}\n\nimpl BrokerConn {\n    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\n    /// deadline (gate-wait + OS write completion) — the broker passes its\n    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\n        let raw = raw_of(&half);\n        let inner = Arc::new(Inner {\n            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n            label: Mutex::new(String::new()),\n            first_write_logged: AtomicBool::new(false),\n            gate: Mutex::new(Gate { half: Some(half) }),\n            gate_cv: Condvar::new(),\n            dog: Mutex::new(Dog {\n                inflight: None,\n                shutdown: false,\n            }),\n            dog_cv: Condvar::new(),\n            poisoned: AtomicBool::new(false),\n            timed_out: AtomicBool::new(false),\n            severed: AtomicBool::new(false),\n            graveyard: Mutex::new(None),\n            op_seq: AtomicU64::new(0),\n            bound,\n            raw,\n        });\n        let dog_inner = Arc::clone(&inner);\n        let dog = thread::Builder::new()\n            .name(\"conn-watchdog\".into())\n            .spawn(move || dog_inner.watchdog())\n            .ok();\n        BrokerConn { inner, dog }\n    }\n\n    /// The stable per-physical-conn id — what lifecycle records key on\n    /// (`Arc::ptr_eq` identity does not survive a log line).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn id(&self) -> u64 {\n        self.inner.id\n    }\n\n    /// Accrete an attribution fact onto this conn's label (subscriber role,\n    /// endpoint/session where known — e.g. `role=brain`,\n    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn describe(&self, fact: &str) {\n        let mut l = recover(&self.inner.label);\n        if l.ends_with('…') {\n            return;\n        }\n        if l.len() + fact.len() + 1 > LABEL_CAP {\n            l.push('…');\n            return;\n        }\n        if !l.is_empty() {\n            l.push(' ');\n        }\n        l.push_str(fact);\n    }\n\n    /// Emit a bounded per-conn lifecycle record through this conn's shared\n    /// attribution format — the broker's sink machinery uses it for\n    /// `writer-exit`, attach/replace, and detach events (leg d).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\n        self.inner.lifecycle(event, extra);\n    }\n\n    /// Bounded framed write. See [`Self::write_gated`].\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n        self.write_gated(env, || true)\n    }\n\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\n    /// serialized gate is acquired and BEFORE any byte is written — the same\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\n    /// superseded controller writer still bails atomically with respect to\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\n    pub(crate) fn write_gated(\n        &self,\n        env: &Envelope,\n        pre: impl FnOnce() -> bool,\n    ) -> io::Result<ConnWrite> {\n        let inner = &*self.inner;\n        let deadline = Instant::now() + inner.bound;\n\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\n        let mut g = recover(&inner.gate);\n        loop {\n            if inner.poisoned.load(Ordering::Acquire) {\n                return Err(poisoned_err());\n            }\n            if g.half.is_some() {\n                break;\n            }\n            let now = Instant::now();\n            if now >= deadline {\n                // The write we queued behind is stuck past OUR whole bound:\n                // that op is the wedge. Abort it out of band and fail — the\n                // watchdog's own deadline (armed at that op's entry) is also\n                // in flight; both paths converge on the same idempotent\n                // poison/cancel.\n                drop(g);\n                inner.timed_out.store(true, Ordering::Release);\n                inner.poison_and_cancel(None);\n                return Err(timeout_err());\n            }\n            let (g2, _) = inner\n                .gate_cv\n                .wait_timeout(g, deadline - now)\n                .unwrap_or_else(|p| p.into_inner());\n            g = g2;\n        }\n\n        if !pre() {\n            return Ok(ConnWrite::Superseded);\n        }\n\n        // Once-per-conn `write-start` lifecycle record (leg d): marks the conn's\n        // first committed write, so a conn that later retires can be correlated\n        // to when it went active (fresh-carrier churn shows as open→start→retire\n        // triplets with fresh ids).\n        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\n            inner.lifecycle(\"write-start\", \"\");\n        }\n\n        // ── Check out the half + arm the watchdog, then write OUT of locks. ──\n        let mut half = g.half.take().expect(\"gate half present at checkout\");\n        let op = inner.op_seq.fetch_add(1, Ordering::Relaxed) + 1;\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = Some((op, deadline));\n            inner.dog_cv.notify_all();\n        }\n        drop(g);\n\n        let res = write_frame(&mut half, env);\n\n        // Completion handshake: `write_frame` has returned, so the OS op —\n        // canceled or not — reported completion and the frame buffer is\n        // released. Clear the arm BEFORE deciding health, so the watchdog\n        // never severs while the op is formally in flight.\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = None;\n            inner.dog_cv.notify_all();\n        }\n\n        let mut g = recover(&inner.gate);\n        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\n            g.half = Some(half);\n            drop(g);\n            inner.gate_cv.notify_all();\n            return Ok(ConnWrite::Done);\n        }\n\n        // ── Retire: poison the whole conn, abort the read side so\n        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\n        // graveyard (its handle must outlive any late raw abort; never\n        // re-gated, never reused — a timed-out length-prefixed frame may be\n        // partially written). ──\n        drop(g);\n        inner.poison_and_cancel(res.as_ref().err());\n        inner.sever();\n        *recover(&inner.graveyard) = Some(half);\n        inner.gate_cv.notify_all();\n        // A DEADLINE retirement (the watchdog aborted OUR op, or the conn was\n        // already deadline-poisoned) surfaces as TimedOut — the caller can\n        // count the wedge (W2 stall-evict tally). An organic I/O failure\n        // (peer death) keeps its own error.\n        if inner.timed_out.load(Ordering::Acquire) {\n            return Err(timeout_err());\n        }\n        Err(match res {\n            Err(e) => e,\n            Ok(()) => poisoned_err(),\n        })\n    }\n}\n\n/// TEST-ONLY determinism lever: holds the serialized write gate (the half is\n/// checked out) so every writer parks AT THE GATE until the pin drops — the\n/// round-4 analog of pinning the old `Mutex<SendHalf>` across a registration\n/// window (the P1c epoch-gate keystone unit relies on it).\n#[cfg(test)]\npub(crate) struct TestGatePin {\n    inner: Arc<Inner>,\n    half: Option<SendHalf>,\n}\n\n#[cfg(test)]\nimpl Drop for TestGatePin {\n    fn drop(&mut self) {\n        let mut g = recover(&self.inner.gate);\n        g.half = self.half.take();\n        drop(g);\n        self.inner.gate_cv.notify_all();\n    }\n}\n\n#[cfg(test)]\nimpl BrokerConn {\n    /// Check the half out of the gate without arming the watchdog — writers\n    /// queue behind the gate (bounded by their own deadlines) until the\n    /// returned pin drops. Test-only; never a production path.\n    pub(crate) fn pin_gate_for_test(&self) -> TestGatePin {\n        let mut g = recover(&self.inner.gate);\n        let half = g.half.take().expect(\"pin: gate half present\");\n        TestGatePin {\n            inner: Arc::clone(&self.inner),\n            half: Some(half),\n        }\n    }\n}\n\nimpl Drop for BrokerConn {\n    fn drop(&mut self) {\n        // Once-per-conn `transport-close` lifecycle record (leg d): the handle\n        // is about to close — the terminal record of this conn id's life, with\n        // how it ended (organic EOF vs poisoned vs deadline-poisoned).\n        self.inner.lifecycle(\n            \"transport-close\",\n            &format!(\n                \"poisoned={} timed_out={}\",\n                self.inner.poisoned.load(Ordering::Acquire),\n                self.inner.timed_out.load(Ordering::Acquire)\n            ),\n        );\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,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,172,173,174,175,176,177,178,179,180,null,182,null,219,null,270,271,272,273,274,275,276,277,278,279,280,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,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,null,662,663]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\conn.rs"}}},"isError":false,"timestamp":1784247632842}}
{"type":"message","id":"03f0c82b","parentId":"1ec3f8bb","timestamp":"2026-07-17T00:20:32.881Z","message":{"role":"toolResult","toolCallId":"call_7gebgj0apJLf0Bo0bzDlYOOO|fc_0996e78cf50e3160016a59754ffd708199bc1b9be53f3ad7a0","toolName":"read","content":[{"type":"text","text":"impl BrainRestart {\n…\n    pub fn request(&self) {\n…\n    }\n\n    /// Consume a pending request (supervisor side): true at most once per\n    /// `request`, clearing it so the next cycle starts fresh.\n    pub fn take(&self) -> bool {\n        self.requested.swap(false, Ordering::SeqCst)\n    }\n}\n\n/// The `spt daemon brain` child entry. Connect to the broker over the existing\n/// socket IPC, signal readiness, host the brain-side logic loops, and idle —\n/// holding the connection and probing broker liveness each heartbeat. A dropped\n/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)\n/// returns `Err`, exiting the process so the broker-side supervisor respawns it\n/// from the (possibly swapped) binary.\n///\n/// **D2-1 — net consumers run here.** The inbound dispatcher + outbound peer\n/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no\n/// shared `Arc<Broker>`), so they belong with the restartable brain, not the\n/// broker — a brain respawn re-spawns them from the swapped binary. They are\n/// meaningful only once the broker-owned NetHost is up, so we gate on the\n/// existing `net-status` verb (which also serves as the broker-liveness probe)\n/// and start them exactly once, on the first `enabled` reply. That single gate\n/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the\n/// NetHost bind in the background and `net-status` flips `enabled` when it\n/// attaches, at which point the brain starts the consumers — no extra signal.\n///\n/// It inherits the broker's already-unelevated token as a plain child, so it\n/// runs no de-elevation guard of its own.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\n    let _ = crate::stderrlog::install(\"brain\", generation);\n    let name = broker_socket_name();\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n    // *counter* is rehydrated, never the wall-clock stamp.\n    eprintln!(\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\n        std::process::id(),\n        reason.as_arg(),\n    );\n\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\n    // from its delivered cursor, the production replacement for the retired\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    match brain.resume_session_cursors() {\n        Ok(ids) if !ids.is_empty() => {\n            eprintln!(\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n                ids.len()\n            )\n        }\n        Ok(_) => {}\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\n    }\n\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\n    // breadcrumb would accept a brain that connected but never rebuilt\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\n    // this is forward-correct: the gate is defined on the real readiness\n    // semantic the live-agent adapter inherits.\n    write_ready(generation);\n\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n    // online/offline exclusivity — a reconcile loop that runs each offline\n    // instance's `wake_command` as a supervised watcher child. Relocated from\n    // the broker process to the brain child: shellwake is brain-owned logic\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\n    // posture. The stop flag is held for symmetry, never raised (the watchers\n    // die with the brain process and the next brain re-sweeps them).\n    // [impl->REQ-SHELL-2]\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\n\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\n    // shell waker). Online-only is staged behind the interim `api listen`\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\n    // brain the first-host. The stop flag is held for symmetry; the threads die\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\n    // [impl->REQ-DAEMON-1]\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\n\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\n    // reconcile in the background (ready-without-warm is preserved because\n    // write_ready already fired above), then drain/debounce/coalesce the\n    // invalidation queue. Same posture as the hosts above: the stop flag is\n    // held for symmetry, the thread dies with the brain, and the next brain\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n\n    let mut consumers_started = false;\n    loop {\n        thread::sleep(BRAIN_HEARTBEAT);\n        // `net-status` is both the net-capability probe and the broker-liveness\n        // probe: an `Err` is the broker gone (or a future planned\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n        let status = match brain.net_status() {\n            Ok(s) => s,\n            Err(e) => {\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\n                return Err(e);\n            }\n        };\n        if consumer_gate(status.enabled, consumers_started) {\n            let hex = status.node_id_hex.clone().unwrap_or_default();\n            spawn_net_consumers(&hex);\n            consumers_started = true;\n            eprintln!(\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n            );\n        }\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\n        // window, so without this the wedge never drains: the promotion DRAINED gate\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n        // best-effort — a transient sessions error must never fell the heartbeat (the\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\n        // its own trial against a live older broker — no coordinated broker restart.\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n        let _ = brain.sessions();\n        write_ready(generation);\n    }\n}\n\n/// Whether to spawn the net consumers this heartbeat: only once the broker\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\n/// semantics that keep a single dispatcher/peer-pump set running.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\n    net_enabled && !already_started\n}\n\n/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\n/// (D9-1) — against the broker's socket. **Relocated from the broker process to\n/// the brain child (restoration D2-1):** both are standalone IPC clients\n/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\n/// restartable brain. Each mints its own registry + stop flag and connects\n/// independently; when the brain process exits these in-process threads die with\n/// it, and the respawned brain re-spawns them from the current binary. Mirrors\n/// the wiring the broker boot used to do, `node_hex` now sourced from the\n/// `net-status` reply rather than the broker's identity load.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n// [impl->REQ-DAEMON-9]\nfn spawn_net_consumers(node_hex: &str) {\n    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\n    let stop = Arc::new(AtomicBool::new(false));\n    let _ = crate::dispatch::spawn_dispatcher(\n        broker_socket_name(),\n        Arc::clone(&registry),\n        Arc::clone(&stop),\n    );\n    let cfg = crate::config::DaemonConfig::load();\n    if cfg.peer_pump {\n        let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);\n    }\n}\n\n[Showing lines 129-333 of 2116. Use :334 to continue]"}],"details":{"truncation":{"content":"    }\r\n\r\n    /// Consume a pending request (supervisor side): true at most once per\r\n    /// `request`, clearing it so the next cycle starts fresh.\r\n    pub fn take(&self) -> bool {\r\n        self.requested.swap(false, Ordering::SeqCst)\r\n    }\r\n}\r\n\r\n/// The `spt daemon brain` child entry. Connect to the broker over the existing\r\n/// socket IPC, signal readiness, host the brain-side logic loops, and idle —\r\n/// holding the connection and probing broker liveness each heartbeat. A dropped\r\n/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)\r\n/// returns `Err`, exiting the process so the broker-side supervisor respawns it\r\n/// from the (possibly swapped) binary.\r\n///\r\n/// **D2-1 — net consumers run here.** The inbound dispatcher + outbound peer\r\n/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no\r\n/// shared `Arc<Broker>`), so they belong with the restartable brain, not the\r\n/// broker — a brain respawn re-spawns them from the swapped binary. They are\r\n/// meaningful only once the broker-owned NetHost is up, so we gate on the\r\n/// existing `net-status` verb (which also serves as the broker-liveness probe)\r\n/// and start them exactly once, on the first `enabled` reply. That single gate\r\n/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the\r\n/// NetHost bind in the background and `net-status` flips `enabled` when it\r\n/// attaches, at which point the brain starts the consumers — no extra signal.\r\n///\r\n/// It inherits the broker's already-unelevated token as a plain child, so it\r\n/// runs no de-elevation guard of its own.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\r\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\r\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\r\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\r\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\r\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\r\n    let _ = crate::stderrlog::install(\"brain\", generation);\r\n    let name = broker_socket_name();\r\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\r\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\r\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\r\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\r\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\r\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\r\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\r\n    // *counter* is rehydrated, never the wall-clock stamp.\r\n    eprintln!(\r\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\r\n        std::process::id(),\r\n        reason.as_arg(),\r\n    );\r\n\r\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\r\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\r\n    // from its delivered cursor, the production replacement for the retired\r\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\r\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\r\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\r\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\r\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\r\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\r\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\r\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\r\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\r\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\r\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\r\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\r\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\r\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\r\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\r\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\r\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    match brain.resume_session_cursors() {\r\n        Ok(ids) if !ids.is_empty() => {\r\n            eprintln!(\r\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\r\n                ids.len()\r\n            )\r\n        }\r\n        Ok(_) => {}\r\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\r\n    }\r\n\r\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\r\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\r\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\r\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\r\n    // breadcrumb would accept a brain that connected but never rebuilt\r\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\r\n    // this is forward-correct: the gate is defined on the real readiness\r\n    // semantic the live-agent adapter inherits.\r\n    write_ready(generation);\r\n\r\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\r\n    // online/offline exclusivity — a reconcile loop that runs each offline\r\n    // instance's `wake_command` as a supervised watcher child. Relocated from\r\n    // the broker process to the brain child: shellwake is brain-owned logic\r\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\r\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\r\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\r\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\r\n    // posture. The stop flag is held for symmetry, never raised (the watchers\r\n    // die with the brain process and the next brain re-sweeps them).\r\n    // [impl->REQ-SHELL-2]\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\r\n\r\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\r\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\r\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\r\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\r\n    // shell waker). Online-only is staged behind the interim `api listen`\r\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\r\n    // brain the first-host. The stop flag is held for symmetry; the threads die\r\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\r\n    // [impl->REQ-DAEMON-1]\r\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\r\n\r\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\r\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\r\n    // reconcile in the background (ready-without-warm is preserved because\r\n    // write_ready already fired above), then drain/debounce/coalesce the\r\n    // invalidation queue. Same posture as the hosts above: the stop flag is\r\n    // held for symmetry, the thread dies with the brain, and the next brain\r\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\r\n\r\n    let mut consumers_started = false;\r\n    loop {\r\n        thread::sleep(BRAIN_HEARTBEAT);\r\n        // `net-status` is both the net-capability probe and the broker-liveness\r\n        // probe: an `Err` is the broker gone (or a future planned\r\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\r\n        let status = match brain.net_status() {\r\n            Ok(s) => s,\r\n            Err(e) => {\r\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\r\n                return Err(e);\r\n            }\r\n        };\r\n        if consumer_gate(status.enabled, consumers_started) {\r\n            let hex = status.node_id_hex.clone().unwrap_or_default();\r\n            spawn_net_consumers(&hex);\r\n            consumers_started = true;\r\n            eprintln!(\r\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\r\n            );\r\n        }\r\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\r\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\r\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\r\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\r\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\r\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\r\n        // window, so without this the wedge never drains: the promotion DRAINED gate\r\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\r\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\r\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\r\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\r\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\r\n        // best-effort — a transient sessions error must never fell the heartbeat (the\r\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\r\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\r\n        // its own trial against a live older broker — no coordinated broker restart.\r\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n        let _ = brain.sessions();\r\n        write_ready(generation);\r\n    }\r\n}\r\n\r\n/// Whether to spawn the net consumers this heartbeat: only once the broker\r\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\r\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\r\n/// semantics that keep a single dispatcher/peer-pump set running.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\r\n    net_enabled && !already_started\r\n}\r\n\r\n/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\r\n/// (D9-1) — against the broker's socket. **Relocated from the broker process to\r\n/// the brain child (restoration D2-1):** both are standalone IPC clients\r\n/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\r\n/// restartable brain. Each mints its own registry + stop flag and connects\r\n/// independently; when the brain process exits these in-process threads die with\r\n/// it, and the respawned brain re-spawns them from the current binary. Mirrors\r\n/// the wiring the broker boot used to do, `node_hex` now sourced from the\r\n/// `net-status` reply rather than the broker's identity load.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n// [impl->REQ-DAEMON-9]\r\nfn spawn_net_consumers(node_hex: &str) {\r\n    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\r\n    let stop = Arc::new(AtomicBool::new(false));\r\n    let _ = crate::dispatch::spawn_dispatcher(\r\n        broker_socket_name(),\r\n        Arc::clone(&registry),\r\n        Arc::clone(&stop),\r\n    );\r\n    let cfg = crate::config::DaemonConfig::load();\r\n    if cfg.peer_pump {\r\n        let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);\r\n    }\r\n}\r","truncated":true,"truncatedBy":"lines","totalLines":1988,"totalBytes":12399,"outputLines":205,"outputBytes":12399,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"impl BrainRestart {\n…\n    pub fn request(&self) {\n…\n    }\r\n\r\n    /// Consume a pending request (supervisor side): true at most once per\r\n    /// `request`, clearing it so the next cycle starts fresh.\r\n    pub fn take(&self) -> bool {\r\n        self.requested.swap(false, Ordering::SeqCst)\r\n    }\r\n}\r\n\r\n/// The `spt daemon brain` child entry. Connect to the broker over the existing\r\n/// socket IPC, signal readiness, host the brain-side logic loops, and idle —\r\n/// holding the connection and probing broker liveness each heartbeat. A dropped\r\n/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)\r\n/// returns `Err`, exiting the process so the broker-side supervisor respawns it\r\n/// from the (possibly swapped) binary.\r\n///\r\n/// **D2-1 — net consumers run here.** The inbound dispatcher + outbound peer\r\n/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no\r\n/// shared `Arc<Broker>`), so they belong with the restartable brain, not the\r\n/// broker — a brain respawn re-spawns them from the swapped binary. They are\r\n/// meaningful only once the broker-owned NetHost is up, so we gate on the\r\n/// existing `net-status` verb (which also serves as the broker-liveness probe)\r\n/// and start them exactly once, on the first `enabled` reply. That single gate\r\n/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the\r\n/// NetHost bind in the background and `net-status` flips `enabled` when it\r\n/// attaches, at which point the brain starts the consumers — no extra signal.\r\n///\r\n/// It inherits the broker's already-unelevated token as a plain child, so it\r\n/// runs no de-elevation guard of its own.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\r\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\r\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\r\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\r\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\r\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\r\n    let _ = crate::stderrlog::install(\"brain\", generation);\r\n    let name = broker_socket_name();\r\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\r\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\r\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\r\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\r\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\r\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\r\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\r\n    // *counter* is rehydrated, never the wall-clock stamp.\r\n    eprintln!(\r\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\r\n        std::process::id(),\r\n        reason.as_arg(),\r\n    );\r\n\r\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\r\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\r\n    // from its delivered cursor, the production replacement for the retired\r\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\r\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\r\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\r\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\r\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\r\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\r\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\r\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\r\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\r\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\r\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\r\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\r\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\r\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\r\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\r\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\r\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    match brain.resume_session_cursors() {\r\n        Ok(ids) if !ids.is_empty() => {\r\n            eprintln!(\r\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\r\n                ids.len()\r\n            )\r\n        }\r\n        Ok(_) => {}\r\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\r\n    }\r\n\r\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\r\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\r\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\r\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\r\n    // breadcrumb would accept a brain that connected but never rebuilt\r\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\r\n    // this is forward-correct: the gate is defined on the real readiness\r\n    // semantic the live-agent adapter inherits.\r\n    write_ready(generation);\r\n\r\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\r\n    // online/offline exclusivity — a reconcile loop that runs each offline\r\n    // instance's `wake_command` as a supervised watcher child. Relocated from\r\n    // the broker process to the brain child: shellwake is brain-owned logic\r\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\r\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\r\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\r\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\r\n    // posture. The stop flag is held for symmetry, never raised (the watchers\r\n    // die with the brain process and the next brain re-sweeps them).\r\n    // [impl->REQ-SHELL-2]\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\r\n\r\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\r\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\r\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\r\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\r\n    // shell waker). Online-only is staged behind the interim `api listen`\r\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\r\n    // brain the first-host. The stop flag is held for symmetry; the threads die\r\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\r\n    // [impl->REQ-DAEMON-1]\r\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\r\n\r\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\r\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\r\n    // reconcile in the background (ready-without-warm is preserved because\r\n    // write_ready already fired above), then drain/debounce/coalesce the\r\n    // invalidation queue. Same posture as the hosts above: the stop flag is\r\n    // held for symmetry, the thread dies with the brain, and the next brain\r\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\r\n\r\n    let mut consumers_started = false;\r\n    loop {\r\n        thread::sleep(BRAIN_HEARTBEAT);\r\n        // `net-status` is both the net-capability probe and the broker-liveness\r\n        // probe: an `Err` is the broker gone (or a future planned\r\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\r\n        let status = match brain.net_status() {\r\n            Ok(s) => s,\r\n            Err(e) => {\r\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\r\n                return Err(e);\r\n            }\r\n        };\r\n        if consumer_gate(status.enabled, consumers_started) {\r\n            let hex = status.node_id_hex.clone().unwrap_or_default();\r\n            spawn_net_consumers(&hex);\r\n            consumers_started = true;\r\n            eprintln!(\r\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\r\n            );\r\n        }\r\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\r\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\r\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\r\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\r\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\r\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\r\n        // window, so without this the wedge never drains: the promotion DRAINED gate\r\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\r\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\r\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\r\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\r\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\r\n        // best-effort — a transient sessions error must never fell the heartbeat (the\r\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\r\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\r\n        // its own trial against a live older broker — no coordinated broker restart.\r\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n        let _ = brain.sessions();\r\n        write_ready(generation);\r\n    }\r\n}\r\n\r\n/// Whether to spawn the net consumers this heartbeat: only once the broker\r\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\r\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\r\n/// semantics that keep a single dispatcher/peer-pump set running.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\r\n    net_enabled && !already_started\r\n}\r\n\r\n/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\r\n/// (D9-1) — against the broker's socket. **Relocated from the broker process to\r\n/// the brain child (restoration D2-1):** both are standalone IPC clients\r\n/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\r\n/// restartable brain. Each mints its own registry + stop flag and connects\r\n/// independently; when the brain process exits these in-process threads die with\r\n/// it, and the respawned brain re-spawns them from the current binary. Mirrors\r\n/// the wiring the broker boot used to do, `node_hex` now sourced from the\r\n/// `net-status` reply rather than the broker's identity load.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n// [impl->REQ-DAEMON-9]\r\nfn spawn_net_consumers(node_hex: &str) {\r\n    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\r\n    let stop = Arc::new(AtomicBool::new(false));\r\n    let _ = crate::dispatch::spawn_dispatcher(\r\n        broker_socket_name(),\r\n        Arc::clone(&registry),\r\n        Arc::clone(&stop),\r\n    );\r\n    let cfg = crate::config::DaemonConfig::load();\r\n    if cfg.peer_pump {\r\n        let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);\r\n    }\r\n}\r","startLine":119,"lineNumbers":[119,null,127,null,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,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2116,"totalBytes":12399,"outputLines":205,"outputBytes":12399,"shownRange":{"start":129,"end":333},"nextOffset":334}}},"isError":false,"timestamp":1784247632881}}
{"type":"message","id":"963e0d02","parentId":"03f0c82b","timestamp":"2026-07-17T00:20:32.893Z","message":{"role":"toolResult","toolCallId":"call_mK2Xl9C4eqTD8ZQojEdINgot|fc_0996e78cf50e3160016a59754ffc888199a5445ca6043a0fc6","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n\n## tests/\n### brain_decouple.rs\n*1|//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\n 2|//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\n 3|//!\n 4|//! ## The bug this gate locks down (the update-wedge root)\n...\n 6|//! A controller's writer thread does a BLOCKING socket write to its brain\n*7|//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\n*8|//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\n 9|//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\n*10|//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\n 11|//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\n 12|//! evicted by neither: control latched, reattach was refused, `rc --take` hung,\n 13|//! `daemon status` still read healthy.\n 14|//!\n*15|//! ## Faithful in-process analog of a suspended brain\n 16|//!\n*17|//! A test process cannot `NtSuspendProcess` itself, so the rig reproduces the exact\n 18|//! MECHANISM: controller A stops reading its socket. The flood child fills A's recv\n 19|//! buffer, the controller channel fills, and A's writer thread parks INSIDE\n*20|//! `write_frame` — precisely the state a suspended brain leaves its broker conn in\n 21|//! (a peer that stopped draining). This is the same analog the 7.12 wedge gate uses.\n 22|//! `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15 s production deadline so the\n 23|//! structural outcome is provable in ~1 s (the deadline knob grants wall-clock only;\n...\n 40|//! journal replay dedup, the `delivered_through` no-rewind CAS `advanced_cursor`, and\n*41|//! the clean-repaint cold-attach batch). A live `NtSuspendProcess` / `SIGSTOP` run on a\n 42|//! real brain child remains the MANUAL field rig from the incident night — this CI test\n 43|//! is the deterministic in-process ANALOG, not that run.\n 44|//!\n...\n 62|    SpawnReq, Spawned, StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_SPAWN,\n*63|    KIND_SPAWNED, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n 64|};\n 65|use spt_daemon::transport::{send_hello, LocalSocketTransport};\n 66|use spt_daemon::Broker;\n...\n 195|/// Query the broker's stall-evict tally.\n*196|fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 197|    let name = name.to_string();\n 198|    let (tx, rx) = std::sync::mpsc::channel();\n 199|    thread::spawn(move || {\n 200|        let mut c = connect(&name);\n*201|        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n 202|        loop {\n 203|            match read_frame(&mut c) {\n*204|                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n 205|                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n 206|                    let _ = tx.send(r.count);\n 207|                    return;\n...\n 221|#[test]\n*222|fn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\n 223|    init_home();\n 224|    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 225|    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\n...\n 252|    // ── Remote controller R: a REMOTE operator (by=Some) takes control of the\n*253|    //    (locally-spawned, undriven) session, then STOPS reading — the suspended-\n 254|    //    brain analog. R MUST be a remote controller: a second remote's Control\n 255|    //    subscribe against a DIFFERENT-remote-driven session busy-refuses PRE-W2\n 256|    //    (the wedge), so only the stall-evict can free the slot — this is what\n...\n 259|    //    read from) on its own thread so the broker's writer for R parks in\n*260|    //    write_frame exactly as against a suspended brain. ──\n 261|    let rname = name.clone();\n 262|    thread::spawn(move || {\n 263|        let mut r = connect(&rname);\n...\n 335|\n*336|    let evicts = stall_evict_count(&name, Duration::from_secs(5));\n 337|\n 338|    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\n 339|    if let Some(pid) = broker.session_pid(sid) {\n...\n 344|        \"=== W2 GATE: viewer_ticks_during_wedge={ticks_during_wedge} take_outcome={outcome:?} \\\n*345|         take_elapsed={took:?} stall_evicts={evicts} ===\"\n 346|    );\n 347|\n 348|    // The viewer kept ticking while the controller conn was wedged.\n...\n 383|// §Regression gate) proved that logical eviction does NOT retire the PHYSICAL\n*384|// connection: `stall_evict_controller` drops the `ControllerSink` (one\n 385|// `SharedSend` clone + the `JoinHandle`) but neither cancels the in-flight\n 386|// Windows named-pipe write, closes the socket, nor `CancelIoEx`'s it. The\n 387|// already-running `controller_writer` keeps its OWN `SharedSend` clone and stays\n...\n 395|// This test reproduces that EXACT Windows failure mode with a real PTY child and\n*396|// a genuinely black-holed controller connection, then asserts the two things the\n 397|// logical gate cannot see:\n*398|//   - the black-holed conn's `handle_conn` thread FINISHES within the write bound\n 399|//     of logical release (broker.conn_handler_count() falls to 0) WITHOUT the\n 400|//     client ever dropping its end;                                    ← RED pre-fix\n 401|//   - a write on the old client connection FAILS (it was physically retired). ← RED pre-fix\n### brain_resume_conn_deadlock.rs\n 268|///   heartbeat conn re-opens the round-3 SharedSend self-deadlock class; post-r4 it\n*269|///   would instead get that heartbeat conn poisoned/retired by the write watchdog —\n 270|///   still a broken resume, just a different failure. Cursor-only must attach nothing.\n 271|///   SEAM-SENSITIVITY (keeps the proof NON-VACUOUS) — a SECOND brain on the SAME broker\n 272|///   takes the hazard path (`resume_sessions`, still present) and MUST raise every\n...\n 323|             the round-3 SharedSend self-deadlock class re-opens (post-r4 it instead \\\n*324|             poisons/retires the heartbeat conn via the write watchdog — still a broken \\\n 325|             resume).\"\n 326|        );\n 327|    }\n### budget.rs\n 12|//! A **dormant** seat is a live, state-preserved harness session held warm\n*13|//! by the broker (PTY master + child process + output ring). A **suspended**\n 14|//! seat is that session closed — only the on-disk perch record remains. So:\n 15|//!\n 16|//! 1. spawn N idle sessions through the real broker (the production PTY\n...\n 21|//!    per-seat overhead: PTY plumbing + output rings);\n*22|//! 4. kill the sessions (the suspended state) and sample the residual.\n 23|//!\n 24|//! The per-seat child cost + broker delta IS the warm premium over\n*25|//! suspended; the suspended residual is ~the daemon baseline.\n 26|//!\n 27|//! ## Knobs (env)\n 28|//!\n...\n 153|\n*154|    // Suspend: close every seat (the session ends; only the perch record\n 155|    // would remain in production). The residual is the daemon baseline.\n 156|    for (brain, _, _) in seats.iter_mut() {\n 157|        let _ = brain.kill_session();\n...\n 161|    println!(\n*162|        \"suspended residual (this process RSS after killing all seats): {} (was {} warm)\",\n 163|        fmt_bytes(self_after.rss_bytes),\n 164|        fmt_bytes(self_warm.rss_bytes)\n 165|    );\n### conn_blackhole_lifecycle.rs\n*1|//! MSG-IDENTITY W6 / F-039 leg (e) — the DETERMINISTIC BLACK-HOLED-CONTROLLER\n 2|//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker\n 3|//! hosting REAL PTY children over the real local IPC.\n 4|//!\n...\n 9|//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the\n*10|//! EXACT Windows named-pipe black-hole (a controller that stops draining while a\n 11|//! finite burst floods its socket, so the broker's writer parks inside\n 12|//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` — no supported\n 13|//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/\n*14|//! src/conn.rs` bounded/cancelable/poison-on-failure writes, landed fc048bb),\n 15|//! asserts hertz's FIVE invariants for ONE incident:\n 16|//!\n 17|//!   1. UNRELATED SESSIONS CONTINUE — a second, independent session's viewer keeps\n*18|//!      receiving output throughout the black-hole incident (the fan-out never\n 19|//!      blocks the whole broker on the one wedged conn);\n 20|//!   2. THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE BOUND — a write\n 21|//!      on the retired old-client connection FAILS, and the conn's `handle_conn`\n...\n 25|//!      unrelated-viewer floor (<= 1; the persistent invariant-1 viewer holds one\n*26|//!      handler) WITHOUT the black-holed client ever dropping its end (the parked\n 27|//!      writer was aborted + the reader EOF-cleaned, so the handler thread ran to\n 28|//!      completion), and the viewer flows output AFTER the sample latches — proving\n 29|//!      the survivor is the viewer's handler, not a leaked one;\n...\n 45|//! This harness GENERALIZES `brain_decouple.rs`'s r4 gate\n*46|//! (`non_draining_controller_stall_evict_releases_writer_and_connection`, the\n 47|//! Windows-mandatory physical-retirement leg for\n 48|//! REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, which stays the primary\n 49|//! gate for that hazard). It reuses the same SEED/trigger/burst choreography,\n 50|//! the hand-back-the-unread-Stream pattern, the `conn_handler_count` and\n*51|//! `stall_evict_count` seams, the old-client-write-fails probe, and the\n 52|//! teardown-before-assert discipline. It ADDS the two pieces the r4 gate does\n 53|//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n 54|//! UNRELATED session whose viewer keeps ticking across the incident, and a fresh\n...\n 59|//! Invariants 2, 3, and the task-ownership proxy of 5 are the PHYSICAL-retirement\n*60|//! asserts, and the deterministic black-hole only reproduces on the Windows\n 61|//! named-pipe path (`interprocess 2.4.2` → `WriteFileEx`+`SleepEx(INFINITE)`,\n 62|//! `set_timeout` → `Unsupported`) — the exact field failure. On Unix the\n*63|//! poison/retire path is a `shutdown(SHUT_RDWR)` abort of a buffer-fill block: a\n 64|//! genuinely DIFFERENT mechanism, not this incident. Faking a Unix companion\n 65|//! would test a different thing, so — mirroring the r4 gate's own\n 66|//! `#[cfg(windows)]` leg — the entire harness is Windows-only. On other targets\n...\n 91|    StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_INPUT, KIND_OUTPUT,\n*92|    KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN, KIND_SPAWNED, KIND_STALL_EVICTS,\n*93|    KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n 94|};\n 95|use spt_daemon::transport::{send_hello, LocalSocketTransport};\n 96|use spt_daemon::Broker;\n...\n 136|/// session so its viewer has a continuous frame stream to keep ticking on\n*137|/// throughout the black-hole incident (invariant 1).\n 138|fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139|    SpawnReq {\n 140|        program: \"cmd\".to_string(),\n...\n 159|/// the burst floods a non-draining controller's socket so its writer parks INSIDE\n*160|/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n 161|/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162|fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n 163|    SpawnReq {\n...\n 197|/// surfaces as a timeout, not a hang). Verbatim sibling of the r4 gate helper.\n*198|fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 199|    let name = name.to_string();\n 200|    let (tx, rx) = std::sync::mpsc::channel();\n 201|    thread::spawn(move || {\n 202|        let mut c = connect(&name);\n*203|        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n 204|        loop {\n 205|            match read_frame(&mut c) {\n*206|                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n 207|                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n 208|                    let _ = tx.send(r.count);\n 209|                    return;\n...\n 222|/// One `KIND_SESSIONS` round-trip, returning the reply. The dispatch runs the\n*223|/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n 224|/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225|/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226|fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n...\n 291|\n*292|    // ── BLACK-HOLED session B: the real SEED+burst PTY child from conn A, then\n*293|    //    DROP A so the ONLY persistent brain conn is the black-holed controller R. ──\n 294|    let (a, sid_b) = spawn_session(&name, seed_burst_spawn_req(\"blackhole-victim-ep\"));\n 295|    drop(a);\n 296|\n...\n 302|    //    draining, so the broker's R-writer parks in `write_frame` exactly as\n*303|    //    against a suspended brain. R's Stream is NEVER dropped until the very end. ──\n 304|    let r_name = name.clone();\n 305|    let (seed_tx, seed_rx) = std::sync::mpsc::channel::<(bool, u64)>();\n 306|    let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();\n### control_stamp_lifetime.rs\n 9|//!\n*10|//! `/exit` ends the RC session INDIRECTLY: the endpoint suspends when the child\n 11|//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n 12|//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n*13|//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n 14|//! path dropped the OutputLog with the controller slot still occupied, so the\n 15|//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\n 16|//! hall-a's info.json still read `controlled:true` and hfenduleam still gossiped\n...\n 30|//! prove nothing about the reap leg. Killing the PTY child fires the exit-waiter\n*31|//! while the controller conn stays open — isolating the reap clear as the ONLY\n 32|//! path that can unlatch the stamps.\n 33|//!\n 34|//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n...\n 183|    // ── Kill the PTY CHILD → the exit-waiter reaps the session (NOT a conn drop, so\n*184|    //    the controller-detach path never runs; only the reap path can unlatch). ──\n 185|    let pid = broker\n 186|        .session_pid(sid)\n 187|        .expect(\"live session must have a pid before kill\");\n### false_promote.rs\n 13|//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\n*14|//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\n 15|//! read from BROKER TRUTH in-process (no brain round-trip).\n 16|//!\n 17|//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\n...\n 19|//! Pre-round-4 this rig manufactured its wedge with a REAL writer parked inside\n*20|//! `write_frame` on a black-holed spawner conn. Round-4\n 21|//! (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK) gives every physical conn\n 22|//! write an independent watchdog that CANCELS + retires a wedged write at the SAME\n 23|//! `brain_write_deadline()` bound — so a real parked writer now self-heals at the\n...\n 32|//! bound/watchdog itself is `brain_decouple.rs`'s concern. The DRAIN below still\n*33|//! rides the REAL logical path (the take's `stall_evict_controller`), unchanged.\n 34|//! `SPT_BRAIN_WRITE_DEADLINE_MS` still shrinks the 15 s deadline (the knob grants\n 35|//! wall-clock only; it never relaxes an assertion).\n 36|//!\n...\n 40|//! 1. the trial does NOT promote while wedged (RED-first: remove the\n*41|//!    `&& env.old_gen_drained()` gate in `run_trial` and it promotes on ready alone,\n 42|//!    reproducing the 22:47 false-promote);\n 43|//! 2. once the wedged old conn drains (here: a second operator's take stall-evicts it\n 44|//!    broker-side, the W2 self-heal), the candidate promotes — ready-seen was LATCHED\n...\n 229|    // The gate under test: REAL broker truth, no brain round-trip.\n*230|    fn old_gen_drained(&self) -> bool {\n 231|        !self.broker.any_local_controller_wedged()\n 232|    }\n 233|}\n...\n 236|#[test]\n*237|fn ready_candidate_does_not_promote_until_the_wedged_old_gen_conn_drains() {\n 238|    init_home();\n 239|    // Shrink the brain-write deadline so the wedged old-gen conn is \"stalled\" in\n 240|    // ~400 ms instead of 15 s (the structural outcome, fast + deterministic).\n### mesh_recovery.rs\n 230|    // NOT a live wrong-identity host: iroh accumulates dial candidates per\n*231|    // endpoint id, so a live decoy's socket would poison later dials for B's\n 232|    // id — a distinct hazard class, out of this wave's scope (seeded).\n 233|    let dead_addr = {\n 234|        let ghost = NetHost::start(hermetic()).expect(\"ghost host\");\n...\n 285|// [int->REQ-PEERADDR-INVARIANT] [int->REQ-PEER-ROUTE-CHAIN] the incident's\n*286|// poison-row class on the production path: the cache holds outer-key B → an\n 287|// address claiming a DIFFERENT identity (the `5ff…`-outer shape found live on\n 288|// both incident nodes). First post-upgrade pump start repairs it from the\n 289|// roster (never bare-deletes the file), and the repaired route converges —\n*290|// the poison value is never dialed as B (it cannot be: the chain validates\n 291|// id-match on every leg).\n 292|#[test]\n*293|fn poison_cache_row_is_repaired_from_the_roster_at_startup() {\n 294|    init_home();\n 295|    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n 296|    let dir = TempDir::new().unwrap();\n...\n 311|    spawn_dispatcher(&b_name, &b_hex, dir.path(), &dstop);\n*312|    // The poison row, seeded the way history did it (pre-validation writers):\n 313|    // outer key B, nested id = somebody else entirely.\n*314|    let mut poison_store = PeerAddrStore::default();\n*315|    poison_store.addrs.insert(\n 316|        b_hex.clone(),\n 317|        serde_json::json!({\"id\": \"0\".repeat(64), \"addrs\": [\"127.0.0.1:9\"]}),\n 318|    );\n*319|    poison_store.save_to(&paths.peer_addrs).unwrap();\n 320|\n 321|    let peer_addrs = paths.peer_addrs.clone();\n 322|    let health_path = PumpHealth::path_beside(&paths.heartbeat);\n...\n 324|\n*325|    converge(\"poison row repaired + route converged\", || {\n 326|        let s = PeerAddrStore::load_from(&peer_addrs);\n 327|        // Repaired-or-rejected: whatever is under B's key must satisfy the\n*328|        // invariant (never the poison) — and a LIVE peer proves the repaired\n 329|        // route carried a real QUIC connect, not just a rewritten file.\n 330|        s.get(&b_hex)\n 331|            .map(|v| spt_store::peeraddrs::addr_peer_id(v) == Some(b_hex.as_str()))\n### net_worker_starve.rs\n 10|//! STAGE 1 (this file, first): the narrow mechanism probe. Stand a dialer whose\n*11|//! connects black-hole a dead peer, fire a `submit_dial` burst straight onto the\n 12|//! broker's own [`NetHost`] (the exact pump path), and read the two round-3\n 13|//! DIAGNOSTIC probes added to `nethost.rs`:\n 14|//!   - `active_dial_tasks()` — in-flight dead-peer connects on the 2 workers;\n...\n 106|/// THE DECIDER (round-3 ruling §5): a burst of `submit_dial`s to a peer that\n*107|/// accepts the QUIC handshake but black-holes the seed-proof. Reports whether\n 108|/// the burst holds the 2 workers (canary goes stale) or the awaits yield (canary\n 109|/// stays fresh). This test does not FAIL on either outcome — it PRINTS the verdict\n 110|/// the ruling needs; an explicit assert nails the direction once observed.\n...\n 121|\n*122|    // B: the black hole — accepts the handshake, never runs the proof responder.\n 123|    let broker_b = served_broker(&name_b, hermetic(Identity::generate()), &dir.path().join(\"b\"));\n 124|    let host_b = broker_b.net().expect(\"host b\");\n 125|    let b_addr = host_b.addr();\n### netbroker.rs\n 53|/// is irrelevant; its PRESENCE is what makes the dialer open the stream and\n*54|/// block, giving the deterministic black hole the broker-side bound must cut.\n 55|fn proving(identity: Identity) -> NetConfig {\n 56|    let mut cfg = hermetic(identity);\n 57|    cfg.membership = Some(std::sync::Arc::new(|| {\n...\n 234|// pump-IPC-deadline B-half): a dial to a peer that ACCEPTS the connection but\n*235|// black-holes the seed-proof (a membership-mismatched/dead peer — the exact\n 236|// shape of the 2.2h hfenduleam wedge) must fail PROMPTLY with an ORDINARY broker\n 237|// error reply within the bound, NEVER an unbounded await. Exactly-once is\n 238|// preserved on timeout: the journaled dial records NO phantom conn — the journal\n...\n 260|\n*261|    // B: the black hole — accepts the QUIC handshake (`membership: None` ⇒ it\n 262|    // registers the conn) but never runs the proof responder, so A's\n 263|    // `prove_membership` awaits a control-stream reply that never comes.\n 264|    let _broker_b = net_broker(&name_b, Identity::generate(), &dir.path().join(\"b\"));\n...\n 273|        .net_dial(b_addr, Some(MintedOp::new(Minter::Cli, OP)))\n*274|        .expect_err(\"a black-holed dial must error, never block forever\");\n 275|    let elapsed = started.elapsed();\n 276|\n 277|    // The broker REPLIED an ordinary error within the bound — not a wedge, and\n### propagate.rs\n 3|//! self-heals across broker-streamed pulls with the full M3c gate at every\n*4|//! receiving node, and every poison path (tampered relay, rollback offer,\n 5|//! untrusted origin) is rejected loud.\n 6|//!\n 7|//! Same harness shape as `tests/sync.rs`: two/three in-process brokers with\n...\n 501|\n*502|// [unit->REQ-UPD-2] a compromised relay node cannot poison the chain: B's\n 503|// staged artifact is tampered at rest, so C's per-node gate ends the pull\n 504|// Rejected(ArtifactMismatch) and stages nothing — the gate is per-node, never\n 505|// inherited from the sender's claim to have verified.\n...\n 517|    // Tamper the bytes at rest (post-verification compromise at B).\n*518|    std::fs::write(b.cache.artifact_path(), b\"poisoned bytes\").unwrap();\n 519|\n 520|    let trust_b = roster_for(&[&c.node_id()]);\n 521|    let (got, served) = pull(&c, &b, &trust_b, &policy(5), &scratch);\n...\n 528|        c.cache.staged().is_none(),\n*529|        \"nothing staged at C — C never re-serves poison\"\n 530|    );\n 531|}\n 532|\n### pump.rs\n 67|/// dial runs `prove_membership`, which a `membership: None` peer never answers\n*68|/// (the documented mixed-pair deadlock) — the deterministic black hole the\n 69|/// broker-side QUIC-op bound must cut. The credential content is irrelevant; its\n 70|/// PRESENCE is what makes the dialer open the proof control stream and block.\n 71|fn proving() -> NetConfig {\n...\n 505|\n*506|    // B: the black hole — accepts the handshake (`membership: None`) but never\n 507|    // runs the proof responder, so A's dial hangs in `prove_membership` until the\n 508|    // broker-side bound cuts it.\n 509|    let _b_broker = net_broker(&b_name, dir.path());\n...\n 645|/// hardcodes `hermetic()`; W2 needs a `proving()` LIVE peer alongside hermetic\n*646|/// black holes). Serve thread detached; effect log named per broker.\n 647|fn net_broker_with(name: &str, dir: &std::path::Path, cfg: NetConfig) -> Arc<Broker> {\n 648|    let host = NetHost::start(cfg).expect(\"net host start\");\n 649|    let broker = Broker::bind_in_with_net(name, dir.join(format!(\"{name}-eff.log\")), Some(host))\n...\n 712|\n*713|    // A's stores: the subnet whose roster is the live peer + the two black holes.\n 714|    let a_root = dir.path().join(\"a\");\n 715|    std::fs::create_dir_all(&a_root).unwrap();\n 716|    let mut a_subnets = SubnetStore::default();\n### pumpdeadline.rs\n 10|//! The wedge this guards: the 2.2h hfenduleam pump stall on\n*11|//! `net_open_stream`'s unbounded `loop { read_event }` against a black-holed\n 12|//! peer the broker never answered for.\n 13|\n 14|use std::io;\n...\n 93|// SILENT surfaces a NON-`TimedOut` (reclassified) error within the SHORT reply-\n*94|// read budget — so `peer_outcome` drops ONLY that peer instead of POISONING the\n 95|// whole round, and it drops PROMPTLY at the decoupled budget, never riding the\n 96|// 30s carrier deadline (the empirical stall: the update leg burned the full 30s\n*97|// -> TimedOut -> whole-round poison -> supervised restart). The sibling of the\n*98|// carrier-op test above: a carrier `TimedOut` poisons; a peer-reply `TimedOut`\n 99|// drops ordinarily.\n 100|#[test]\n 101|fn pump_reply_read_reclassifies_a_silent_peer_within_the_budget() {\n...\n 129|        io::ErrorKind::TimedOut,\n*130|        \"reclassified OUT of TimedOut → peer_outcome drops the peer, never poisons the round\"\n 131|    );\n 132|    assert!(\n 133|        elapsed >= timeout,\n### twohost.rs\n 110|const DRIVE_TGT_ALIAS: &str = \"DriveTgt\";\n*111|// REMOTE-TRUTH B-2 rung — the dead-controller reap target: a dedicated endpoint\n 112|// whose session TICKS continuously (the reap triggers on a FAILED WRITE, so the\n 113|// B-side controller writer must be mid-write when the conn dies ungracefully).\n 114|const ID_B2: &str = \"ling-twohost-b2\";\n...\n 726|\n*727|    // ── M5 rung 8 — remote suspend/wake (serve side): A's wire rest ops run\n 728|    // through THIS node's transition host; observe the round trip on the\n 729|    // durable rest record. The wake edge restores Active (and re-stamps\n 730|    // recency below, so the presence rungs see a live winner).\n...\n 732|    rig_wait(\n*733|        \"rest: A's remote suspend landed (B suspended)\",\n 734|        rig.wait,\n 735|        || {\n 736|            spt_daemon::resting::read_rest(&perch_b)\n*737|                .map(|r| r.state == spt_daemon::resting::RestState::Suspended)\n 738|                .unwrap_or(false)\n 739|        },\n 740|    );\n...\n 855|\n*856|    // ── REMOTE-TRUTH A-3 rung (serve side): A suspends ID_B (setup), then wakes\n 857|    // it with a BARE id — routed at A by the PRODUCTION `route_rest_event`\n 858|    // assembly (seam #2; no `@node` qualifier anywhere) — landing here as an\n 859|    // ordinary wire rest op. The durable rest record is the same observable\n...\n 861|    // (A asserts the routing decision on its side).\n*862|    rig_wait(\"A-3: A's setup suspend landed (B suspended)\", rig.wait, || {\n 863|        spt_daemon::resting::read_rest(&perch_b)\n*864|            .map(|r| r.state == spt_daemon::resting::RestState::Suspended)\n 865|            .unwrap_or(false)\n 866|    });\n 867|    // [int->REQ-REST-VERB-ROUTING]\n...\n 875|\n*876|    // ── REMOTE-TRUTH B-2 rung (serve side): the dead-controller reap under a\n 877|    // REAL ungraceful transport sever. A's throwaway CHILD controls the ticker\n 878|    // session (continuous output keeps THIS broker's controller writer actively\n 879|    // writing); the child is SIGKILL'd — no detach, no conn close — so the next\n...\n 940|    // outcome B-2 exists for (a stale ONLINE+CONTROLLED that never healed).\n*941|    // [int->REQ-CONTROLLER-LIVENESS-REAP]\n*942|    rig_wait(\"B-2: the severed controller's stamps CLEAR (reap + converge)\", rig.wait, || {\n 943|        let _ = host_brain.sessions();\n 944|        info::read_info(&perch_b2).is_some_and(|i| !i.controlled && i.driven_by.is_none())\n 945|    });\n*946|    println!(\"TWOHOST OK: B-2 dead-controller reap cleared the stamps\");\n 947|\n 948|    // ── Hold until A finishes its side (it pushes the done-file as its last\n 949|    // act — the file transfer rung doubling as the ladder's completion barrier).\n...\n 1211|\n*1212|    // ── M5 rung 8 — remote suspend/wake (drive side): `spt suspend id@node`'s\n 1213|    // machinery — one wire rest op each way, the TARGET's transition host\n 1214|    // runs the edge, and the state flip comes back through the ordinary\n 1215|    // registry advertisement (epoch-leased, never wall-clock).\n...\n 1220|        ID_B,\n*1221|        spt_net::net::rest::REST_EVENT_SUSPEND,\n*1222|        \"twohost-rest-suspend\",\n 1223|        MintedOp::new(Minter::Cli, op()),\n 1224|    )\n*1225|    .expect(\"suspend round trip\");\n 1226|    assert!(\n 1227|        matches!(out, spt_daemon::resthost::RestRequestOutcome::Edge(_)),\n*1228|        \"remote suspend applied an edge at B: {out:?}\"\n 1229|    );\n*1230|    rig_wait(\"rest: B's row advertises Suspended at A\", rig.wait, || {\n 1231|        registry\n 1232|            .rows(&rig.subnet, ID_B)\n 1233|            .iter()\n*1234|            .any(|i| i.node == rig.b_hex() && i.status == spt_net::net::registry::Status::Suspended)\n 1235|    });\n 1236|    let out = spt_daemon::resthost::request_rest(\n 1237|        &mut a,\n...\n 1259|    );\n*1260|    println!(\"TWOHOST OK: remote suspend/wake round trip\");\n 1261|\n 1262|    // ── M5 rung 9 — presence-shift redirect (drive side): the user is\n 1263|    // most-recently-active at B (the gossiped stamp), so A's produce\n...\n 1489|    // rest op the qualified arm uses, and B asserts the durable flip.\n*1490|    // Setup: suspend ID_B so the ∃-Active wake goal has one actionable instance.\n 1491|    let out = spt_daemon::resthost::request_rest(\n 1492|        &mut a,\n 1493|        conn.conn_id,\n\n## src/\n### broker.rs\n 64|    KIND_ADAPTER_APPLY, KIND_APPLIED,\n*65|    KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY,\n 66|    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n 67|    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n 68|    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n...\n 80|/// `Arc<Mutex<SendHalf>>` — [`BrokerConn`] owns the half and every write is\n*81|/// bounded + cancelable + poison-on-failure (see [`crate::conn`]). Identity\n 82|/// matching stays `Arc::ptr_eq`.\n 83|pub(crate) type SharedSend = Arc<BrokerConn>;\n 84|\n...\n 156|/// How long a controller's writer thread may sit BLOCKED inside a single socket\n*157|/// write — the brain subscriber suspended / black-holed — before a take/reattach\n 158|/// or the `KIND_SESSIONS` reap stall-evicts it (LIFECYCLE-TRUTH W2,\n 159|/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n 160|/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n...\n 163|/// full-with-**zero writer progress** and covers the case the output-driven path\n*164|/// can't — an IDLE session whose brain-subscriber conn is suspended produces no\n 165|/// output, so `append`/`mark_controller_gone` never runs, and the stalled\n 166|/// controller would otherwise latch `driven_by` forever (reattach refused, `--take`\n 167|/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n...\n 174|/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n*175|/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n 176|/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n 177|/// the slot) is provable in ~1 s instead of 15 (mirrors the\n 178|/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n...\n 254|/// A brain that never returns is then a DIAGNOSABLE fact — surfaced on `daemon\n*255|/// status` (via [`Broker::stall_evict_stats`]) alongside the per-evict broker log\n 256|/// line, not a silent absence.\n 257|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*258|static STALL_EVICT_COUNT: AtomicU64 = AtomicU64::new(0);\n*259|static STALL_EVICT_LAST_MS: AtomicU64 = AtomicU64::new(0);\n 260|\n 261|/// Record one stall-evict in the broker-global tally (best-effort observability).\n 262|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*263|fn record_stall_evict() {\n*264|    STALL_EVICT_COUNT.fetch_add(1, Ordering::Relaxed);\n*265|    STALL_EVICT_LAST_MS.store(crate::brain::now_ms(), Ordering::Relaxed);\n 266|}\n 267|\n 268|/// The current stall-evict tally `(count_since_boot, last_evict_wall_ms)` —\n...\n 271|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*272|pub(crate) fn stall_evict_stats() -> (u64, u64) {\n 273|    (\n*274|        STALL_EVICT_COUNT.load(Ordering::Relaxed),\n*275|        STALL_EVICT_LAST_MS.load(Ordering::Relaxed),\n 276|    )\n 277|}\n 278|\n...\n 515|    /// ([`OutputLog::controller_stalled`]) sees a `Some` older than\n*516|    /// [`BRAIN_WRITE_DEADLINE`] as a writer WEDGED on a suspended/black-holed brain\n 517|    /// conn and stall-evicts it — the signal `last_ok` (channel-handoff time) cannot\n 518|    /// give: an IDLE controller (writer parked on `rx.recv()`, `None` here) and a\n 519|    /// BLOCKED one (writer stuck in the socket write, `Some` here) both leave\n...\n 737|    #[must_use]\n*738|    /// Coherence clamp for a poison-RECOVERED ring (B-1, doyle ruling; run only by\n 739|    /// [`recover_log`] on the recovery path). A panic mid-[`append`] can leave the\n 740|    /// ring torn — an over-cap ring (interrupted prune), a last seq not below\n 741|    /// `next_seq`, or a non-monotonic front/back — and serving those bytes blind\n...\n 746|    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\n*747|    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\n*748|    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n 749|    fn clamp_or_reset(&mut self) -> bool {\n 750|        let within_cap = self.ring.len() <= self.cap_chunks;\n 751|        let last_below_next = match self.ring.back() {\n...\n 764|        eprintln!(\n*765|            \"broker: session {} output ring was torn by a poison-recover \\\n 766|             (had {had} chunks, next_seq {}); reset to empty — scrollback self-heals \\\n*767|             on the next output [REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\",\n 768|            self.session_id, self.next_seq\n 769|        );\n 770|        true\n### conn.rs\n*1|//! Bounded, cancelable, poison-on-failure framed writes for one physical\n 2|//! broker connection (UPDATE-WEDGE round-4).\n 3|//!\n 4|//! ## The failure class this kills\n...\n 10|//! A connected consumer that stops reading (an `rc --take` whose process\n*11|//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n*12|//! logical stall-evict (`stall_evict_controller`) releases only the controller\n 13|//! ROLE — it never cancels the in-flight pipe write or closes the physical\n 14|//! connection, so the writer (and every write queued behind the same gate)\n 15|//! stays wedged until the peer process dies (~128 s in the field capture, on a\n...\n 30|//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\n*31|//! write returning, nor on opportunistic stall-evict sampling: it poisons the\n 32|//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\n 33|//! completion APC makes the wedged `SleepEx` loop return with\n 34|//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\n...\n 37|//!\n*38|//! ## Poison / retire invariant (both OSes)\n 39|//!\n 40|//! On deadline, write failure, or cancellation: (1) the WHOLE physical\n*41|//! connection is poisoned, idempotently — a length-prefixed frame may have\n 42|//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\n 43|//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\n 44|//! (3) the buffer of the canceled op is released only after the op reports\n...\n 58|//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\n*59|//! healthy connections; only the poison/retire path is new.\n 60|// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n 61|\n 62|use std::io;\n...\n 74|/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n*75|/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n 76|/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n 77|/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n*78|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n 79|static CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n 80|\n 81|/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n...\n 89|/// thing by both daemon entries, next to `scrub_identity_env`.\n*90|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n 91|pub(crate) fn init_log_anchor() {\n 92|    let _ = MONO_ANCHOR.set(Instant::now());\n 93|}\n...\n 99|/// to field incident times (wall).\n*100|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n 101|pub(crate) fn log_stamp() -> String {\n 102|    let wall_ms = SystemTime::now()\n 103|        .duration_since(UNIX_EPOCH)\n...\n 114|\n*115|/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\n 116|#[derive(Debug, PartialEq, Eq)]\n 117|pub(crate) enum ConnWrite {\n 118|    /// The frame was fully written.\n...\n 125|/// The serialized write gate. `half: Some` = free; `None` = a write is in\n*126|/// flight (checked out) or the conn has retired (then `poisoned` is set).\n 127|struct Gate {\n 128|    half: Option<SendHalf>,\n 129|}\n...\n 152|    dog_cv: Condvar,\n*153|    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\n 154|    /// conn refuses every future write and is never re-gated.\n*155|    poisoned: AtomicBool,\n 156|    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\n 157|    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\n 158|    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\n...\n 163|    severed: AtomicBool,\n*164|    /// A retired (poisoned) half parks here so its OS handle stays alive\n 165|    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\n 166|    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\n 167|    graveyard: Mutex<Option<SendHalf>>,\n...\n 221|    /// accreted role/endpoint/session facts, wall + monotonic stamps.\n*222|    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 223|    fn attribution(&self) -> String {\n 224|        let label = recover(&self.label);\n 225|        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\n...\n 229|    /// Render the once-per-conn retirement record — the F-039 token split\n*230|    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n 231|    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 232|    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 233|    /// already-dead counterpart, the offline-peer dial churn) emits the\n...\n 236|    /// class is what discriminates fresh-carrier churn from interleave\n*237|    /// artifact in the field). Behavior is identical either way: poison,\n 238|    /// abort, retire, never reuse.\n*239|    // [impl->REQ-CONN-POISON-DIAL-SCOPE]\n 240|    fn render_retirement(&self, timed_out: bool, cause: Option<&io::Error>) -> String {\n 241|        if timed_out {\n 242|            format!("}],"details":{"scopePath":"crates/spt-daemon/tests, crates/spt-daemon/src/broker.rs, crates/spt-daemon/src/conn.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":147,"fileCount":15,"files":["crates/spt-daemon/tests/brain_decouple.rs","crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","crates/spt-daemon/tests/budget.rs","crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","crates/spt-daemon/tests/control_stamp_lifetime.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt-daemon/tests/mesh_recovery.rs","crates/spt-daemon/tests/net_worker_starve.rs","crates/spt-daemon/tests/netbroker.rs","crates/spt-daemon/tests/propagate.rs","crates/spt-daemon/tests/pump.rs","crates/spt-daemon/tests/pumpdeadline.rs","crates/spt-daemon/tests/twohost.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/conn.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/brain_decouple.rs","count":20},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","count":2},{"path":"crates/spt-daemon/tests/budget.rs","count":5},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","count":20},{"path":"crates/spt-daemon/tests/control_stamp_lifetime.rs","count":4},{"path":"crates/spt-daemon/tests/false_promote.rs","count":6},{"path":"crates/spt-daemon/tests/mesh_recovery.rs","count":10},{"path":"crates/spt-daemon/tests/net_worker_starve.rs","count":3},{"path":"crates/spt-daemon/tests/netbroker.rs","count":4},{"path":"crates/spt-daemon/tests/propagate.rs","count":4},{"path":"crates/spt-daemon/tests/pump.rs","count":4},{"path":"crates/spt-daemon/tests/pumpdeadline.rs","count":5},{"path":"crates/spt-daemon/tests/twohost.rs","count":20},{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/conn.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/\n\n## tests/\n### brain_decouple.rs\n  *1│//! LIFECYCLE-TRUTH W2 (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — the suspend-brain\n   2│//! int rig, on a REAL broker hosting a REAL PTY child over the real local IPC.\n   3│//!\n   4│//! ## The bug this gate locks down (the update-wedge root)\n    │...\n   6│//! A controller's writer thread does a BLOCKING socket write to its brain\n  *7│//! subscriber connection. When that brain is SUSPENDED (doyle's rig:\n  *8│//! `NtSuspendProcess` on the brain child) the write never returns. Pre-W2 the\n   9│//! output-driven eviction (`append` → `mark_controller_gone`, bounded by the 5 s\n *10│//! `CONTROLLER_WRITE_DEADLINE`) fires only on NEW output, and `reap_dead_controller`\n  11│//! only caught an EXITED writer — so a controller BLOCKED on an idle session was\n  12│//! evicted by neither: control latched, reattach was refused, `rc --take` hung,\n  13│//! `daemon status` still read healthy.\n  14│//!\n *15│//! ## Faithful in-process analog of a suspended brain\n  16│//!\n *17│//! A test process cannot `NtSuspendProcess` itself, so the rig reproduces the exact\n  18│//! MECHANISM: controller A stops reading its socket. The flood child fills A's recv\n  19│//! buffer, the controller channel fills, and A's writer thread parks INSIDE\n *20│//! `write_frame` — precisely the state a suspended brain leaves its broker conn in\n  21│//! (a peer that stopped draining). This is the same analog the 7.12 wedge gate uses.\n  22│//! `SPT_BRAIN_WRITE_DEADLINE_MS` shrinks the 15 s production deadline so the\n  23│//! structural outcome is provable in ~1 s (the deadline knob grants wall-clock only;\n    │...\n  40│//! journal replay dedup, the `delivered_through` no-rewind CAS `advanced_cursor`, and\n *41│//! the clean-repaint cold-attach batch). A live `NtSuspendProcess` / `SIGSTOP` run on a\n  42│//! real brain child remains the MANUAL field rig from the incident night — this CI test\n  43│//! is the deterministic in-process ANALOG, not that run.\n  44│//!\n    │...\n  62│    SpawnReq, Spawned, StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_SPAWN,\n *63│    KIND_SPAWNED, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n  64│};\n  65│use spt_daemon::transport::{send_hello, LocalSocketTransport};\n  66│use spt_daemon::Broker;\n    │...\n 195│/// Query the broker's stall-evict tally.\n*196│fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 197│    let name = name.to_string();\n 198│    let (tx, rx) = std::sync::mpsc::channel();\n 199│    thread::spawn(move || {\n 200│        let mut c = connect(&name);\n*201│        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n 202│        loop {\n 203│            match read_frame(&mut c) {\n*204│                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n 205│                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n 206│                    let _ = tx.send(r.count);\n 207│                    return;\n    │...\n 221│#[test]\n*222│fn suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks() {\n 223│    init_home();\n 224│    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 225│    // instead of 15 s — the structural outcome, fast + deterministic. Well under the\n    │...\n 252│    // ── Remote controller R: a REMOTE operator (by=Some) takes control of the\n*253│    //    (locally-spawned, undriven) session, then STOPS reading — the suspended-\n 254│    //    brain analog. R MUST be a remote controller: a second remote's Control\n 255│    //    subscribe against a DIFFERENT-remote-driven session busy-refuses PRE-W2\n 256│    //    (the wedge), so only the stall-evict can free the slot — this is what\n    │...\n 259│    //    read from) on its own thread so the broker's writer for R parks in\n*260│    //    write_frame exactly as against a suspended brain. ──\n 261│    let rname = name.clone();\n 262│    thread::spawn(move || {\n 263│        let mut r = connect(&rname);\n    │...\n 335│\n*336│    let evicts = stall_evict_count(&name, Duration::from_secs(5));\n 337│\n 338│    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\n 339│    if let Some(pid) = broker.session_pid(sid) {\n    │...\n 344│        \"=== W2 GATE: viewer_ticks_during_wedge={ticks_during_wedge} take_outcome={outcome:?} \\\n*345│         take_elapsed={took:?} stall_evicts={evicts} ===\"\n 346│    );\n 347│\n 348│    // The viewer kept ticking while the controller conn was wedged.\n    │...\n 383│// §Regression gate) proved that logical eviction does NOT retire the PHYSICAL\n*384│// connection: `stall_evict_controller` drops the `ControllerSink` (one\n 385│// `SharedSend` clone + the `JoinHandle`) but neither cancels the in-flight\n 386│// Windows named-pipe write, closes the socket, nor `CancelIoEx`'s it. The\n 387│// already-running `controller_writer` keeps its OWN `SharedSend` clone and stays\n    │...\n 395│// This test reproduces that EXACT Windows failure mode with a real PTY child and\n*396│// a genuinely black-holed controller connection, then asserts the two things the\n 397│// logical gate cannot see:\n*398│//   - the black-holed conn's `handle_conn` thread FINISHES within the write bound\n 399│//     of logical release (broker.conn_handler_count() falls to 0) WITHOUT the\n 400│//     client ever dropping its end;                                    ← RED pre-fix\n 401│//   - a write on the old client connection FAILS (it was physically retired). ← RED pre-fix\n### brain_resume_conn_deadlock.rs\n 268│///   heartbeat conn re-opens the round-3 SharedSend self-deadlock class; post-r4 it\n*269│///   would instead get that heartbeat conn poisoned/retired by the write watchdog —\n 270│///   still a broken resume, just a different failure. Cursor-only must attach nothing.\n 271│///   SEAM-SENSITIVITY (keeps the proof NON-VACUOUS) — a SECOND brain on the SAME broker\n 272│///   takes the hazard path (`resume_sessions`, still present) and MUST raise every\n    │...\n 323│             the round-3 SharedSend self-deadlock class re-opens (post-r4 it instead \\\n*324│             poisons/retires the heartbeat conn via the write watchdog — still a broken \\\n 325│             resume).\"\n 326│        );\n 327│    }\n### budget.rs\n  12│//! A **dormant** seat is a live, state-preserved harness session held warm\n *13│//! by the broker (PTY master + child process + output ring). A **suspended**\n  14│//! seat is that session closed — only the on-disk perch record remains. So:\n  15│//!\n  16│//! 1. spawn N idle sessions through the real broker (the production PTY\n    │...\n  21│//!    per-seat overhead: PTY plumbing + output rings);\n *22│//! 4. kill the sessions (the suspended state) and sample the residual.\n  23│//!\n  24│//! The per-seat child cost + broker delta IS the warm premium over\n *25│//! suspended; the suspended residual is ~the daemon baseline.\n  26│//!\n  27│//! ## Knobs (env)\n  28│//!\n    │...\n 153│\n*154│    // Suspend: close every seat (the session ends; only the perch record\n 155│    // would remain in production). The residual is the daemon baseline.\n 156│    for (brain, _, _) in seats.iter_mut() {\n 157│        let _ = brain.kill_session();\n    │...\n 161│    println!(\n*162│        \"suspended residual (this process RSS after killing all seats): {} (was {} warm)\",\n 163│        fmt_bytes(self_after.rss_bytes),\n 164│        fmt_bytes(self_warm.rss_bytes)\n 165│    );\n### conn_blackhole_lifecycle.rs\n  *1│//! MSG-IDENTITY W6 / F-039 leg (e) — the DETERMINISTIC BLACK-HOLED-CONTROLLER\n   2│//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker\n   3│//! hosting REAL PTY children over the real local IPC.\n   4│//!\n    │...\n   9│//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the\n *10│//! EXACT Windows named-pipe black-hole (a controller that stops draining while a\n  11│//! finite burst floods its socket, so the broker's writer parks inside\n  12│//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` — no supported\n  13│//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/\n *14│//! src/conn.rs` bounded/cancelable/poison-on-failure writes, landed fc048bb),\n  15│//! asserts hertz's FIVE invariants for ONE incident:\n  16│//!\n  17│//!   1. UNRELATED SESSIONS CONTINUE — a second, independent session's viewer keeps\n *18│//!      receiving output throughout the black-hole incident (the fan-out never\n  19│//!      blocks the whole broker on the one wedged conn);\n  20│//!   2. THE BAD PHYSICAL CONNECTION IS CANCELED/CLOSED WITHIN THE BOUND — a write\n  21│//!      on the retired old-client connection FAILS, and the conn's `handle_conn`\n    │...\n  25│//!      unrelated-viewer floor (<= 1; the persistent invariant-1 viewer holds one\n *26│//!      handler) WITHOUT the black-holed client ever dropping its end (the parked\n  27│//!      writer was aborted + the reader EOF-cleaned, so the handler thread ran to\n  28│//!      completion), and the viewer flows output AFTER the sample latches — proving\n  29│//!      the survivor is the viewer's handler, not a leaked one;\n    │...\n  45│//! This harness GENERALIZES `brain_decouple.rs`'s r4 gate\n *46│//! (`non_draining_controller_stall_evict_releases_writer_and_connection`, the\n  47│//! Windows-mandatory physical-retirement leg for\n  48│//! REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, which stays the primary\n  49│//! gate for that hazard). It reuses the same SEED/trigger/burst choreography,\n  50│//! the hand-back-the-unread-Stream pattern, the `conn_handler_count` and\n *51│//! `stall_evict_count` seams, the old-client-write-fails probe, and the\n  52│//! teardown-before-assert discipline. It ADDS the two pieces the r4 gate does\n  53│//! not have, which are exactly hertz's invariants (1) and (4): a SECOND\n  54│//! UNRELATED session whose viewer keeps ticking across the incident, and a fresh\n    │...\n  59│//! Invariants 2, 3, and the task-ownership proxy of 5 are the PHYSICAL-retirement\n *60│//! asserts, and the deterministic black-hole only reproduces on the Windows\n  61│//! named-pipe path (`interprocess 2.4.2` → `WriteFileEx`+`SleepEx(INFINITE)`,\n  62│//! `set_timeout` → `Unsupported`) — the exact field failure. On Unix the\n *63│//! poison/retire path is a `shutdown(SHUT_RDWR)` abort of a buffer-fill block: a\n  64│//! genuinely DIFFERENT mechanism, not this incident. Faking a Unix companion\n  65│//! would test a different thing, so — mirroring the r4 gate's own\n  66│//! `#[cfg(windows)]` leg — the entire harness is Windows-only. On other targets\n    │...\n  91│    StallEvictsReply, SubscribeOutcome, SubscribeReq, SubscribedReply, KIND_INPUT, KIND_OUTPUT,\n *92│    KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN, KIND_SPAWNED, KIND_STALL_EVICTS,\n *93│    KIND_STALL_EVICTS_REPLY, KIND_SUBSCRIBE, KIND_SUBSCRIBED,\n  94│};\n  95│use spt_daemon::transport::{send_hello, LocalSocketTransport};\n  96│use spt_daemon::Broker;\n    │...\n 136│/// session so its viewer has a continuous frame stream to keep ticking on\n*137│/// throughout the black-hole incident (invariant 1).\n 138│fn flood_spawn_req(endpoint: &str) -> SpawnReq {\n 139│    SpawnReq {\n 140│        program: \"cmd\".to_string(),\n    │...\n 159│/// the burst floods a non-draining controller's socket so its writer parks INSIDE\n*160│/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n 161│/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162│fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n 163│    SpawnReq {\n    │...\n 197│/// surfaces as a timeout, not a hang). Verbatim sibling of the r4 gate helper.\n*198│fn stall_evict_count(name: &str, deadline: Duration) -> u64 {\n 199│    let name = name.to_string();\n 200│    let (tx, rx) = std::sync::mpsc::channel();\n 201│    thread::spawn(move || {\n 202│        let mut c = connect(&name);\n*203│        send(&mut c, KIND_STALL_EVICTS, serde_json::Value::Null);\n 204│        loop {\n 205│            match read_frame(&mut c) {\n*206│                Ok(f) if f.kind == KIND_STALL_EVICTS_REPLY => {\n 207│                    let r: StallEvictsReply = serde_json::from_value(f.payload).unwrap();\n 208│                    let _ = tx.send(r.count);\n 209│                    return;\n    │...\n 222│/// One `KIND_SESSIONS` round-trip, returning the reply. The dispatch runs the\n*223│/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n 224│/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225│/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226│fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n    │...\n 291│\n*292│    // ── BLACK-HOLED session B: the real SEED+burst PTY child from conn A, then\n*293│    //    DROP A so the ONLY persistent brain conn is the black-holed controller R. ──\n 294│    let (a, sid_b) = spawn_session(&name, seed_burst_spawn_req(\"blackhole-victim-ep\"));\n 295│    drop(a);\n 296│\n    │...\n 302│    //    draining, so the broker's R-writer parks in `write_frame` exactly as\n*303│    //    against a suspended brain. R's Stream is NEVER dropped until the very end. ──\n 304│    let r_name = name.clone();\n 305│    let (seed_tx, seed_rx) = std::sync::mpsc::channel::<(bool, u64)>();\n 306│    let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();\n### control_stamp_lifetime.rs\n   9│//!\n *10│//! `/exit` ends the RC session INDIRECTLY: the endpoint suspends when the child\n  11│//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n  12│//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n *13│//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n  14│//! path dropped the OutputLog with the controller slot still occupied, so the\n  15│//! stamps latched. Live evidence (triage #2): hours after an operator `/exit`,\n  16│//! hall-a's info.json still read `controlled:true` and hfenduleam still gossiped\n    │...\n  30│//! prove nothing about the reap leg. Killing the PTY child fires the exit-waiter\n *31│//! while the controller conn stays open — isolating the reap clear as the ONLY\n  32│//! path that can unlatch the stamps.\n  33│//!\n  34│//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded\n    │...\n 183│    // ── Kill the PTY CHILD → the exit-waiter reaps the session (NOT a conn drop, so\n*184│    //    the controller-detach path never runs; only the reap path can unlatch). ──\n 185│    let pid = broker\n 186│        .session_pid(sid)\n 187│        .expect(\"live session must have a pid before kill\");\n### false_promote.rs\n  13│//! fix: the promotion gate adds a DRAINED precondition — promote only on ready AND\n *14│//! `old_gen_drained()` (the old-gen local controller conn CLOSED or stall-evicted),\n  15│//! read from BROKER TRUTH in-process (no brain round-trip).\n  16│//!\n  17│//! ## The wedge analog (round-4 revision — statically held, doyle Option C)\n    │...\n  19│//! Pre-round-4 this rig manufactured its wedge with a REAL writer parked inside\n *20│//! `write_frame` on a black-holed spawner conn. Round-4\n  21│//! (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK) gives every physical conn\n  22│//! write an independent watchdog that CANCELS + retires a wedged write at the SAME\n  23│//! `brain_write_deadline()` bound — so a real parked writer now self-heals at the\n    │...\n  32│//! bound/watchdog itself is `brain_decouple.rs`'s concern. The DRAIN below still\n *33│//! rides the REAL logical path (the take's `stall_evict_controller`), unchanged.\n  34│//! `SPT_BRAIN_WRITE_DEADLINE_MS` still shrinks the 15 s deadline (the knob grants\n  35│//! wall-clock only; it never relaxes an assertion).\n  36│//!\n    │...\n  40│//! 1. the trial does NOT promote while wedged (RED-first: remove the\n *41│//!    `&& env.old_gen_drained()` gate in `run_trial` and it promotes on ready alone,\n  42│//!    reproducing the 22:47 false-promote);\n  43│//! 2. once the wedged old conn drains (here: a second operator's take stall-evicts it\n  44│//!    broker-side, the W2 self-heal), the candidate promotes — ready-seen was LATCHED\n    │...\n 229│    // The gate under test: REAL broker truth, no brain round-trip.\n*230│    fn old_gen_drained(&self) -> bool {\n 231│        !self.broker.any_local_controller_wedged()\n 232│    }\n 233│}\n    │...\n 236│#[test]\n*237│fn ready_candidate_does_not_promote_until_the_wedged_old_gen_conn_drains() {\n 238│    init_home();\n 239│    // Shrink the brain-write deadline so the wedged old-gen conn is \"stalled\" in\n 240│    // ~400 ms instead of 15 s (the structural outcome, fast + deterministic).\n### mesh_recovery.rs\n 230│    // NOT a live wrong-identity host: iroh accumulates dial candidates per\n*231│    // endpoint id, so a live decoy's socket would poison later dials for B's\n 232│    // id — a distinct hazard class, out of this wave's scope (seeded).\n 233│    let dead_addr = {\n 234│        let ghost = NetHost::start(hermetic()).expect(\"ghost host\");\n    │...\n 285│// [int->REQ-PEERADDR-INVARIANT] [int->REQ-PEER-ROUTE-CHAIN] the incident's\n*286│// poison-row class on the production path: the cache holds outer-key B → an\n 287│// address claiming a DIFFERENT identity (the `5ff…`-outer shape found live on\n 288│// both incident nodes). First post-upgrade pump start repairs it from the\n 289│// roster (never bare-deletes the file), and the repaired route converges —\n*290│// the poison value is never dialed as B (it cannot be: the chain validates\n 291│// id-match on every leg).\n 292│#[test]\n*293│fn poison_cache_row_is_repaired_from_the_roster_at_startup() {\n 294│    init_home();\n 295│    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());\n 296│    let dir = TempDir::new().unwrap();\n    │...\n 311│    spawn_dispatcher(&b_name, &b_hex, dir.path(), &dstop);\n*312│    // The poison row, seeded the way history did it (pre-validation writers):\n 313│    // outer key B, nested id = somebody else entirely.\n*314│    let mut poison_store = PeerAddrStore::default();\n*315│    poison_store.addrs.insert(\n 316│        b_hex.clone(),\n 317│        serde_json::json!({\"id\": \"0\".repeat(64), \"addrs\": [\"127.0.0.1:9\"]}),\n 318│    );\n*319│    poison_store.save_to(&paths.peer_addrs).unwrap();\n 320│\n 321│    let peer_addrs = paths.peer_addrs.clone();\n 322│    let health_path = PumpHealth::path_beside(&paths.heartbeat);\n    │...\n 324│\n*325│    converge(\"poison row repaired + route converged\", || {\n 326│        let s = PeerAddrStore::load_from(&peer_addrs);\n 327│        // Repaired-or-rejected: whatever is under B's key must satisfy the\n*328│        // invariant (never the poison) — and a LIVE peer proves the repaired\n 329│        // route carried a real QUIC connect, not just a rewritten file.\n 330│        s.get(&b_hex)\n 331│            .map(|v| spt_store::peeraddrs::addr_peer_id(v) == Some(b_hex.as_str()))\n### net_worker_starve.rs\n  10│//! STAGE 1 (this file, first): the narrow mechanism probe. Stand a dialer whose\n *11│//! connects black-hole a dead peer, fire a `submit_dial` burst straight onto the\n  12│//! broker's own [`NetHost`] (the exact pump path), and read the two round-3\n  13│//! DIAGNOSTIC probes added to `nethost.rs`:\n  14│//!   - `active_dial_tasks()` — in-flight dead-peer connects on the 2 workers;\n    │...\n 106│/// THE DECIDER (round-3 ruling §5): a burst of `submit_dial`s to a peer that\n*107│/// accepts the QUIC handshake but black-holes the seed-proof. Reports whether\n 108│/// the burst holds the 2 workers (canary goes stale) or the awaits yield (canary\n 109│/// stays fresh). This test does not FAIL on either outcome — it PRINTS the verdict\n 110│/// the ruling needs; an explicit assert nails the direction once observed.\n    │...\n 121│\n*122│    // B: the black hole — accepts the handshake, never runs the proof responder.\n 123│    let broker_b = served_broker(&name_b, hermetic(Identity::generate()), &dir.path().join(\"b\"));\n 124│    let host_b = broker_b.net().expect(\"host b\");\n 125│    let b_addr = host_b.addr();\n### netbroker.rs\n  53│/// is irrelevant; its PRESENCE is what makes the dialer open the stream and\n *54│/// block, giving the deterministic black hole the broker-side bound must cut.\n  55│fn proving(identity: Identity) -> NetConfig {\n  56│    let mut cfg = hermetic(identity);\n  57│    cfg.membership = Some(std::sync::Arc::new(|| {\n    │...\n 234│// pump-IPC-deadline B-half): a dial to a peer that ACCEPTS the connection but\n*235│// black-holes the seed-proof (a membership-mismatched/dead peer — the exact\n 236│// shape of the 2.2h hfenduleam wedge) must fail PROMPTLY with an ORDINARY broker\n 237│// error reply within the bound, NEVER an unbounded await. Exactly-once is\n 238│// preserved on timeout: the journaled dial records NO phantom conn — the journal\n    │...\n 260│\n*261│    // B: the black hole — accepts the QUIC handshake (`membership: None` ⇒ it\n 262│    // registers the conn) but never runs the proof responder, so A's\n 263│    // `prove_membership` awaits a control-stream reply that never comes.\n 264│    let _broker_b = net_broker(&name_b, Identity::generate(), &dir.path().join(\"b\"));\n    │...\n 273│        .net_dial(b_addr, Some(MintedOp::new(Minter::Cli, OP)))\n*274│        .expect_err(\"a black-holed dial must error, never block forever\");\n 275│    let elapsed = started.elapsed();\n 276│\n 277│    // The broker REPLIED an ordinary error within the bound — not a wedge, and\n### propagate.rs\n   3│//! self-heals across broker-streamed pulls with the full M3c gate at every\n  *4│//! receiving node, and every poison path (tampered relay, rollback offer,\n   5│//! untrusted origin) is rejected loud.\n   6│//!\n   7│//! Same harness shape as `tests/sync.rs`: two/three in-process brokers with\n    │...\n 501│\n*502│// [unit->REQ-UPD-2] a compromised relay node cannot poison the chain: B's\n 503│// staged artifact is tampered at rest, so C's per-node gate ends the pull\n 504│// Rejected(ArtifactMismatch) and stages nothing — the gate is per-node, never\n 505│// inherited from the sender's claim to have verified.\n    │...\n 517│    // Tamper the bytes at rest (post-verification compromise at B).\n*518│    std::fs::write(b.cache.artifact_path(), b\"poisoned bytes\").unwrap();\n 519│\n 520│    let trust_b = roster_for(&[&c.node_id()]);\n 521│    let (got, served) = pull(&c, &b, &trust_b, &policy(5), &scratch);\n    │...\n 528│        c.cache.staged().is_none(),\n*529│        \"nothing staged at C — C never re-serves poison\"\n 530│    );\n 531│}\n 532│\n### pump.rs\n  67│/// dial runs `prove_membership`, which a `membership: None` peer never answers\n *68│/// (the documented mixed-pair deadlock) — the deterministic black hole the\n  69│/// broker-side QUIC-op bound must cut. The credential content is irrelevant; its\n  70│/// PRESENCE is what makes the dialer open the proof control stream and block.\n  71│fn proving() -> NetConfig {\n    │...\n 505│\n*506│    // B: the black hole — accepts the handshake (`membership: None`) but never\n 507│    // runs the proof responder, so A's dial hangs in `prove_membership` until the\n 508│    // broker-side bound cuts it.\n 509│    let _b_broker = net_broker(&b_name, dir.path());\n    │...\n 645│/// hardcodes `hermetic()`; W2 needs a `proving()` LIVE peer alongside hermetic\n*646│/// black holes). Serve thread detached; effect log named per broker.\n 647│fn net_broker_with(name: &str, dir: &std::path::Path, cfg: NetConfig) -> Arc<Broker> {\n 648│    let host = NetHost::start(cfg).expect(\"net host start\");\n 649│    let broker = Broker::bind_in_with_net(name, dir.join(format!(\"{name}-eff.log\")), Some(host))\n    │...\n 712│\n*713│    // A's stores: the subnet whose roster is the live peer + the two black holes.\n 714│    let a_root = dir.path().join(\"a\");\n 715│    std::fs::create_dir_all(&a_root).unwrap();\n 716│    let mut a_subnets = SubnetStore::default();\n### pumpdeadline.rs\n  10│//! The wedge this guards: the 2.2h hfenduleam pump stall on\n *11│//! `net_open_stream`'s unbounded `loop { read_event }` against a black-holed\n  12│//! peer the broker never answered for.\n  13│\n  14│use std::io;\n    │...\n  93│// SILENT surfaces a NON-`TimedOut` (reclassified) error within the SHORT reply-\n *94│// read budget — so `peer_outcome` drops ONLY that peer instead of POISONING the\n  95│// whole round, and it drops PROMPTLY at the decoupled budget, never riding the\n  96│// 30s carrier deadline (the empirical stall: the update leg burned the full 30s\n *97│// -> TimedOut -> whole-round poison -> supervised restart). The sibling of the\n *98│// carrier-op test above: a carrier `TimedOut` poisons; a peer-reply `TimedOut`\n  99│// drops ordinarily.\n 100│#[test]\n 101│fn pump_reply_read_reclassifies_a_silent_peer_within_the_budget() {\n    │...\n 129│        io::ErrorKind::TimedOut,\n*130│        \"reclassified OUT of TimedOut → peer_outcome drops the peer, never poisons the round\"\n 131│    );\n 132│    assert!(\n 133│        elapsed >= timeout,\n### twohost.rs\n  110│const DRIVE_TGT_ALIAS: &str = \"DriveTgt\";\n *111│// REMOTE-TRUTH B-2 rung — the dead-controller reap target: a dedicated endpoint\n  112│// whose session TICKS continuously (the reap triggers on a FAILED WRITE, so the\n  113│// B-side controller writer must be mid-write when the conn dies ungracefully).\n  114│const ID_B2: &str = \"ling-twohost-b2\";\n     │...\n  726│\n *727│    // ── M5 rung 8 — remote suspend/wake (serve side): A's wire rest ops run\n  728│    // through THIS node's transition host; observe the round trip on the\n  729│    // durable rest record. The wake edge restores Active (and re-stamps\n  730│    // recency below, so the presence rungs see a live winner).\n     │...\n  732│    rig_wait(\n *733│        \"rest: A's remote suspend landed (B suspended)\",\n  734│        rig.wait,\n  735│        || {\n  736│            spt_daemon::resting::read_rest(&perch_b)\n *737│                .map(|r| r.state == spt_daemon::resting::RestState::Suspended)\n  738│                .unwrap_or(false)\n  739│        },\n  740│    );\n     │...\n  855│\n *856│    // ── REMOTE-TRUTH A-3 rung (serve side): A suspends ID_B (setup), then wakes\n  857│    // it with a BARE id — routed at A by the PRODUCTION `route_rest_event`\n  858│    // assembly (seam #2; no `@node` qualifier anywhere) — landing here as an\n  859│    // ordinary wire rest op. The durable rest record is the same observable\n     │...\n  861│    // (A asserts the routing decision on its side).\n *862│    rig_wait(\"A-3: A's setup suspend landed (B suspended)\", rig.wait, || {\n  863│        spt_daemon::resting::read_rest(&perch_b)\n *864│            .map(|r| r.state == spt_daemon::resting::RestState::Suspended)\n  865│            .unwrap_or(false)\n  866│    });\n  867│    // [int->REQ-REST-VERB-ROUTING]\n     │...\n  875│\n *876│    // ── REMOTE-TRUTH B-2 rung (serve side): the dead-controller reap under a\n  877│    // REAL ungraceful transport sever. A's throwaway CHILD controls the ticker\n  878│    // session (continuous output keeps THIS broker's controller writer actively\n  879│    // writing); the child is SIGKILL'd — no detach, no conn close — so the next\n     │...\n  940│    // outcome B-2 exists for (a stale ONLINE+CONTROLLED that never healed).\n *941│    // [int->REQ-CONTROLLER-LIVENESS-REAP]\n *942│    rig_wait(\"B-2: the severed controller's stamps CLEAR (reap + converge)\", rig.wait, || {\n  943│        let _ = host_brain.sessions();\n  944│        info::read_info(&perch_b2).is_some_and(|i| !i.controlled && i.driven_by.is_none())\n  945│    });\n *946│    println!(\"TWOHOST OK: B-2 dead-controller reap cleared the stamps\");\n  947│\n  948│    // ── Hold until A finishes its side (it pushes the done-file as its last\n  949│    // act — the file transfer rung doubling as the ladder's completion barrier).\n     │...\n 1211│\n*1212│    // ── M5 rung 8 — remote suspend/wake (drive side): `spt suspend id@node`'s\n 1213│    // machinery — one wire rest op each way, the TARGET's transition host\n 1214│    // runs the edge, and the state flip comes back through the ordinary\n 1215│    // registry advertisement (epoch-leased, never wall-clock).\n     │...\n 1220│        ID_B,\n*1221│        spt_net::net::rest::REST_EVENT_SUSPEND,\n*1222│        \"twohost-rest-suspend\",\n 1223│        MintedOp::new(Minter::Cli, op()),\n 1224│    )\n*1225│    .expect(\"suspend round trip\");\n 1226│    assert!(\n 1227│        matches!(out, spt_daemon::resthost::RestRequestOutcome::Edge(_)),\n*1228│        \"remote suspend applied an edge at B: {out:?}\"\n 1229│    );\n*1230│    rig_wait(\"rest: B's row advertises Suspended at A\", rig.wait, || {\n 1231│        registry\n 1232│            .rows(&rig.subnet, ID_B)\n 1233│            .iter()\n*1234│            .any(|i| i.node == rig.b_hex() && i.status == spt_net::net::registry::Status::Suspended)\n 1235│    });\n 1236│    let out = spt_daemon::resthost::request_rest(\n 1237│        &mut a,\n     │...\n 1259│    );\n*1260│    println!(\"TWOHOST OK: remote suspend/wake round trip\");\n 1261│\n 1262│    // ── M5 rung 9 — presence-shift redirect (drive side): the user is\n 1263│    // most-recently-active at B (the gossiped stamp), so A's produce\n     │...\n 1489│    // rest op the qualified arm uses, and B asserts the durable flip.\n*1490│    // Setup: suspend ID_B so the ∃-Active wake goal has one actionable instance.\n 1491│    let out = spt_daemon::resthost::request_rest(\n 1492│        &mut a,\n 1493│        conn.conn_id,\n\n## src/\n### broker.rs\n  64│    KIND_ADAPTER_APPLY, KIND_APPLIED,\n *65│    KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY,\n  66│    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n  67│    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n  68│    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n    │...\n  80│/// `Arc<Mutex<SendHalf>>` — [`BrokerConn`] owns the half and every write is\n *81│/// bounded + cancelable + poison-on-failure (see [`crate::conn`]). Identity\n  82│/// matching stays `Arc::ptr_eq`.\n  83│pub(crate) type SharedSend = Arc<BrokerConn>;\n  84│\n    │...\n 156│/// How long a controller's writer thread may sit BLOCKED inside a single socket\n*157│/// write — the brain subscriber suspended / black-holed — before a take/reattach\n 158│/// or the `KIND_SESSIONS` reap stall-evicts it (LIFECYCLE-TRUTH W2,\n 159│/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n 160│/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n    │...\n 163│/// full-with-**zero writer progress** and covers the case the output-driven path\n*164│/// can't — an IDLE session whose brain-subscriber conn is suspended produces no\n 165│/// output, so `append`/`mark_controller_gone` never runs, and the stalled\n 166│/// controller would otherwise latch `driven_by` forever (reattach refused, `--take`\n 167│/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n    │...\n 174│/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n*175│/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n 176│/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n 177│/// the slot) is provable in ~1 s instead of 15 (mirrors the\n 178│/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n    │...\n 254│/// A brain that never returns is then a DIAGNOSABLE fact — surfaced on `daemon\n*255│/// status` (via [`Broker::stall_evict_stats`]) alongside the per-evict broker log\n 256│/// line, not a silent absence.\n 257│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*258│static STALL_EVICT_COUNT: AtomicU64 = AtomicU64::new(0);\n*259│static STALL_EVICT_LAST_MS: AtomicU64 = AtomicU64::new(0);\n 260│\n 261│/// Record one stall-evict in the broker-global tally (best-effort observability).\n 262│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*263│fn record_stall_evict() {\n*264│    STALL_EVICT_COUNT.fetch_add(1, Ordering::Relaxed);\n*265│    STALL_EVICT_LAST_MS.store(crate::brain::now_ms(), Ordering::Relaxed);\n 266│}\n 267│\n 268│/// The current stall-evict tally `(count_since_boot, last_evict_wall_ms)` —\n    │...\n 271│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*272│pub(crate) fn stall_evict_stats() -> (u64, u64) {\n 273│    (\n*274│        STALL_EVICT_COUNT.load(Ordering::Relaxed),\n*275│        STALL_EVICT_LAST_MS.load(Ordering::Relaxed),\n 276│    )\n 277│}\n 278│\n    │...\n 515│    /// ([`OutputLog::controller_stalled`]) sees a `Some` older than\n*516│    /// [`BRAIN_WRITE_DEADLINE`] as a writer WEDGED on a suspended/black-holed brain\n 517│    /// conn and stall-evicts it — the signal `last_ok` (channel-handoff time) cannot\n 518│    /// give: an IDLE controller (writer parked on `rx.recv()`, `None` here) and a\n 519│    /// BLOCKED one (writer stuck in the socket write, `Some` here) both leave\n    │...\n 737│    #[must_use]\n*738│    /// Coherence clamp for a poison-RECOVERED ring (B-1, doyle ruling; run only by\n 739│    /// [`recover_log`] on the recovery path). A panic mid-[`append`] can leave the\n 740│    /// ring torn — an over-cap ring (interrupted prune), a last seq not below\n 741│    /// `next_seq`, or a non-monotonic front/back — and serving those bytes blind\n    │...\n 746│    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\n*747│    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\n*748│    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n 749│    fn clamp_or_reset(&mut self) -> bool {\n 750│        let within_cap = self.ring.len() <= self.cap_chunks;\n 751│        let last_below_next = match self.ring.back() {\n    │...\n 764│        eprintln!(\n*765│            \"broker: session {} output ring was torn by a poison-recover \\\n 766│             (had {had} chunks, next_seq {}); reset to empty — scrollback self-heals \\\n*767│             on the next output [REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\",\n 768│            self.session_id, self.next_seq\n 769│        );\n 770│        true\n### conn.rs\n  *1│//! Bounded, cancelable, poison-on-failure framed writes for one physical\n   2│//! broker connection (UPDATE-WEDGE round-4).\n   3│//!\n   4│//! ## The failure class this kills\n    │...\n  10│//! A connected consumer that stops reading (an `rc --take` whose process\n *11│//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n *12│//! logical stall-evict (`stall_evict_controller`) releases only the controller\n  13│//! ROLE — it never cancels the in-flight pipe write or closes the physical\n  14│//! connection, so the writer (and every write queued behind the same gate)\n  15│//! stays wedged until the peer process dies (~128 s in the field capture, on a\n    │...\n  30│//! The watchdog fires OUT OF BAND at the deadline — it never relies on the\n *31│//! write returning, nor on opportunistic stall-evict sampling: it poisons the\n  32│//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\n  33│//! completion APC makes the wedged `SleepEx` loop return with\n  34│//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\n    │...\n  37│//!\n *38│//! ## Poison / retire invariant (both OSes)\n  39│//!\n  40│//! On deadline, write failure, or cancellation: (1) the WHOLE physical\n *41│//! connection is poisoned, idempotently — a length-prefixed frame may have\n  42│//! been PARTIALLY written, so reuse would corrupt framing; (2) read and write\n  43│//! sides are aborted so `handle_conn` reaches its existing EOF cleanup;\n  44│//! (3) the buffer of the canceled op is released only after the op reports\n    │...\n  58│//! (recorded mid-frame corruption risk). Unix keeps its existing semantics for\n *59│//! healthy connections; only the poison/retire path is new.\n  60│// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n  61│\n  62│use std::io;\n    │...\n  74│/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n *75│/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n  76│/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n  77│/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n *78│// [impl->REQ-CONN-POISON-ATTRIBUTION]\n  79│static CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n  80│\n  81│/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n    │...\n  89│/// thing by both daemon entries, next to `scrub_identity_env`.\n *90│// [impl->REQ-CONN-POISON-ATTRIBUTION]\n  91│pub(crate) fn init_log_anchor() {\n  92│    let _ = MONO_ANCHOR.set(Instant::now());\n  93│}\n    │...\n  99│/// to field incident times (wall).\n*100│// [impl->REQ-CONN-POISON-ATTRIBUTION]\n 101│pub(crate) fn log_stamp() -> String {\n 102│    let wall_ms = SystemTime::now()\n 103│        .duration_since(UNIX_EPOCH)\n    │...\n 114│\n*115│/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\n 116│#[derive(Debug, PartialEq, Eq)]\n 117│pub(crate) enum ConnWrite {\n 118│    /// The frame was fully written.\n    │...\n 125│/// The serialized write gate. `half: Some` = free; `None` = a write is in\n*126│/// flight (checked out) or the conn has retired (then `poisoned` is set).\n 127│struct Gate {\n 128│    half: Option<SendHalf>,\n 129│}\n    │...\n 152│    dog_cv: Condvar,\n*153│    /// Idempotently latched on the first deadline/failure/cancel. A poisoned\n 154│    /// conn refuses every future write and is never re-gated.\n*155│    poisoned: AtomicBool,\n 156│    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\n 157│    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\n 158│    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\n    │...\n 163│    severed: AtomicBool,\n*164│    /// A retired (poisoned) half parks here so its OS handle stays alive\n 165│    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\n 166│    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\n 167│    graveyard: Mutex<Option<SendHalf>>,\n    │...\n 221│    /// accreted role/endpoint/session facts, wall + monotonic stamps.\n*222│    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 223│    fn attribution(&self) -> String {\n 224│        let label = recover(&self.label);\n 225│        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\n    │...\n 229│    /// Render the once-per-conn retirement record — the F-039 token split\n*230│    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n 231│    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 232│    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 233│    /// already-dead counterpart, the offline-peer dial churn) emits the\n    │...\n 236│    /// class is what discriminates fresh-carrier churn from interleave\n*237│    /// artifact in the field). Behavior is identical either way: poison,\n 238│    /// abort, retire, never reuse.\n*239│    // [impl->REQ-CONN-POISON-DIAL-SCOPE]\n 240│    fn render_retirement(&self, timed_out: bool, cause: Option<&io::Error>) -> String {\n 241│        if timed_out {\n 242│            format!("},"isError":false,"timestamp":1784247632893}}
{"type":"message","id":"43735522","parentId":"963e0d02","timestamp":"2026-07-17T00:20:32.898Z","message":{"role":"toolResult","toolCallId":"call_DntVd3d0gqwJ7uR4A3G0Gjlm|fc_0996e78cf50e3160016a59754ffd6081998c561cfc5556f8d9","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/tests/\n### attach.rs\n 27|use spt_daemon::attach::{\n*28|    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n*29|    AttachServeOutcome, ControllerIrrecoverablyBehind,\n 30|};\n 31|// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n 32|#[cfg(unix)]\n...\n 140|/// origin), drive `marker` through the PTY and render it back over the wire,\n*141|/// then detach and confirm the serve loop ended `Detached` (session outlives).\n 142|/// Returns the rendered viewport bytes + the (still-live) target brain so the\n 143|/// caller can assert session survival and clean up.\n 144|#[allow(clippy::too_many_arguments)]\n*145|fn attach_drive_detach(\n 146|    operator: &mut Brain,\n 147|    target_broker: &str,\n 148|    mut target: Brain,\n...\n 191|\n*192|    // Detach: the viewport ends with the session still running.\n 193|    operator\n 194|        .net_stream_send(stream_b, &[], None, true)\n*195|        .expect(\"detach\");\n 196|    let (outcome, target) = server.join().expect(\"serve thread\");\n*197|    assert_eq!(outcome, AttachServeOutcome::Detached);\n 198|    (viewport, target)\n 199|}\n 200|\n...\n 295|// endpoint\" over the byte-identical cross-node attach pump; the session outlives\n*296|// the viewport (detach never kills it).\n 297|#[test]\n 298|fn spt_hosted_bringup_then_cross_node_attach_drives_the_pty() {\n 299|    init_home();\n...\n 360|\n*361|    // Detach: the brought-up session outlives the viewport.\n 362|    operator\n 363|        .net_stream_send(stream_b, &[], None, true)\n*364|        .expect(\"detach\");\n 365|    let (outcome, mut target) = server.join().expect(\"serve thread\");\n*366|    assert_eq!(outcome, AttachServeOutcome::Detached);\n 367|    target.kill_session().expect(\"cleanup kill\");\n 368|}\n 369|\n...\n 373|// its own marker echoed remotely. The serve gate sees the handshake-proven\n*374|// origin; B detaching ends the viewport with the session still running on A.\n 375|#[test]\n 376|fn remote_attach_drives_a_real_pty_cross_daemon() {\n 377|    init_home();\n...\n 389|\n*390|    // Operator B: dial A (CROSS-NODE QUIC conn), then drive + detach over the\n 391|    // SHARED attach pump. The serve gate sees the QUIC handshake identity.\n 392|    let mut operator = connect_retry(&name_b);\n 393|    let b_node = operator\n...\n 398|    let conn = operator.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\n*399|    let (_viewport, mut target) = attach_drive_detach(\n 400|        &mut operator,\n 401|        &name_a,\n 402|        target,\n...\n 423|// `remote_attach_drives_a_real_pty_cross_daemon` call the identical\n*424|// `attach_drive_detach` helper; they differ only in how `conn_id` is acquired\n 425|// (`net_dial_loopback` vs `net_dial`) and in the expected origin. The loopback\n 426|// origin is THIS node's own id, gating same-node (access short-circuit Allow in\n 427|// production; default-open in this hermetic home — both Allow). The session\n...\n 449|    let own_origin = dialed.remote_id_hex.clone();\n*450|    let (_viewport, mut target) = attach_drive_detach(\n 451|        &mut operator,\n 452|        &name,\n 453|        target,\n...\n 470|// already-producing session — exactly the endpoint-run shape, whose transient\n*471|// cold-start spawn conn drops after `Spawned`), `become_controller` replays MANY\n 472|// frames into the SERVING brain while `serve_attach` must forward each back to the\n 473|// operator. Before the two-conn split both rode ONE broker IPC connection, so the\n 474|// broker's single-threaded per-conn handler — stuck mid-replay (broker→serve) —\n...\n 573|        .net_stream_send(stream_b, &[], None, true)\n*574|        .expect(\"detach\");\n 575|    let _ = server.join().expect(\"serve thread\");\n 576|}\n 577|\n...\n 584|// screen and paints; a fresh endpoint-resolved cold attach (from_seq=0) must land\n*585|// on the controller's legacy-baseline path (which accepts the repaint's forward\n 586|// pseudo-seq without a reject-gap fatal), render the alt viewport, and NOT dump the\n 587|// main-screen scrollback (the exact #6/#7/#8 corruption). Proves the two seams\n 588|// compose over the wire, not just in isolation. Unix-only: the child emits raw VT\n...\n 675|        .net_stream_send(stream_b, &[], None, true)\n*676|        .expect(\"detach\");\n 677|    let _ = server.join().expect(\"serve thread\");\n 678|\n 679|    // The raw wire carries the alt viewport but NOT the main scrollback…\n### brain_resume_conn_deadlock.rs\n*1|//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\n 2|//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\n 3|//!\n 4|//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n*5|//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n 6|//! onto the brain's OWN request/reply conn. A conn's send half is one\n 7|//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n*8|//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\n 9|//! `write_frame`, and the dispatch reply path (`send_frame`) needs the SAME lock.\n 10|//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\n 11|//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\n...\n 19|//! heartbeat stays LIVE while sessions flood. Against the CURRENT tree (where\n*20|//! `resume_sessions` still subscribes) they go RED (the heartbeat starves). Option A\n*21|//! (resume_sessions does NOT subscribe) turns them GREEN. The observable is the number\n 22|//! of heartbeat round-trips that COMPLETE inside a fixed window — robust to the\n 23|//! std `Mutex`'s unfairness (a slow-but-progressing heartbeat still fails the RED bar,\n 24|//! a truly deadlocked one completes ~zero).\n...\n 27|//! Shares one process-global `SPT_HOME`; spins a REAL broker + REAL flood PTY children.\n*28|//! `cargo nextest run --test brain_resume_conn_deadlock` (CI path) or\n*29|//! `cargo test --test brain_resume_conn_deadlock -- --test-threads=1`. Never a bare\n 30|//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\n 31|//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\n 32|\n...\n 48|    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n*49|    format!(\"spt-daemon-resumedeadlock-{}-{}.sock\", std::process::id(), n)\n 50|}\n 51|\n 52|static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n...\n 113|/// Spawn one flood PTY session; return (session_id, pid, spawner_conn). The conn is\n*114|/// kept alive (the spawner is the local by:None controller) but NOT drained — the\n 115|/// child floods the PTY regardless, so the broker's ring keeps advancing and the\n 116|/// brain's viewer subscription keeps receiving live output.\n 117|fn spawn_flood(name: &str, endpoint: &str) -> (u64, u32, Stream) {\n...\n 132|\n*133|/// How the probe brain resumes before running the heartbeat: the SHIPPING daemon\n*134|/// path — `CursorOnly` → `resume_session_cursors`, no subscribe. (The hazard path,\n*135|/// `resume_sessions`, is exercised directly by the deterministic zero-subscriber guard\n*136|/// `daemon_resume_leaves_zero_brain_subscribers` below, not through this probe.)\n 137|#[derive(Clone, Copy)]\n*138|enum ResumeMode {\n 139|    CursorOnly,\n 140|}\n 141|\n*142|/// Drive `run_brain`'s heartbeat pattern on a fresh `Brain` conn: resume per `mode`,\n 143|/// then loop `net_status()` + `sessions()` — the exact per-tick IPC — incrementing a\n 144|/// shared counter per COMPLETED tick, so the caller can measure liveness in a window.\n 145|/// The probe NEVER drains subscribed output (it only issues request/reply, exactly like\n...\n 149|    name: &str,\n*150|    mode: ResumeMode,\n 151|    ticks: Arc<AtomicU32>,\n 152|    stop: Arc<std::sync::atomic::AtomicBool>,\n 153|) {\n...\n 161|            // Shipping daemon path: seed cursors, NO subscribe.\n*162|            ResumeMode::CursorOnly => {\n*163|                let _ = brain.resume_session_cursors();\n 164|            }\n 165|        }\n 166|        while !stop.load(Ordering::Relaxed) {\n...\n 180|/// N flood PTY sessions are already streaming before the probe starts.\n*181|fn heartbeat_ticks_under_flood(mode: ResumeMode, n_sessions: usize, window: Duration) -> u32 {\n 182|    init_home();\n 183|    let name = unique_name();\n 184|    let broker = Broker::bind(&name).expect(\"bind broker\");\n...\n 189|\n*190|    // Stand up N flooding PTY sessions the brain will resume.\n 191|    let mut conns = Vec::new();\n 192|    let mut pids = Vec::new();\n 193|    for i in 0..n_sessions {\n### conn_blackhole_lifecycle.rs\n*1|//! MSG-IDENTITY W6 / F-039 leg (e) — the DETERMINISTIC BLACK-HOLED-CONTROLLER\n 2|//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker\n 3|//! hosting REAL PTY children over the real local IPC.\n 4|//!\n...\n 9|//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the\n*10|//! EXACT Windows named-pipe black-hole (a controller that stops draining while a\n 11|//! finite burst floods its socket, so the broker's writer parks inside\n 12|//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` — no supported\n 13|//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/\n...\n 45|//! This harness GENERALIZES `brain_decouple.rs`'s r4 gate\n*46|//! (`non_draining_controller_stall_evict_releases_writer_and_connection`, the\n 47|//! Windows-mandatory physical-retirement leg for\n 48|//! REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, which stays the primary\n 49|//! gate for that hazard). It reuses the same SEED/trigger/burst choreography,\n...\n 54|//! UNRELATED session whose viewer keeps ticking across the incident, and a fresh\n*55|//! VIEWER attach (not just a controller take) proven to receive output.\n 56|//!\n 57|//! ## Why Windows-mandatory (`#![cfg(windows)]` — the whole file)\n 58|//!\n...\n 157|/// then on a stdin trigger emits a finite ~1.4 MiB burst (1400 lines × 1023 `B`s)\n*158|/// and parks again. The seq-0 boot output gives a real `seq>0` controller frame;\n*159|/// the burst floods a non-draining controller's socket so its writer parks INSIDE\n 160|/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n 161|/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162|fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n...\n 222|/// One `KIND_SESSIONS` round-trip, returning the reply. The dispatch runs the\n*223|/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n*224|/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225|/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226|fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n 227|    let name = name.to_string();\n...\n 248|#[test]\n*249|fn blackholed_controller_lifecycle_five_invariants() {\n 250|    init_home();\n 251|    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 252|    // instead of 15 s — the structural outcome, fast + deterministic (the knob\n...\n 263|    // ── UNRELATED session U (invariant 1): a second, independent flood child on\n*264|    //    its OWN endpoint. Drop its spawner conn (so U has no controller to wedge\n 265|    //    — the PTY keeps producing headless), and attach a SEPARATE DRAINING\n 266|    //    viewer whose frame count must keep advancing ACROSS the whole incident. ──\n 267|    let (u_spawn, sid_u) = spawn_session(&name, flood_spawn_req(\"blackhole-unrelated-ep\"));\n...\n 292|    // ── BLACK-HOLED session B: the real SEED+burst PTY child from conn A, then\n*293|    //    DROP A so the ONLY persistent brain conn is the black-holed controller R. ──\n 294|    let (a, sid_b) = spawn_session(&name, seed_burst_spawn_req(\"blackhole-victim-ep\"));\n 295|    drop(a);\n 296|\n*297|    // ── Remote controller R: subscribes Control from_seq 0, drains until it has\n*298|    //    seen `SEED` (a real controller frame flowed), signals main so main can\n 299|    //    freeze the PRE-burst high-water, then on main's go fires the burst trigger,\n 300|    //    drains a few burst frames (a real seq>0 frame delivered), and STOPS\n 301|    //    reading — handing the Stream back to main UNREAD. Main holds it, never\n...\n 370|        );\n*371|        // Drain a bounded handful of burst frames so a REAL seq>0 controller frame\n 372|        // is proven delivered, then STOP reading forever (the black-hole). The\n 373|        // remaining ~1300 frames flood the now-unread socket → R-writer wedges.\n 374|        let post_deadline = Instant::now() + Duration::from_secs(5);\n...\n 408|    // Prove the burst is real: the victim's ring high-water advances ≥64 frames\n*409|    // past the trigger (independent of the wedged controller).\n 410|    let mut burst_advanced = false;\n 411|    let step_deadline = Instant::now() + Duration::from_secs(20);\n 412|    while Instant::now() < step_deadline {\n...\n 436|    //    control-plane connection (the KIND_SESSIONS reap runs the 400 ms\n*437|    //    stall-evict). Record t_release + freeze the resume cursor for the sid_b\n 438|    //    row. Accept a tally of {1, 2}: POISON-BEATS-EVICT (F-039 rider,\n 439|    //    doyle-ruled) — the physical watchdog can poison first and record the same\n 440|    //    release class, so the tally can step 0→2 when watchdog races the reap. ──\n...\n 453|                .and_then(|r| r.sessions.iter().find(|s| s.session_id == sid_b))\n*454|                .map(|s| s.resume_seq)\n 455|                .or_else(|| {\n 456|                    query_sessions(&name, Duration::from_secs(3)).and_then(|r| {\n 457|                        r.sessions\n...\n 459|                            .find(|s| s.session_id == sid_b)\n*460|                            .map(|s| s.resume_seq)\n 461|                    })\n 462|                });\n 463|            break;\n...\n 526|    //    Bounded thread + channel. ──\n*527|    let resume_seq = frozen_cursor.unwrap_or(0);\n 528|    let fv_name = name.clone();\n 529|    let (fv_tx, fv_rx) =\n 530|        std::sync::mpsc::channel::<(Option<SubscribeOutcome>, bool, Option<u64>)>();\n...\n 537|                session_id: sid_b,\n*538|                from_seq: resume_seq,\n 539|                intent: AttachIntent::Viewer,\n 540|                by: Some(\"fresh-viewer\".to_string()),\n 541|            })\n\n## spt/tests/\n### attach_wedge_e2e.rs\n 1|//! Wave 2 (v0.12.1) **L2** — REQ-HAZARD-ATTACH-WEDGE robustness gate, on the REAL\n*2|//! dummy-harness fixture + a REAL detached `spt daemon run` (no mocks, no in-proc\n 3|//! `reconcile_once`). Companion to `dummy_harness_e2e.rs` (L0 keystone bring-up).\n 4|//!\n 5|//! The hazard (v0.12.0 real-harness finding): a *legitimately* dead PTY child (real\n*6|//! crash/kill) plus an operator pump that was DROPPED without a clean detach (closed\n 7|//! tab / `taskkill /F`) must NOT wedge the broker — every NEW attach / `endpoint run`\n 8|//! must still be served, the dead endpoint must be offlined within a reconcile tick,\n 9|//! and `daemon stop` must complete bounded.\n...\n 116|/// the window — the strong \"this endpoint is being SERVED\" proof. The rc child is then\n*117|/// reaped ABRUPTLY (`kill()` = TerminateProcess, no clean detach record) — exactly the\n 118|/// closed-tab / crashed-operator case (a DROPPED pump), which is what makes this the\n 119|/// L2 wedge trigger when called on the victim. The reader thread observes the killed\n 120|/// pipe (read → 0/Err) and exits.\n...\n 127|        .stderr(Stdio::null())\n*128|        .stdin(Stdio::piped()) // hold stdin open so the pump does not EOF-detach early\n 129|        .spawn()\n 130|        .expect(\"spawn spt rc <id>\");\n 131|    let _rc_stdin = rc.stdin.take();\n...\n 152|    let saw = rx.recv_timeout(window).unwrap_or(false);\n*153|    // Abrupt reap = the dropped-pump wedge trigger (no clean detach). `kill()` is\n 154|    // idempotent on an already-exited child; `wait()` reaps the handle (clippy\n 155|    // zombie_processes).\n 156|    let _ = rc.kill();\n...\n 233|    // rc_sees_tick reaps its rc ABRUPTLY before returning = the dropped-pump half of\n*234|    // the wedge trigger (no clean detach record).\n 235|    let victim_served = rc_sees_tick(&spt_bin, home.path(), victim, Duration::from_secs(10));\n 236|\n 237|    // ── (5) THE WEDGE TRIGGER (second half): kill the harness PTY child abruptly (a\n 238|    //    legitimately dead child). Together with the just-dropped operator pump this is\n*239|    //    exactly the closed-tab + crash case — no clean teardown, no detach record. ──\n 240|    if let Some(p) = harness_pid1 {\n 241|        kill_pid(p);\n 242|    }\n### resume_no_control_steal_e2e.rs\n*1|//! UPDATE-WEDGE round 2 (REQ-BRAIN-RESUME-NO-CONTROL-STEAL) — the multi-broker-PTY-\n 2|//! session RESPAWN rig (doyle-ruled 2026-07-09). This is the coverage the counter-54\n 3|//! single-black-holed-session A'-rig (`trial_drain_drive_e2e.rs`) LACKED: N REAL broker\n*4|//! PTY sessions, each with its OWN local controller producing output, PRESENT at a real\n 5|//! brain respawn — the exact field condition (7 spt-hosted PTYs at a `fetch --apply`) the\n*6|//! resume-steal bug needed and the single-session rig never exercised.\n 7|//!\n 8|//! ## The field incident this locks down (root, log-anchored — docs/UPDATE-WEDGE-2-RCA.md)\n 9|//!\n...\n 12|//! retake. ROOT (SME docs/UPDATE-WEDGE-2-SME-todlando.md): `run_brain` calls\n*13|//! `resume_sessions()` at boot, which re-attached EVERY session `KIND_SESSIONS` returns as\n 14|//! `subscribe_with(AttachIntent::Control, by:None)`. The docstring's \"a None identity never\n 15|//! displaces (falls back to viewer)\" is a MYTH for FREE / SAME-LOCAL-IDENTITY slots:\n*16|//! `resolve_subscribe` (broker.rs) stall-evicts first, then `become_controller` when the\n 17|//! slot is free OR the incumbent is also local (`None == None`) — viewer-fallback fires\n*18|//! ONLY against a DIFFERENT *remote* controller. So the respawned brain STOLE the by:None\n*19|//! controller of every free/local-controlled session (incl. the operator's local `spt rc`\n*20|//! and each spt-hosted PTY's spawn-time controller), which the daemon brain never drains\n 21|//! (it hosts no PTY sessions — brainproc.rs) → the session FREEZES the instant the brain takes\n 22|//! over + head-of-line-blocked the shared brain↔broker conn on the N-session replay → every\n 23|//! journaled `rc` retake deadlined (Failure B).\n 24|//!\n*25|//! All N controllers on the field box were LOCAL (by:None — the operator's local `spt rc` +\n*26|//! each PTY's spawn-time controller). The respawn's by:None Control re-attach →\n*27|//! `become_controller` SILENTLY re-takes (same-local-identity `None==None`, the gate-#7 path\n*28|//! that fires NO `Displaced` notice) → the incumbent local controller is ORPHANED: the broker\n 29|//! stops writing its output but does NOT close the conn → no EOF, no `PumpEnd`, no 30s\n*30|//! reconnect/detach splash (that fires only on `BrokerGone`/`Severed`). So the session FREEZES\n 31|//! (output stops entirely, not just control) the instant the brain takes over. The stall-evict\n 32|//! (>15s) is a SEPARATE, LATER, output-dependent symptom: sessions actively producing output\n 33|//! make the stolen writer block >15s → evict (the field's 5); idle sessions never block →\n...\n 35|//! ACTIVE-vs-IDLE, not remote-vs-local. This rig asserts the IMMEDIATE, deterministic signal —\n*36|//! a held controller's output-tick freezes on the steal (RED) vs keeps ticking under the Viewer\n 37|//! fix (GREEN) — NOT the flaky, timing-dependent stall-evict.\n 38|//!\n 39|//! ## The FIX and what this rig discriminates\n 40|//!\n*41|//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` no longer\n*42|//! calls `resume_sessions` at all — it uses the cursor-only `resume_session_cursors` (NO\n 43|//! subscribe), which supersedes the Viewer re-attach as the no-steal cure (no attach ⇒ no\n 44|//! steal, a fortiori) AND fixes the SharedSend self-deadlock. This rig's no-steal\n*45|//! discriminator (the operator's `A_i` controllers keep ticking across the swap) therefore\n 46|//! passes for the stronger reason — the daemon brain never attaches. The Viewer text below\n 47|//! describes the round-2 shape this superseded; the assertions are unchanged and still hold.\n 48|//!\n*49|//! `resume_sessions` (still used by DRAINING callers) re-attaches as **`AttachIntent::Viewer`**\n 50|//! (a viewer never owns `driven_by` and is never stall-evicted). The RED→GREEN discriminator\n 51|//! this rig asserts is\n 52|//! the STEAL itself — the deterministic ROOT that CAUSES both A and B — observed as OUTPUT\n...\n 54|//!\n*55|//! - Each session is spawned by a controller conn (`A_i`) the test HOLDS; `dispatch_spawn`\n*56|//!   pre-attaches `A_i` as the session's LOCAL (by:None) controller. A reader thread on\n 57|//!   `A_i` counts `KIND_OUTPUT` frames from a child that ticks forever (~150ms)."}],"details":{"scopePath":"crates/spt-daemon/tests/attach.rs, crates/spt-daemon/tests/brain_resume_conn_deadlock.rs, crates/spt-daemon/tests/conn_blackhole_lifecycle.rs, crates/spt/tests/attach_wedge_e2e.rs, crates/spt/tests/resume_no_control_steal_e2e.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":87,"fileCount":5,"files":["crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","crates/spt/tests/attach_wedge_e2e.rs","crates/spt/tests/resume_no_control_steal_e2e.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/attach.rs","count":20},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","count":20},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","count":20},{"path":"crates/spt/tests/attach_wedge_e2e.rs","count":7},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/tests/\n### attach.rs\n  27│use spt_daemon::attach::{\n *28│    read_controller_event_resuming, request_attach, send_attach_input, serve_attach,\n *29│    AttachServeOutcome, ControllerIrrecoverablyBehind,\n  30│};\n  31│// Only the unix-gated cross-node alt-screen int uses the endpoint-leg request.\n  32│#[cfg(unix)]\n    │...\n 140│/// origin), drive `marker` through the PTY and render it back over the wire,\n*141│/// then detach and confirm the serve loop ended `Detached` (session outlives).\n 142│/// Returns the rendered viewport bytes + the (still-live) target brain so the\n 143│/// caller can assert session survival and clean up.\n 144│#[allow(clippy::too_many_arguments)]\n*145│fn attach_drive_detach(\n 146│    operator: &mut Brain,\n 147│    target_broker: &str,\n 148│    mut target: Brain,\n    │...\n 191│\n*192│    // Detach: the viewport ends with the session still running.\n 193│    operator\n 194│        .net_stream_send(stream_b, &[], None, true)\n*195│        .expect(\"detach\");\n 196│    let (outcome, target) = server.join().expect(\"serve thread\");\n*197│    assert_eq!(outcome, AttachServeOutcome::Detached);\n 198│    (viewport, target)\n 199│}\n 200│\n    │...\n 295│// endpoint\" over the byte-identical cross-node attach pump; the session outlives\n*296│// the viewport (detach never kills it).\n 297│#[test]\n 298│fn spt_hosted_bringup_then_cross_node_attach_drives_the_pty() {\n 299│    init_home();\n    │...\n 360│\n*361│    // Detach: the brought-up session outlives the viewport.\n 362│    operator\n 363│        .net_stream_send(stream_b, &[], None, true)\n*364│        .expect(\"detach\");\n 365│    let (outcome, mut target) = server.join().expect(\"serve thread\");\n*366│    assert_eq!(outcome, AttachServeOutcome::Detached);\n 367│    target.kill_session().expect(\"cleanup kill\");\n 368│}\n 369│\n    │...\n 373│// its own marker echoed remotely. The serve gate sees the handshake-proven\n*374│// origin; B detaching ends the viewport with the session still running on A.\n 375│#[test]\n 376│fn remote_attach_drives_a_real_pty_cross_daemon() {\n 377│    init_home();\n    │...\n 389│\n*390│    // Operator B: dial A (CROSS-NODE QUIC conn), then drive + detach over the\n 391│    // SHARED attach pump. The serve gate sees the QUIC handshake identity.\n 392│    let mut operator = connect_retry(&name_b);\n 393│    let b_node = operator\n    │...\n 398│    let conn = operator.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1))).expect(\"dial\");\n*399│    let (_viewport, mut target) = attach_drive_detach(\n 400│        &mut operator,\n 401│        &name_a,\n 402│        target,\n    │...\n 423│// `remote_attach_drives_a_real_pty_cross_daemon` call the identical\n*424│// `attach_drive_detach` helper; they differ only in how `conn_id` is acquired\n 425│// (`net_dial_loopback` vs `net_dial`) and in the expected origin. The loopback\n 426│// origin is THIS node's own id, gating same-node (access short-circuit Allow in\n 427│// production; default-open in this hermetic home — both Allow). The session\n    │...\n 449│    let own_origin = dialed.remote_id_hex.clone();\n*450│    let (_viewport, mut target) = attach_drive_detach(\n 451│        &mut operator,\n 452│        &name,\n 453│        target,\n    │...\n 470│// already-producing session — exactly the endpoint-run shape, whose transient\n*471│// cold-start spawn conn drops after `Spawned`), `become_controller` replays MANY\n 472│// frames into the SERVING brain while `serve_attach` must forward each back to the\n 473│// operator. Before the two-conn split both rode ONE broker IPC connection, so the\n 474│// broker's single-threaded per-conn handler — stuck mid-replay (broker→serve) —\n    │...\n 573│        .net_stream_send(stream_b, &[], None, true)\n*574│        .expect(\"detach\");\n 575│    let _ = server.join().expect(\"serve thread\");\n 576│}\n 577│\n    │...\n 584│// screen and paints; a fresh endpoint-resolved cold attach (from_seq=0) must land\n*585│// on the controller's legacy-baseline path (which accepts the repaint's forward\n 586│// pseudo-seq without a reject-gap fatal), render the alt viewport, and NOT dump the\n 587│// main-screen scrollback (the exact #6/#7/#8 corruption). Proves the two seams\n 588│// compose over the wire, not just in isolation. Unix-only: the child emits raw VT\n    │...\n 675│        .net_stream_send(stream_b, &[], None, true)\n*676│        .expect(\"detach\");\n 677│    let _ = server.join().expect(\"serve thread\");\n 678│\n 679│    // The raw wire carries the alt viewport but NOT the main scrollback…\n### brain_resume_conn_deadlock.rs\n  *1│//! UPDATE-WEDGE round-3 CONFIRMER RIG (REQ-BRAIN-RESUME-NO-CONN-DEADLOCK) —\n   2│//! doyle RULING 3 (docs/UPDATE-WEDGE-2-ROUND3-RULING.md ## RULING 3).\n   3│//!\n   4│//! Proves the code-read root (docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md), DEAD-PEER\n  *5│//! INDEPENDENT: the daemon brain's `resume_sessions` subscribes broker PTY sessions\n   6│//! onto the brain's OWN request/reply conn. A conn's send half is one\n   7│//! `SharedSend = Arc<Mutex<SendHalf>>`; the subscriber writer threads\n  *8│//! (`viewer_writer`/`controller_writer`) hold `send.lock()` ACROSS a BLOCKING\n   9│//! `write_frame`, and the dispatch reply path (`send_frame`) needs the SAME lock.\n  10│//! The daemon brain hosts no PTY sessions, so `run_brain` never drains that output —\n  11│//! it reads the conn only during its 500ms-heartbeat `net_status()`/`sessions()`\n    │...\n  19│//! heartbeat stays LIVE while sessions flood. Against the CURRENT tree (where\n *20│//! `resume_sessions` still subscribes) they go RED (the heartbeat starves). Option A\n *21│//! (resume_sessions does NOT subscribe) turns them GREEN. The observable is the number\n  22│//! of heartbeat round-trips that COMPLETE inside a fixed window — robust to the\n  23│//! std `Mutex`'s unfairness (a slow-but-progressing heartbeat still fails the RED bar,\n  24│//! a truly deadlocked one completes ~zero).\n    │...\n  27│//! Shares one process-global `SPT_HOME`; spins a REAL broker + REAL flood PTY children.\n *28│//! `cargo nextest run --test brain_resume_conn_deadlock` (CI path) or\n *29│//! `cargo test --test brain_resume_conn_deadlock -- --test-threads=1`. Never a bare\n  30│//! `cargo test` on Windows. All spawned pids reaped SCOPED. Do NOT rename to include\n  31│//! \"update\"/\"setup\"/\"install\" (Windows installer-detection → os error 740).\n  32│\n    │...\n  48│    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n *49│    format!(\"spt-daemon-resumedeadlock-{}-{}.sock\", std::process::id(), n)\n  50│}\n  51│\n  52│static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();\n    │...\n 113│/// Spawn one flood PTY session; return (session_id, pid, spawner_conn). The conn is\n*114│/// kept alive (the spawner is the local by:None controller) but NOT drained — the\n 115│/// child floods the PTY regardless, so the broker's ring keeps advancing and the\n 116│/// brain's viewer subscription keeps receiving live output.\n 117│fn spawn_flood(name: &str, endpoint: &str) -> (u64, u32, Stream) {\n    │...\n 132│\n*133│/// How the probe brain resumes before running the heartbeat: the SHIPPING daemon\n*134│/// path — `CursorOnly` → `resume_session_cursors`, no subscribe. (The hazard path,\n*135│/// `resume_sessions`, is exercised directly by the deterministic zero-subscriber guard\n*136│/// `daemon_resume_leaves_zero_brain_subscribers` below, not through this probe.)\n 137│#[derive(Clone, Copy)]\n*138│enum ResumeMode {\n 139│    CursorOnly,\n 140│}\n 141│\n*142│/// Drive `run_brain`'s heartbeat pattern on a fresh `Brain` conn: resume per `mode`,\n 143│/// then loop `net_status()` + `sessions()` — the exact per-tick IPC — incrementing a\n 144│/// shared counter per COMPLETED tick, so the caller can measure liveness in a window.\n 145│/// The probe NEVER drains subscribed output (it only issues request/reply, exactly like\n    │...\n 149│    name: &str,\n*150│    mode: ResumeMode,\n 151│    ticks: Arc<AtomicU32>,\n 152│    stop: Arc<std::sync::atomic::AtomicBool>,\n 153│) {\n    │...\n 161│            // Shipping daemon path: seed cursors, NO subscribe.\n*162│            ResumeMode::CursorOnly => {\n*163│                let _ = brain.resume_session_cursors();\n 164│            }\n 165│        }\n 166│        while !stop.load(Ordering::Relaxed) {\n    │...\n 180│/// N flood PTY sessions are already streaming before the probe starts.\n*181│fn heartbeat_ticks_under_flood(mode: ResumeMode, n_sessions: usize, window: Duration) -> u32 {\n 182│    init_home();\n 183│    let name = unique_name();\n 184│    let broker = Broker::bind(&name).expect(\"bind broker\");\n    │...\n 189│\n*190│    // Stand up N flooding PTY sessions the brain will resume.\n 191│    let mut conns = Vec::new();\n 192│    let mut pids = Vec::new();\n 193│    for i in 0..n_sessions {\n### conn_blackhole_lifecycle.rs\n  *1│//! MSG-IDENTITY W6 / F-039 leg (e) — the DETERMINISTIC BLACK-HOLED-CONTROLLER\n   2│//! LIFECYCLE HARNESS (REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS), on a REAL broker\n   3│//! hosting REAL PTY children over the real local IPC.\n   4│//!\n    │...\n   9│//! conformance rig for the round-4 `SharedSend` fix class. It reproduces the\n *10│//! EXACT Windows named-pipe black-hole (a controller that stops draining while a\n  11│//! finite burst floods its socket, so the broker's writer parks inside\n  12│//! `write_frame` on `WriteFileEx`+`SleepEx(INFINITE, alertable)` — no supported\n  13│//! write timeout) and, against CURRENT v0.30.6 semantics (`crates/spt-daemon/\n    │...\n  45│//! This harness GENERALIZES `brain_decouple.rs`'s r4 gate\n *46│//! (`non_draining_controller_stall_evict_releases_writer_and_connection`, the\n  47│//! Windows-mandatory physical-retirement leg for\n  48│//! REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, which stays the primary\n  49│//! gate for that hazard). It reuses the same SEED/trigger/burst choreography,\n    │...\n  54│//! UNRELATED session whose viewer keeps ticking across the incident, and a fresh\n *55│//! VIEWER attach (not just a controller take) proven to receive output.\n  56│//!\n  57│//! ## Why Windows-mandatory (`#![cfg(windows)]` — the whole file)\n  58│//!\n    │...\n 157│/// then on a stdin trigger emits a finite ~1.4 MiB burst (1400 lines × 1023 `B`s)\n*158│/// and parks again. The seq-0 boot output gives a real `seq>0` controller frame;\n*159│/// the burst floods a non-draining controller's socket so its writer parks INSIDE\n 160│/// `write_frame` — the exact black-hole the RCA pinned. Verbatim sibling of the\n 161│/// r4 gate's `seed_burst_spawn_req` in `brain_decouple.rs`.\n 162│fn seed_burst_spawn_req(endpoint: &str) -> SpawnReq {\n    │...\n 222│/// One `KIND_SESSIONS` round-trip, returning the reply. The dispatch runs the\n*223│/// `reap_dead_controller` → time-based `stall_evict_controller`, so polling this\n*224│/// both DRIVES the logical eviction and reads the frozen `resume_seq` cursor.\n 225│/// Bounded thread + channel. Verbatim sibling of the r4 gate helper.\n 226│fn query_sessions(name: &str, deadline: Duration) -> Option<SessionsReply> {\n 227│    let name = name.to_string();\n    │...\n 248│#[test]\n*249│fn blackholed_controller_lifecycle_five_invariants() {\n 250│    init_home();\n 251│    // Shrink the brain-write deadline so the wedged writer is \"stalled\" in ~400 ms\n 252│    // instead of 15 s — the structural outcome, fast + deterministic (the knob\n    │...\n 263│    // ── UNRELATED session U (invariant 1): a second, independent flood child on\n*264│    //    its OWN endpoint. Drop its spawner conn (so U has no controller to wedge\n 265│    //    — the PTY keeps producing headless), and attach a SEPARATE DRAINING\n 266│    //    viewer whose frame count must keep advancing ACROSS the whole incident. ──\n 267│    let (u_spawn, sid_u) = spawn_session(&name, flood_spawn_req(\"blackhole-unrelated-ep\"));\n    │...\n 292│    // ── BLACK-HOLED session B: the real SEED+burst PTY child from conn A, then\n*293│    //    DROP A so the ONLY persistent brain conn is the black-holed controller R. ──\n 294│    let (a, sid_b) = spawn_session(&name, seed_burst_spawn_req(\"blackhole-victim-ep\"));\n 295│    drop(a);\n 296│\n*297│    // ── Remote controller R: subscribes Control from_seq 0, drains until it has\n*298│    //    seen `SEED` (a real controller frame flowed), signals main so main can\n 299│    //    freeze the PRE-burst high-water, then on main's go fires the burst trigger,\n 300│    //    drains a few burst frames (a real seq>0 frame delivered), and STOPS\n 301│    //    reading — handing the Stream back to main UNREAD. Main holds it, never\n    │...\n 370│        );\n*371│        // Drain a bounded handful of burst frames so a REAL seq>0 controller frame\n 372│        // is proven delivered, then STOP reading forever (the black-hole). The\n 373│        // remaining ~1300 frames flood the now-unread socket → R-writer wedges.\n 374│        let post_deadline = Instant::now() + Duration::from_secs(5);\n    │...\n 408│    // Prove the burst is real: the victim's ring high-water advances ≥64 frames\n*409│    // past the trigger (independent of the wedged controller).\n 410│    let mut burst_advanced = false;\n 411│    let step_deadline = Instant::now() + Duration::from_secs(20);\n 412│    while Instant::now() < step_deadline {\n    │...\n 436│    //    control-plane connection (the KIND_SESSIONS reap runs the 400 ms\n*437│    //    stall-evict). Record t_release + freeze the resume cursor for the sid_b\n 438│    //    row. Accept a tally of {1, 2}: POISON-BEATS-EVICT (F-039 rider,\n 439│    //    doyle-ruled) — the physical watchdog can poison first and record the same\n 440│    //    release class, so the tally can step 0→2 when watchdog races the reap. ──\n    │...\n 453│                .and_then(|r| r.sessions.iter().find(|s| s.session_id == sid_b))\n*454│                .map(|s| s.resume_seq)\n 455│                .or_else(|| {\n 456│                    query_sessions(&name, Duration::from_secs(3)).and_then(|r| {\n 457│                        r.sessions\n    │...\n 459│                            .find(|s| s.session_id == sid_b)\n*460│                            .map(|s| s.resume_seq)\n 461│                    })\n 462│                });\n 463│            break;\n    │...\n 526│    //    Bounded thread + channel. ──\n*527│    let resume_seq = frozen_cursor.unwrap_or(0);\n 528│    let fv_name = name.clone();\n 529│    let (fv_tx, fv_rx) =\n 530│        std::sync::mpsc::channel::<(Option<SubscribeOutcome>, bool, Option<u64>)>();\n    │...\n 537│                session_id: sid_b,\n*538│                from_seq: resume_seq,\n 539│                intent: AttachIntent::Viewer,\n 540│                by: Some(\"fresh-viewer\".to_string()),\n 541│            })\n\n## spt/tests/\n### attach_wedge_e2e.rs\n   1│//! Wave 2 (v0.12.1) **L2** — REQ-HAZARD-ATTACH-WEDGE robustness gate, on the REAL\n  *2│//! dummy-harness fixture + a REAL detached `spt daemon run` (no mocks, no in-proc\n   3│//! `reconcile_once`). Companion to `dummy_harness_e2e.rs` (L0 keystone bring-up).\n   4│//!\n   5│//! The hazard (v0.12.0 real-harness finding): a *legitimately* dead PTY child (real\n  *6│//! crash/kill) plus an operator pump that was DROPPED without a clean detach (closed\n   7│//! tab / `taskkill /F`) must NOT wedge the broker — every NEW attach / `endpoint run`\n   8│//! must still be served, the dead endpoint must be offlined within a reconcile tick,\n   9│//! and `daemon stop` must complete bounded.\n    │...\n 116│/// the window — the strong \"this endpoint is being SERVED\" proof. The rc child is then\n*117│/// reaped ABRUPTLY (`kill()` = TerminateProcess, no clean detach record) — exactly the\n 118│/// closed-tab / crashed-operator case (a DROPPED pump), which is what makes this the\n 119│/// L2 wedge trigger when called on the victim. The reader thread observes the killed\n 120│/// pipe (read → 0/Err) and exits.\n    │...\n 127│        .stderr(Stdio::null())\n*128│        .stdin(Stdio::piped()) // hold stdin open so the pump does not EOF-detach early\n 129│        .spawn()\n 130│        .expect(\"spawn spt rc <id>\");\n 131│    let _rc_stdin = rc.stdin.take();\n    │...\n 152│    let saw = rx.recv_timeout(window).unwrap_or(false);\n*153│    // Abrupt reap = the dropped-pump wedge trigger (no clean detach). `kill()` is\n 154│    // idempotent on an already-exited child; `wait()` reaps the handle (clippy\n 155│    // zombie_processes).\n 156│    let _ = rc.kill();\n    │...\n 233│    // rc_sees_tick reaps its rc ABRUPTLY before returning = the dropped-pump half of\n*234│    // the wedge trigger (no clean detach record).\n 235│    let victim_served = rc_sees_tick(&spt_bin, home.path(), victim, Duration::from_secs(10));\n 236│\n 237│    // ── (5) THE WEDGE TRIGGER (second half): kill the harness PTY child abruptly (a\n 238│    //    legitimately dead child). Together with the just-dropped operator pump this is\n*239│    //    exactly the closed-tab + crash case — no clean teardown, no detach record. ──\n 240│    if let Some(p) = harness_pid1 {\n 241│        kill_pid(p);\n 242│    }\n### resume_no_control_steal_e2e.rs\n *1│//! UPDATE-WEDGE round 2 (REQ-BRAIN-RESUME-NO-CONTROL-STEAL) — the multi-broker-PTY-\n  2│//! session RESPAWN rig (doyle-ruled 2026-07-09). This is the coverage the counter-54\n  3│//! single-black-holed-session A'-rig (`trial_drain_drive_e2e.rs`) LACKED: N REAL broker\n *4│//! PTY sessions, each with its OWN local controller producing output, PRESENT at a real\n  5│//! brain respawn — the exact field condition (7 spt-hosted PTYs at a `fetch --apply`) the\n *6│//! resume-steal bug needed and the single-session rig never exercised.\n  7│//!\n  8│//! ## The field incident this locks down (root, log-anchored — docs/UPDATE-WEDGE-2-RCA.md)\n  9│//!\n   │...\n 12│//! retake. ROOT (SME docs/UPDATE-WEDGE-2-SME-todlando.md): `run_brain` calls\n*13│//! `resume_sessions()` at boot, which re-attached EVERY session `KIND_SESSIONS` returns as\n 14│//! `subscribe_with(AttachIntent::Control, by:None)`. The docstring's \"a None identity never\n 15│//! displaces (falls back to viewer)\" is a MYTH for FREE / SAME-LOCAL-IDENTITY slots:\n*16│//! `resolve_subscribe` (broker.rs) stall-evicts first, then `become_controller` when the\n 17│//! slot is free OR the incumbent is also local (`None == None`) — viewer-fallback fires\n*18│//! ONLY against a DIFFERENT *remote* controller. So the respawned brain STOLE the by:None\n*19│//! controller of every free/local-controlled session (incl. the operator's local `spt rc`\n*20│//! and each spt-hosted PTY's spawn-time controller), which the daemon brain never drains\n 21│//! (it hosts no PTY sessions — brainproc.rs) → the session FREEZES the instant the brain takes\n 22│//! over + head-of-line-blocked the shared brain↔broker conn on the N-session replay → every\n 23│//! journaled `rc` retake deadlined (Failure B).\n 24│//!\n*25│//! All N controllers on the field box were LOCAL (by:None — the operator's local `spt rc` +\n*26│//! each PTY's spawn-time controller). The respawn's by:None Control re-attach →\n*27│//! `become_controller` SILENTLY re-takes (same-local-identity `None==None`, the gate-#7 path\n*28│//! that fires NO `Displaced` notice) → the incumbent local controller is ORPHANED: the broker\n 29│//! stops writing its output but does NOT close the conn → no EOF, no `PumpEnd`, no 30s\n*30│//! reconnect/detach splash (that fires only on `BrokerGone`/`Severed`). So the session FREEZES\n 31│//! (output stops entirely, not just control) the instant the brain takes over. The stall-evict\n 32│//! (>15s) is a SEPARATE, LATER, output-dependent symptom: sessions actively producing output\n 33│//! make the stolen writer block >15s → evict (the field's 5); idle sessions never block →\n   │...\n 35│//! ACTIVE-vs-IDLE, not remote-vs-local. This rig asserts the IMMEDIATE, deterministic signal —\n*36│//! a held controller's output-tick freezes on the steal (RED) vs keeps ticking under the Viewer\n 37│//! fix (GREEN) — NOT the flaky, timing-dependent stall-evict.\n 38│//!\n 39│//! ## The FIX and what this rig discriminates\n 40│//!\n*41│//! NOTE (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK): `run_brain` no longer\n*42│//! calls `resume_sessions` at all — it uses the cursor-only `resume_session_cursors` (NO\n 43│//! subscribe), which supersedes the Viewer re-attach as the no-steal cure (no attach ⇒ no\n 44│//! steal, a fortiori) AND fixes the SharedSend self-deadlock. This rig's no-steal\n*45│//! discriminator (the operator's `A_i` controllers keep ticking across the swap) therefore\n 46│//! passes for the stronger reason — the daemon brain never attaches. The Viewer text below\n 47│//! describes the round-2 shape this superseded; the assertions are unchanged and still hold.\n 48│//!\n*49│//! `resume_sessions` (still used by DRAINING callers) re-attaches as **`AttachIntent::Viewer`**\n 50│//! (a viewer never owns `driven_by` and is never stall-evicted). The RED→GREEN discriminator\n 51│//! this rig asserts is\n 52│//! the STEAL itself — the deterministic ROOT that CAUSES both A and B — observed as OUTPUT\n   │...\n 54│//!\n*55│//! - Each session is spawned by a controller conn (`A_i`) the test HOLDS; `dispatch_spawn`\n*56│//!   pre-attaches `A_i` as the session's LOCAL (by:None) controller. A reader thread on\n 57│//!   `A_i` counts `KIND_OUTPUT` frames from a child that ticks forever (~150ms)."},"isError":false,"timestamp":1784247632898}}
{"type":"message","id":"9ca596bc","parentId":"43735522","timestamp":"2026-07-17T00:20:43.192Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Clarifying dispatcher bug and session fixes**"},{"type":"toolCall","id":"call_Ri1ucLSkdgoz9xlii4b0GBnf|fc_0996e78cf50e3160016a59755524508199b2f83e6ccaa9b7a3","name":"irc","arguments":{"i":"Sending field causal interpretation","op":"send","to":"Main","message":"Refined field tie: conn `b095b` poison is a delayed secondary observable, not refresh root. The old brain-owned serve_attach IPC conn remains non-draining/half-open after cycle; an idle session emits no write, so watchdog is unarmed for ~7m. At 10:51:24 first post-cycle PTY output calls controller_writer→BrokerConn::write, arming 15s bound; poison at 10:51:40 is exactly deadline, then EOF cleanup detaches roles. Fresh `sessions()` polls cannot evict before a write is in flight (`write_blocked_since=None` while idle). Primary immediate freeze is stale finished Attach replay: new dispatcher enumerates append-only peer streams; same-origin attach_as silently become_controller, then replayed EOF detach_if clears controller. The legitimate stream remains open but no longer selected, hence no EOF and detach+fresh rc retakes. Current ADR-0038/redispatch.rs fixes/guards it.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":32606,"output":333,"cacheRead":97792,"cacheWrite":0,"totalTokens":130731,"reasoningTokens":78,"cost":{"input":0.16303,"output":0.00999,"cacheRead":0.048895999999999995,"cacheWrite":0,"total":0.221916}},"stopReason":"toolUse","timestamp":1784247632939,"responseId":"resp_0996e78cf50e3160016a5975515f00819997b099b2e9eb2f91","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a597552cde481999571ddd5a5d55334","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXVV9GA6AVEiW9Ye-yq7N3GIgT5zsOeYw6CpypUPGj72Ukofjs2vFqFVvZLv_PWp9yjPYueUSQkBl1omnEfDjaq36JhF_ofrUeuFPgDracPigReTzRB1NntHNO7KS5QEBLvl8tbnOrGmx7FHqZyg9wb0QFDl6owF5D5lSvDT_fnNZdXEjMxuimWJGj3lr5CffFV-MdjWkju4ejFjGlcCwmWcly6g6VijIqfTrDHiTphjXLslII99qF74kKA2Q-wV2n_Pbi8OPOCgLH706sHJK4NJBqz6KyGJ-ypyv0glkoL4E0Io9kbEBxByleCRDKZCKI8eJIgm8rDBBNsTjxVnxyPJOfcaq8IgCv7ezjcX6y_5mLPYXsgALcFoMbX9XYGJxIiT9nYkBKbKFEs0vvazN1DxAtaI26nprff3PemA1ZqkElOEb2xshEYIr-ymXaTMKZdagM-mluXjIjhQW9sWQcJJaNq6DcM8LqG4YYLC16IIiYkS_K5Rgi43Kc7EYbkoYXHfjO5RiiAxXi6rv_OrlsynZOVILs1eVhntntDcq9QRzLRsY3BjNmxmFb4Jy029lFlWjx3ulgZ1jDnPUkSC6JNvwlKQkBKuC4XB1UikSFfbwj1vCqYSjJraF_9nw8Ba8mJh9Cw36st7gzY7QKPBfyM05Ls9E6JNuAXjzzcWTLm0wNQg1OnlurZq6JBI1-l_JJtasHrHaar-PMtBwZ3khIN6KmZPYrtGnbHm5lzgHxdswdEhJcupeEIPrMhF8RgcIHACGMVN3qlJlfYEQqqA6mrc5KLegKkKEDBgeZI557w5lgZUaBc-vScRPgBd2Bzcl6lXOTLLULC_qhA8-r521YD0IrFUvc3sbv3pokkBuESYGjbdoLZ2S-9tq2DzGWNEPEbumH1U_PCno27shmkx-NiEBD3dxdwJM9MzdADiANU312v_PZp82T2pkOllSnBHX8JU4rUd26yLgDn36-8Av837gJrvZtqDHhe8v4Yj0-AAWDbcPC6JGcGO1tJ90zOR_FJFYTLr0Xxh3xw7su75HyYYWjeMP_zxBy3pYCLWsXX81wKtXvh0jctpp1PCnxKatH7kiWcbstFdCsefPJlyAm8VEja0EdTMrCSS6HgGKeXQJGEgLGCOIu5YfWlyR7ods2jGF8GYy-kAoywA-beLSUXI4U9mED4R5AXNn4BfJJJqF2N-8BOO3n_NxJJyCrrc-tqr0hQjOcfTCpxodO-SVIK1gOQEBQAu__slilMv6GSXseRwTym9SCEFNM5GXvA_amsi3mfJrzUuEQjxgdQt94q0o35Lc37H_yCQx2lB_b_S3kBCwyUprOQIrkGLV1YhXlkhkFAKUu2zjhP_MbESs0RAtWDgt4tDqP36-8g3huuB-_PJ_Qv3Hgsf6TLv3IOyp2_m9wrzpWomFYcsfOsjKqlswvnfZKmugPvzmYQqVaSE3tl5Yr21OlGz3XvNHpQhoF39","summary":[{"type":"summary_text","text":"**Clarifying dispatcher bug and session fixes**"}]},{"id":"fc_0996e78cf50e3160016a59755524508199b2f83e6ccaa9b7a3","type":"function_call","status":"completed","arguments":"{\"i\":\"Sending field causal interpretation\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Refined field tie: conn `b095b` poison is a delayed secondary observable, not refresh root. The old brain-owned serve_attach IPC conn remains non-draining/half-open after cycle; an idle session emits no write, so watchdog is unarmed for ~7m. At 10:51:24 first post-cycle PTY output calls controller_writer→BrokerConn::write, arming 15s bound; poison at 10:51:40 is exactly deadline, then EOF cleanup detaches roles. Fresh `sessions()` polls cannot evict before a write is in flight (`write_blocked_since=None` while idle). Primary immediate freeze is stale finished Attach replay: new dispatcher enumerates append-only peer streams; same-origin attach_as silently become_controller, then replayed EOF detach_if clears controller. The legitimate stream remains open but no longer selected, hence no EOF and detach+fresh rc retakes. Current ADR-0038/redispatch.rs fixes/guards it.\" ,\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_Ri1ucLSkdgoz9xlii4b0GBnf","name":"irc"}]},"duration":10251.96099999547,"ttft":1857.794599995017,"contextSnapshot":{"promptTokens":130398,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Ri1ucLSkdgoz9xlii4b0GBnf|fc_0996e78cf50e3160016a59755524508199b2f83e6ccaa9b7a3","toolName":"irc","startedAt":"2026-07-17T00:20:43.192Z","intent":"Sending field causal interpretation"},"id":"4544a3a3","parentId":"9ca596bc","timestamp":"2026-07-17T00:20:43.192Z"}
{"type":"message","id":"0111e686","parentId":"4544a3a3","timestamp":"2026-07-17T00:20:43.193Z","message":{"role":"toolResult","toolCallId":"call_Ri1ucLSkdgoz9xlii4b0GBnf|fc_0996e78cf50e3160016a59755524508199b2f83e6ccaa9b7a3","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"PtyControl","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784247643192}}
{"type":"message","id":"aef4385e","parentId":"0111e686","timestamp":"2026-07-17T00:20:52.533Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning final architecture inspection**"},{"type":"toolCall","id":"call_dpa87i9hEZPRZqmesUz0BOZT|fc_0996e78cf50e3160016a597563ac448199bcf084751c15b07a","name":"read","arguments":{"i":"Reading rc attach connection flow","path":"crates/spt/src/rc.rs","selector":"1070-1285,1360-1510,1760-2040,2130-2440"}},{"type":"toolCall","id":"call_6CcYCOP97oW1MVXc59k2vTTh|fc_0996e78cf50e3160016a597563ac588199a848b23f965ca029","name":"read","arguments":{"i":"Reading attach server teardown flow","path":"crates/spt-daemon/src/attach.rs","selector":"430-625"}},{"type":"toolCall","id":"call_5XBaNO7r9bAosYsrATmOqg2C|fc_0996e78cf50e3160016a597563ac608199be025e7031bf1146","name":"read","arguments":{"i":"Reading net stream lifecycle symbols","path":"crates/spt-daemon/src/nethost.rs","selector":"525-570,1520-1595,1720-1825"}},{"type":"toolCall","id":"call_GTiD30PZNHVTnRtLLSWosFLb|fc_0996e78cf50e3160016a597563ac6881998004f7f8731cf008","name":"read","arguments":{"i":"Reading broker conn cleanup exact","path":"crates/spt-daemon/src/broker.rs","selector":"2970-3225"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":6336,"output":233,"cacheRead":124416,"cacheWrite":0,"totalTokens":130985,"reasoningTokens":34,"cost":{"input":0.03168,"output":0.0069900000000000006,"cacheRead":0.062208,"cacheWrite":0,"total":0.100878}},"stopReason":"toolUse","timestamp":1784247643214,"responseId":"resp_0996e78cf50e3160016a59755b7f188199ad3921704d5a2c11","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a59755f4e508199b4598d7207c3c60a","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXVjIUPW79U11yBFDvrNJYryU5jvtZR2g2IKnMEZ1JIEvmS3ZUwm0PShHKCUm4-PTcpmjZ5ZDFHhaTbpqQpnyf5L2WffwlNZjATYCM4W66hMgB82VgwpwnX2M7VHdo0ytqCcuJ4LhClrF_hul-fZSOd_v9vHIPEPS7YgzykfJW1Q5W1HqcboLQ6cU7-1xN72lLY-B8zDHG1jluxO4kerj_tqiNIw9VBKA8NDGCX2PpK066Y0r0LAuMEaPIlOqPSnl-oFp5LuTZzIdCWFnWpqJ70f11q54bszP674ln9-qq5gNH1MoRMAeS5zRAvWM9hcIdVU61Y9XtuJNbI22tYTOpN3RrLdZDxFDQSIYRYL3LuJkD413JJNNpxg9O2D7K7emPE6iH2cscXLktVRcIIv289Lj812Ppbgypp9b3DRRnnIB4S7rstSeAeTOwlsgVSHBqVekSpYIyGx4vm13pSedTtdTSYSvKc9Pn1SygfD37J2AaQxDdFUKdRSVL4HTWhrH4kx8H2bbLWSjXcPaIu2eR7r7tYk-1jpVotOXg90fuZsR0I0H2ojcYDxM0AtqkDUIAjAQoGwVF0n0CPoMtZdVuPXYW0UGqnD844blEiTX7Ck8Yi5WLNcQR_CRIZ1yN69QMpjpm8B8itd061t_Jm-33arL_8LCDoAFGDqr2nJEuA4iWdU-qvRQdiNXOcEUqlHyaiQCXzVEQR5gdl1fiJx_GM68waghVCPfUoouPSDhkuW86FMX6rqEXAv8V3cgsCedz2f8RkMNA_Qf4U8SOSMJ9tBvBlTdovEhBJh-mVOqV9CKtIwJcka_jn69JsifdjwSq_k87LfvNvak-aW6Ma2fJqxBWLjr6iva8qmQLX-hvylhjFW_ZLMxlF_DqhZHj25Q_uI3VdctkpKWzRiJb17d55CKvectix_UMFNRxDHvxWZpdPPUiYCbXf7LovlvUiczRKtZz5d3wNLnAeEnYjKi-T3NbCgM-gctD-JuZdTCW5Z6N-koq5qhmi07aRmniy4VlK-eUvpmZFAfufmL5dKMCrUq4egS3m3Yw94enqx2VgHExaLdfR-rzcqZ-8y0plXytL2J-lm-EhLhX4fxlBYYGHfakJDDsDCn024i9JIMrLMinnDcoUXHqDFXhRm5hVDiPO3","summary":[{"type":"summary_text","text":"**Planning final architecture inspection**"}]},{"id":"fc_0996e78cf50e3160016a597563ac448199bcf084751c15b07a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading rc attach connection flow\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1070-1285,1360-1510,1760-2040,2130-2440\"}","call_id":"call_dpa87i9hEZPRZqmesUz0BOZT","name":"read"},{"id":"fc_0996e78cf50e3160016a597563ac588199a848b23f965ca029","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach server teardown flow\",\"path\":\"crates/spt-daemon/src/attach.rs\",\"selector\":\"430-625\"}","call_id":"call_6CcYCOP97oW1MVXc59k2vTTh","name":"read"},{"id":"fc_0996e78cf50e3160016a597563ac608199be025e7031bf1146","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading net stream lifecycle symbols\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":\"525-570,1520-1595,1720-1825\"}","call_id":"call_5XBaNO7r9bAosYsrATmOqg2C","name":"read"},{"id":"fc_0996e78cf50e3160016a597563ac6881998004f7f8731cf008","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker conn cleanup exact\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"2970-3225\"}","call_id":"call_GTiD30PZNHVTnRtLLSWosFLb","name":"read"}]},"duration":9287.59619999677,"ttft":4084.2800999954343,"contextSnapshot":{"promptTokens":130752,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_dpa87i9hEZPRZqmesUz0BOZT|fc_0996e78cf50e3160016a597563ac448199bcf084751c15b07a","toolName":"read","startedAt":"2026-07-17T00:20:52.534Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading rc attach connection flow"},"id":"4ada5dfd","parentId":"aef4385e","timestamp":"2026-07-17T00:20:52.534Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_6CcYCOP97oW1MVXc59k2vTTh|fc_0996e78cf50e3160016a597563ac588199a848b23f965ca029","toolName":"read","startedAt":"2026-07-17T00:20:52.534Z","args":{"path":"crates/spt-daemon/src/attach.rs"},"intent":"Reading attach server teardown flow"},"id":"43324e73","parentId":"4ada5dfd","timestamp":"2026-07-17T00:20:52.534Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_5XBaNO7r9bAosYsrATmOqg2C|fc_0996e78cf50e3160016a597563ac608199be025e7031bf1146","toolName":"read","startedAt":"2026-07-17T00:20:52.534Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Reading net stream lifecycle symbols"},"id":"9c47fffc","parentId":"43324e73","timestamp":"2026-07-17T00:20:52.534Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_GTiD30PZNHVTnRtLLSWosFLb|fc_0996e78cf50e3160016a597563ac6881998004f7f8731cf008","toolName":"read","startedAt":"2026-07-17T00:20:52.534Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker conn cleanup exact"},"id":"a332886f","parentId":"9c47fffc","timestamp":"2026-07-17T00:20:52.534Z"}
{"type":"message","id":"8e68dfaf","parentId":"a332886f","timestamp":"2026-07-17T00:20:52.583Z","message":{"role":"toolResult","toolCallId":"call_dpa87i9hEZPRZqmesUz0BOZT|fc_0996e78cf50e3160016a597563ac448199bcf084751c15b07a","toolName":"read","content":[{"type":"text","text":"fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {\n…\n    std::thread::spawn(move || {\n…\n        while let Ok(ev) = event::read() {\n            match ev {\n…\n            }\n        }\n    });\n}\n\n/// Attach a local terminal to the broker-held session for `endpoint_id` under\n/// `intent` (REQ-RCVIEW-1 / REQ-KICK-1): `Control` drives a free endpoint (and is\n/// busy-refused with guidance on a controlled one), `Viewer` watches read-only,\n/// `Take` kicks the incumbent controller (loud `Displaced` notice to them) and\n/// takes control. Returns when the session exits, the operator detaches, or an\n/// error occurs.\n// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\npub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, false)\n}\n\n/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\n/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\n/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\n/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND\n/// stamp), yet the confirmed live session IS attachable — \"attach to a live\n/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).\n/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a\n/// session that dies after confirmation, so this is not a blank-hang reopening.\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub fn run_attach_session_confirmed(\n    endpoint_id: &str,\n    intent: AttachIntent,\n) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, true)\n}\n\nfn run_attach_inner(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    session_confirmed: bool,\n) -> Result<(), String> {\n    let view = intent == AttachIntent::Viewer;\n    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\n    // an endpoint a DIFFERENT node already controls gets actionable guidance here\n    // (the broker would also refuse on the wire — this is the friendly path). A\n    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\n    if intent == AttachIntent::Control {\n        if let Some(node) = current_driver(endpoint_id) {\n            println!(\"'{endpoint_id}' is controlled by {node}.\");\n            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\n            println!(\"  spt rc {endpoint_id} --take   to take control (kicks {node})\");\n            return Ok(());\n        }\n    }\n    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\n    // `status=offline` is not attachable — short-circuit with clean guidance\n    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\n    // instead of the broker dance that would block on a dead session (the\n    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\n    // spt-hosted perch offline, so this catches the common case before any IPC. An\n    // ABSENT or status-less perch falls through to the broker path, which already\n    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\n    // is NOT used here: it returns true for an absent info.json by interim parity.)\n    //\n    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\n    // NOT on STATUS_ONLINE (REQ-ENDPOINT-UNBOUND-ATTACH). A STATUS_UNBOUND skeleton\n    // (session spawned, perch not yet bound) is `!= offline`, so it falls through\n    // here and attaches via the live-session resolve below — the headless /\n    // clear-a-bind-prompt path. `offline` is stamped only on session DEATH, so it\n    // never co-occurs with a live session.\n    //\n    // KNOWN BOUNDARY (doyle, W4): a RESUMING perch is still `offline` during its\n    // pre-bind window — the W2 skeleton-write short-circuits on an existing perch,\n    // so resume gets NO UNBOUND stamp. A standalone `spt rc <id>` against it hits\n    // this failfast (\"offline — start it\") rather than waiting for the bind, and\n    // retries fine once the bind flips the perch online. The `endpoint run\n    // --attach` path does NOT hit it: it arrives via run_attach_session_confirmed\n    // (session already proven live by await_endpoint_session), so the gate is\n    // SKIPPED — it attaches to the confirmed live session even while the perch is\n    // still stamped offline mid-rebringup. Accepted, not contorted around.\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    let perch_path =\n        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);\n    if !session_confirmed\n        && spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.status)\n            .as_deref()\n            == Some(spt_store::liveness::STATUS_OFFLINE)\n    {\n        println!(\"Endpoint '{endpoint_id}' is offline — nothing to attach to.\");\n        println!(\"  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it\");\n        return Ok(());\n    }\n    // Establish the attach (broker conn → session resolve → dial → attach-open →\n    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\n    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\n    // keeps today's clean-exit copy on a no-target miss.\n    let est = match establish_attach(endpoint_id, intent) {\n        Ok(est) => est,\n        Err(EstablishFail::NoTarget(msg)) => {\n            println!(\"{msg}\");\n            return Ok(());\n        }\n        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\n        // rc won't birth one. Plain operator language — what's wrong, what to do —\n        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\n        // [impl->REQ-RC-RECONNECT-TRUTH]\n        Err(EstablishFail::DaemonDown) => {\n            println!(\n                \"The spt daemon isn't running — nothing to attach to. \\\n                 Start it with `spt daemon start`, then `spt rc {endpoint_id}`.\"\n            );\n            return Ok(());\n        }\n        Err(EstablishFail::Error(e)) => return Err(e),\n    };\n    let mut remote_node: Option<String> = est.remote_node.clone();\n\n    let _raw = RawGuard::enable();\n    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n    // pump and read by the Windows stdin reader to gate scroll forwarding\n    // (REQ-RC-MOUSE-FORWARD).\n    let mouse_mode = std::sync::Arc::new(MouseMode::default());\n    let rx = if view {\n        None\n    } else {\n        let (tx, rx) = mpsc::channel();\n        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\n        Some(rx)\n    };\n\n    // Controller status row (REQ-RC-IDENTITY): the operator-side identity banner\n    // on the real terminal's reserved row 1 — controller only (a viewer's viewport\n    // is read-only and may letterbox, so it owns no row). Resolved ONCE here.\n    // [impl->REQ-RC-IDMARKER-DISABLE]\n    let mut status = if status_row_active(view) {\n        let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n        Some(StatusRow {\n            text: resolve_identity_line(endpoint_id),\n            rows,\n            cols,\n        })\n    } else {\n        None\n    };\n\n    let mut stdout = std::io::stdout();\n    let outcome = attach_viewport(\n        endpoint_id,\n        intent,\n        view,\n        est,\n        rx.as_ref(),\n        &mouse_mode,\n        status.as_mut(),\n        &mut remote_node,\n        &mut stdout,\n    );\n\n    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\n    // leaving raw mode, on every pump-exit path (REQ-RC-IDENTITY).\n    if status.is_some() {\n        let _ = stdout.write_all(&StatusRow::restore_bytes());\n        let _ = stdout.flush();\n    }\n    // Out of raw mode (guard drops after) before the parting line.\n    drop(_raw);\n    match outcome {\n        Ok(PumpEnd::Exited(code)) => {\n            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Detached) => {\n            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Displaced(by)) => {\n            // The loud kick notice (REQ-KICK-1): a different operator took control.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\n            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\n            // successful re-attach. Plain operator language (Q4 UX rule): what\n            // happened, what it means, what to do next — never op/read-err lingo.\n            if daemon_down {\n                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\n                // went down and never came back — rc did NOT resurrect it. Name that\n                // plainly so the operator restarts the daemon rather than guessing.\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\n                     didn't come back within {}s. rc won't auto-start it. Bring it \\\n                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            } else {\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\n                     within {}s — the session may still be running. Try `spt rc \\\n                     {endpoint_id}` again once the node/daemon is back]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            }\n            Ok(())\n        }\n        // Reconnectable ends never escape the loop (they enter the retry arm and\n        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n        // so a future PumpEnd variant still forces a decision here.\n        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n        }\n        Ok(PumpEnd::Stalled) => {\n            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced\n            // no output at all — a dead/wedged session, not a slow one. A clear\n…\n        }\n…\n    }\n}\n…\nenum EstablishFail {\n…\n    /// retry (the operator may bring the daemon back), never a resurrection.\n    /// [impl->REQ-RC-RECONNECT-TRUTH]\n    DaemonDown,\n    /// A setup error (broker connect / dial / attach-open / subscribe).\n    Error(String),\n}\n\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n///\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\n/// the broker wires an in-process loopback conn whose streams surface as the\n/// SAME net-stream events the cross-node QUIC path produces. From\n/// `request_attach` down this is byte-identical to a cross-node attach — the\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\n/// The loopback peer's origin is this node's own id, so the target's serve gates\n/// same-node.\n///\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\n/// identically to loopback — so from here down the two legs converge. Honest\n/// failure on every resolve/dial miss: a clean message, never a false attach or\n/// a hang (D2/D6).\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n    // launcher. It must NEVER auto-start the broker — the historical\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\n    // broker cold-connect below is already deadline-bounded, so a down daemon\n    // never hangs.\n    // [impl->REQ-RC-RECONNECT-TRUTH]\n    if !spt_daemon::daemon::is_running() {\n        return Err(EstablishFail::DaemonDown);\n    }\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\n    // pipes), so the single-threaded pump interleaves stdin + broker events\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\n    // sessions, attach-open ack); the pump itself passes its own short slice.\n    let mut brain = Brain::cold_start_pump(\n        &spt_daemon::endpoint::broker_socket_name(),\n        now_ms(),\n        Duration::from_secs(10),\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\n\n    let ops = OpMinter::new();\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    // resolve the OWNING node from the registry and dial it, letting THAT node\n    // resolve endpoint→its session server-side.\n    let local_session = resolve_session(&mut brain, endpoint_id)\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\n    let mut remote_node: Option<String> = None;\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\n        Some(sid) => {\n            let dialed = brain\n                .net_dial_loopback()\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\n            (dialed.conn_id, sid, None)\n        }\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\n                remote_node = Some(node);\n                // session_id is a placeholder the owning node ignores — it\n                // resolves the endpoint to its OWN session id server-side.\n                (conn_id, 0, Some(endpoint_id))\n            }\n            crate::wansend::OwnerDial::NotFound => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"No live session for '{endpoint_id}' — not running on this node \\\n                     or any visible subnet.\"\n                )))\n            }\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\n                return Err(EstablishFail::NoTarget(msg))\n            }\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\n                )))\n            }\n        },\n    };\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\n    // ONCE more. The first attempt uses the conn already established above (happy\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\n    // drops the CONN too, not just the stream, so opening against the dead conn\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\n    let mut established: Option<u64> = Some(conn_id);\n    let stream_id = spt_daemon::with_tracing_retry(\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\n        |op| {\n            let cid = match established.take() {\n                Some(c) => c,\n                None => match local_session {\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\n                    Some(_) => brain\n                        .net_dial_loopback()\n                        .map_err(|e| {\n                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\n                        })?\n                        .conn_id,\n                    // Remote: re-resolve + re-dial the owning node.\n                    None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n                        crate::wansend::OwnerDial::Dialed { conn_id, .. } => conn_id,\n                        _ => {\n                            return Err(std::io::Error::other(format!(\n                                \"'{endpoint_id}' is no longer reachable\"\n                            )))\n                        }\n                    },\n                },\n            };\n            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\n        },\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n    // Subscribe our side of the stream so the target's `Output` records surface\n    // as `NetStreamData` events (the open + Request do not auto-subscribe).\n    brain\n        .net_stream_subscribe(stream_id, 0)\n        .map_err(|e| EstablishFail::Error(format!(\"stream subscribe: {e}\")))?;\n    Ok(Established {\n        brain,\n        ops,\n        stream_id,\n        remote_node,\n    })\n}\n\n/// Drive an established viewport to its FINAL end: the B-3 reconnect loop\n/// (REQ-RC-RECONNECT) — each pass is one full viewport (initial resize → pump).\n/// A severed viewport (broker bounce, remote conn drop — the reconnectable\n/// class) paints a centered reconnect banner and RE-DRIVES [`establish_attach`]\n/// inside a bounded window; every other end is FINAL and returns to the\n/// caller's parting copy. The FAULT-MATRIX row-9 promise (\"rc reconnects\") is\n/// made TRUE here, not edited down (triage F-3).\n///\n…\nenum PumpEnd {\n…\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n    NoLiveSession,\n}\n\n/// Generous first-event backstop for the attach pump (REQ-HAZARD-RC-ATTACH-FAILFAST\n/// path b): if NO broker event arrives within this window after attach, the\n/// session is treated as dead/wedged. Long on purpose — a fresh, mid-init alive\n/// session (empty OutputLog, still painting) MUST pass, so this only catches a\n/// true hang, never the working fresh-attach.\nconst FIRST_EVENT_GRACE: Duration = Duration::from_secs(30);\n\n/// The path-b stall decision (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach has\n/// stalled iff NO broker event has ever arrived AND the generous grace has\n/// elapsed. Pulled out so the decision is unit-testable without a 30s pump run.\n// [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\nfn first_event_stalled(seen_any: bool, elapsed: Duration) -> bool {\n    !seen_any && elapsed >= FIRST_EVENT_GRACE\n}\n\n/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ReadDisposition {\n    /// Transient poll-slice timeout — loop and poll again.\n    Retry,\n    /// Severed broker stream (broker bounced / hung up) — recoverable, surface\n    /// gracefully.\n    BrokerGone,\n    /// A genuine read failure — propagate.\n    Fatal,\n}\n\n/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\n/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\n/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\n/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\n/// gracefully rather than crashing the PTY with the raw io text (the same EOF\n/// family the v0.9.1 seed fix handles); anything else is fatal.\n// [impl->REQ-HAZARD-RC-EOF]\nfn classify_read_err(kind: std::io::ErrorKind) -> ReadDisposition {\n    use std::io::ErrorKind::*;\n    match kind {\n        WouldBlock | TimedOut => ReadDisposition::Retry,\n        UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {\n            ReadDisposition::BrokerGone\n        }\n        _ => ReadDisposition::Fatal,\n    }\n}\n\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n#[allow(clippy::too_many_arguments)]\nfn pump(\n    brain: &mut Brain,\n    stream_id: u64,\n    ops: &OpMinter,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    decoder: &mut AttachDecoder,\n    stdout: &mut impl std::io::Write,\n    controller: bool,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n) -> Result<PumpEnd, String> {\n    // Track the harness's mouse-reporting mode from its output DECSET sequences so\n    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The\n    // scanner is pump-local (single producer) and survives a sequence split across\n    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.\n    let mut mouse_scanner = MouseModeScanner::default();\n    // The reserved-row re-assert scanner (REQ-RC-IDENTITY): watches the harness\n    // output for an alt-screen enter / DECSTBM reset that would destroy our\n    // scroll-region margin, so we can re-paint the status row. Pump-local, single\n    // producer, survives a trigger split across output chunks (like mouse_scanner).\n    let mut reassert_scanner = ReassertScanner::default();\n    // Reserve the top row + paint the identity banner up front (REQ-RC-IDENTITY).\n    // Controller-only (the caller passes `Some` only for a control attach).\n    if let Some(status) = status.as_deref_mut() {\n        stdout\n            .write_all(&status.assert_bytes())\n            .map_err(|e| format!(\"status assert: {e}\"))?;\n        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\n    }\n    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n    // (the session outlives the viewport). Best-effort: an already-finished or\n    // exited stream send is harmless to attempt.\n    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\n        let _ = brain.net_stream_send(stream_id, &[], None, true);\n        PumpEnd::Detached\n    }\n    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n    // drop any `seq` already rendered (the operator render cursor, matching the\n    // local `OutputLog` replay discipline).\n    let mut cursor = 0u64;\n    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size\n    // each slice and forward a Resize when it changes (cross-platform without\n    // hooking SIGWINCH / the crossterm event loop, which would conflict with the\n    // raw-stdin reader). `None` until the first read.\n    let mut last_size: Option<(u16, u16)> = None;\n    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown\n    // as a one-line indicator when it differs from our own terminal.\n    let mut pty_size: Option<(u16, u16)> = None;\n    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged\n    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE\n    // rather than an infinite blank. Any broker event clears it.\n    let started = Instant::now();\n    let mut seen_any = false;\n    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\n    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n    // Any live session replays its buffered ring on attach, so a real attach sets\n    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\n    let mut rendered_any = false;\n    loop {\n        // ── controller window-change → resize ──────────────────────────────\n        if controller {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                if last_size != Some((rows, cols)) {\n                    last_size = Some((rows, cols));\n                    match status.as_deref_mut() {\n                        // Status active: the harness PTY owns rows-1 (we keep row 1\n                        // for the banner), and we re-assert the margin + repaint on\n                        // the new size (REQ-RC-IDENTITY). Never resize the PTY to 0.\n                        Some(status) => {\n                            status.update_size(rows, cols);\n                            let pty_rows = rows.saturating_sub(1).max(1);\n                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\n                            if stdout.write_all(&status.assert_bytes()).is_ok() {\n                                let _ = stdout.flush();\n                            }\n                        }\n                        None => {\n                            let _ = send_attach_resize(brain, stream_id, rows, cols);\n                        }\n                    }\n                }\n            }\n        }\n\n        // ── stdin → session ────────────────────────────────────────────────\n        if let Some(rx) = rx {\n            loop {\n                match rx.try_recv() {\n                    Ok(StdinMsg::Bytes(bytes)) => {\n                        send_attach_input(brain, stream_id, &bytes, ops.next())\n                            .map_err(|e| format!(\"send input: {e}\"))?;\n                    }\n                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\n                    Err(mpsc::TryRecvError::Empty) => break,\n                    Err(mpsc::TryRecvError::Disconnected) => {\n                        return Ok(detach(brain, stream_id))\n                    }\n                }\n            }\n        }\n\n        // ── session → terminal ─────────────────────────────────────────────\n        let ev = match brain.read_event_until(Some(Instant::now() + POLL_SLICE)) {\n            Ok(ev) => ev,\n            Err(e) => match classify_read_err(e.kind()) {\n                // A poll-slice timeout: no event this slice. Before looping, the\n                // first-event backstop — if NOTHING has ever arrived past the\n                // generous grace, the attach is dead/wedged (path b).\n                ReadDisposition::Retry => {\n                    if first_event_stalled(seen_any, started.elapsed()) {\n                        return Ok(PumpEnd::Stalled);\n                    }\n                    continue;\n                }\n                // A severed broker stream (broker bounce / hang-up) — surface it\n                // gracefully, never as a raw RC_FAIL crash (REQ-HAZARD-RC-EOF).\n                ReadDisposition::BrokerGone => return Ok(PumpEnd::BrokerGone),\n                ReadDisposition::Fatal => return Err(format!(\"read event: {e}\")),\n            },\n        };\n        seen_any = true; // any broker event proves the attach stream is live\n        match ev {\n            BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Output { seq, data_b64 } => {\n                            if seq < cursor {\n                                continue; // already rendered — re-serve dedup\n                            }\n                            cursor = seq + 1;\n                            rendered_any = true;\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\n                            // Track the harness's mouse-reporting mode from its output\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\n                            mouse_scanner.feed(mouse_mode, &bytes);\n                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\n                            // Re-assert the reserved row if the harness output just\n                            // destroyed our scroll region — alt-screen enter or a\n                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\n                            // margin is gone (alt-screen has its own full-screen\n                            // margins; a DECSTBM reset clears ours), so re-set the\n                            // MARGIN + repaint (assert_bytes), not just the text —\n                            // else the harness scrolls back over row 1. Scanned on the\n                            // SAME bytes we rendered, after writing them.\n                            if let Some(status) = status.as_deref_mut() {\n                                if reassert_scanner.feed(&bytes) {\n                                    stdout\n                                        .write_all(&status.assert_bytes())\n                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\n                                }\n                            }\n                            stdout.flush().map_err(|e| format!(\"flush: {e}\"))?;\n                        }\n                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\n                        AttachRecord::Displaced { by } => {\n                            return Ok(PumpEnd::Displaced(by));\n                        }\n                        // Viewer letterbox feed (REQ-RCVIEW-1): on a size change,\n                        // show a one-line indicator when our terminal differs from\n                        // the new PTY size (rc is a byte pump — true clip/pad needs\n                        // a grid model, deferred; the honest size cue renders here).\n                        AttachRecord::Size { rows, cols } if pty_size != Some((rows, cols)) => {\n                            pty_size = Some((rows, cols));\n                            match crossterm::terminal::size() {\n                                Ok((mycols, myrows)) if (myrows, mycols) != (rows, cols) => {\n                                    let _ = write!(\n                                        stdout,\n                                        \"\\r\\n[viewing {cols}x{rows} — your terminal is {mycols}x{myrows}]\\r\\n\"\n                                    );\n                                    let _ = stdout.flush();\n                                }\n                                _ => {}\n                            }\n                        }\n                        _ => {} // Request/Input echoed back: protocol noise, ignore.\n                    }\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                return Ok(serve_eof_end(rendered_any));\n            }\n            BrokerEvent::Error { message } => return Err(message),\n            _ => {} // acks + unrelated events: not ours.\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn parse(armed: &mut bool, input: &[u8]) -> (Vec<u8>, bool) {\n        let p = parse_stdin_chunk(armed, input);\n        (p.forward, p.detach)\n    }\n\n    // [unit->REQ-RC-WIN-VT-OUTPUT] the rc attach path composes the OUTPUT console\n    // mode that renders ANSI escapes instead of printing them literally (#12): it\n    // adds VT + processed-output, preserves the prior bits, and is idempotent.\n    #[cfg(windows)]\n    #[test]\n    fn with_vt_output_enables_virtual_terminal_processing() {\n        use windows_sys::Win32::System::Console::{\n            ENABLE_PROCESSED_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WRAP_AT_EOL_OUTPUT,\n        };\n        let prior = ENABLE_WRAP_AT_EOL_OUTPUT;\n        let got = with_vt_output(prior);\n        assert!(\n            got & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0,\n            \"VT-output processing enabled\"\n        );\n        assert!(got & ENABLE_PROCESSED_OUTPUT != 0, \"processed output enabled\");\n        assert!(\n            got & ENABLE_WRAP_AT_EOL_OUTPUT != 0,\n            \"prior console bits preserved\"\n        );\n        assert_eq!(with_vt_output(got), got, \"idempotent\");\n    }\n\n    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (a): attaching to a perch stamped\n    // status=offline (the B2-reconcile / endpoint-stop state) SHORT-CIRCUITS with\n    // clean guidance — it returns Ok WITHOUT any broker dance (no daemon needed),\n    // so it can never hang on a dead session.\n    #[test]\n    fn attach_offline_endpoint_short_circuits_clean() {\n        let _home = crate::testutil::isolated_home();\n        let id = \"stopped-ep\";\n…\n    }\n…\n    // auto-reconnects — the capture shows pre-bounce output, then the centered\n    // reconnect banner, then post-bounce output from the re-established serve,\n    // and the viewport still ends HONESTLY (session exit → Exited). Everything\n    // is in-process against an isolated home: the broker + seed sockets are\n    // home-tagged (no live-box collision), and the seed channel stays UP through\n    // the bounce so establish_attach's CONNECT-ONLY is_running() probe stays\n    // satisfied (REQ-RC-RECONNECT-TRUTH: rc never auto-starts a daemon — the seed\n    // channel up lets the retry re-connect the broker child; a seed death would\n    // instead yield DaemonDown + a bounded retry with NO resurrection — the gated\n    // step's remote leg owns the real-daemon `spt daemon stop` shape at B).\n    #[test]\n    // Both broker children are kill()+wait()ed on the assert path; a panicking\n    // test leaks them only until process teardown (their pipes die with us).\n    #[allow(clippy::zombie_processes)]\n    fn attach_viewport_reconnects_across_a_broker_bounce() {\n        let _home = crate::testutil::isolated_home();\n        let _seed = SeedGuard::start();\n        let broker_name = spt_daemon::endpoint::broker_socket_name();\n        let flag = spt_store::perch::spt_home().join(\"reheal.flag\");\n\n        #[cfg(unix)]\n        let (prog, args) = (\n            \"sh\".to_string(),\n            vec![\n                \"-c\".to_string(),\n                format!(\n                    \"while [ ! -f '{}' ]; do echo tick; sleep 0.2; done\",\n                    flag.display()\n                ),\n            ],\n        );\n        #[cfg(windows)]\n        let (prog, args) = (\n            \"cmd\".to_string(),\n            vec![\n                \"/c\".to_string(),\n                format!(\n                    \"for /l %i in (0,0,1) do @(if exist \\\"{}\\\" (exit) else (echo tick & ping -n 2 127.0.0.1 >nul))\",\n                    flag.display()\n                ),\n            ],\n        );\n        // Returns the OWNING brain too — the finish kills the session through it\n        // (KIND_KILL requires the spawner's session binding).\n        let spawn_ticker = |broker_name: &str| -> (spt_daemon::brain::Brain, u64) {\n            let mut host = spt_daemon::brain::Brain::cold_start(broker_name, 1)\n                .expect(\"host brain connects\");\n            let sid = host.spawn_session(spt_daemon::msg::SpawnReq {\n                program: prog.clone(),\n                args: args.clone(),\n                rows: 24,\n                cols: 80,\n                endpoint: \"reheal\".to_string(),\n                cwd: None,\n                env: Default::default(),\n                translation_binary: None,\n                adapter: String::new(),\n                install_dir: None,\n            })\n            .expect(\"spawn ticker session\");\n            (host, sid)\n        };\n\n        // The broker lives in a KILLABLE CHILD (the B-2 attacher re-exec\n        // pattern, inverted): an in-process broker can't model a daemon death —\n        // Broker::stop halts the ACCEPT loop, but per-conn handler threads in\n        // THIS process keep answering open conns (the viewport never sees an\n        // EOF, so never BrokerGone) AND their parked read_frame instances pin\n        // the pipe name against the rebind. A child-process broker dies like\n        // the real daemon: every pipe breaks at once, the name frees, the\n        // viewport's poll errors into the reconnect loop honestly.\n        let spawn_broker_child = || {\n            let exe = std::env::current_exe().expect(\"current test exe\");\n            let child = std::process::Command::new(&exe)\n                .args([\"rc::tests::broker_host_child\", \"--exact\", \"--nocapture\"])\n                .env(\"SPT_RC_FIXTURE_BROKER_CHILD\", \"1\")\n                .spawn()\n                .expect(\"spawn broker-host child\");\n            // Barrier: the broker answers a brain connect (bounded).\n            let end = Instant::now() + Duration::from_secs(30);\n            loop {\n                if let Ok(b) = spt_daemon::brain::Brain::cold_start(&broker_name, 1) {\n                    drop(b);\n                    break child;\n                }\n                assert!(Instant::now() < end, \"broker child never came up\");\n                std::thread::sleep(Duration::from_millis(50));\n            }\n        };\n        // The DISPATCHER serves the attach streams (the broker alone answers no\n        // stream Requests — production's daemon runs this loop; without it the\n        // viewport sees zero records and honestly Stalls). One per broker\n        // generation, stop-flagged across the bounce.\n        let start_dispatch = |name: String| {\n            let registry = std::sync::Arc::new(spt_daemon::registryhost::RegistryHost::new_at(\n                \"fixturenode\",\n                spt_store::epoch::EpochSource::load_from(&spt_store::perch::epoch_file()),\n                spt_daemon::presence::registry_snapshot_dir(),\n            ));\n            let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));\n            let flag = std::sync::Arc::clone(&stop);\n            let paths = spt_daemon::dispatch::DispatchPaths {\n                owlery: spt_store::perch::owlery_dir(),\n                releases: spt_store::perch::spt_home().join(\"releases\"),\n                scratch: spt_store::perch::spt_home().join(\"dispatch-scratch\"),\n            };\n            std::thread::spawn(move || {\n                let _ = spt_daemon::dispatch::run_dispatch_loop(\n                    &name,\n                    &paths,\n                    registry,\n                    &flag,\n                    Duration::from_millis(50),\n                );\n            });\n            stop\n        };\n\n        let mut broker_child1 = spawn_broker_child();\n        let dispatch1_stop = start_dispatch(broker_name.clone());\n        let (_host1, _sid1) = spawn_ticker(&broker_name);\n\n        // Drive the REAL loop headless: viewer intent (no stdin reader, no\n        // status row), the capture buffer as the out sink.\n        let cap = Capture::default();\n        let mut cap_out = cap.clone();\n        let viewport = std::thread::spawn(move || {\n            let est = establish_attach(\"reheal\", AttachIntent::Viewer).map_err(|e| {\n                let msg = match e {\n                    EstablishFail::NoTarget(m) => format!(\"establish no-target: {m}\"),\n                    EstablishFail::DaemonDown => \"establish daemon-down\".to_string(),\n                    EstablishFail::Error(m) => format!(\"establish error: {m}\"),\n                };\n                msg\n            })?;\n            let mouse = MouseMode::default();\n            let mut remote = est.remote_node.clone();\n            attach_viewport(\n                \"reheal\",\n                AttachIntent::Viewer,\n                true,\n                est,\n                None,\n                &mouse,\n                None,\n                &mut remote,\n                &mut cap_out,\n            )\n        });\n\n        let wait_for = |viewport: &std::thread::JoinHandle<Result<PumpEnd, String>>,\n                        what: &str,\n                        probe: &dyn Fn(&[u8]) -> bool| {\n            let end = Instant::now() + Duration::from_secs(60);\n            loop {\n                let snap = cap.snapshot();\n                if probe(&snap) {\n                    return snap;\n                }\n                assert!(\n                    Instant::now() < end,\n                    \"never observed: {what} (viewport finished: {}; {} bytes captured: {:?})\",\n                    viewport.is_finished(),\n                    snap.len(),\n                    String::from_utf8_lossy(&snap[..snap.len().min(200)])\n                );\n                std::thread::sleep(Duration::from_millis(50));\n            }\n        };\n        let pre = wait_for(&viewport, \"pre-bounce ticker output\", &|s| {\n            String::from_utf8_lossy(s).contains(\"tick\")\n        });\n\n        // BOUNCE: the broker CHILD dies (seed stays up) — every pipe breaks at\n        // once, exactly like a daemon death. The viewport's poll errors →\n        // BrokerGone → the reconnect loop paints the banner and re-drives\n        // establish; the successor child + respawn land well inside the 30s\n        // window.\n        dispatch1_stop.store(true, std::sync::atomic::Ordering::Relaxed);\n        broker_child1.kill().expect(\"kill broker child 1\");\n        let _ = broker_child1.wait();\n        let mut _broker_child2 = spawn_broker_child();\n        let _dispatch2_stop = start_dispatch(broker_name.clone());\n        let (mut host2, _sid2) = spawn_ticker(&broker_name);\n\n        let banner_needle = b\"Reconnecting to \";\n        let full = wait_for(&viewport, \"the reconnect banner + post-bounce output\", &|s| {\n            s.windows(banner_needle.len())\n                .position(|w| w == banner_needle)\n                .is_some_and(|i| String::from_utf8_lossy(&s[i..]).contains(\"tick\"))\n        });\n        // Ordering proof: output BEFORE the banner (the live pre-bounce\n        // viewport), the banner, then output AFTER it (the re-established one).\n        let banner_at = full\n            .windows(banner_needle.len())\n            .position(|w| w == banner_needle)\n            .expect(\"banner present\");\n        assert!(\n            banner_at >= pre.len(),\n            \"the banner paints after pre-bounce output\"\n        );\n        assert!(\n            String::from_utf8_lossy(&full[..banner_at]).contains(\"tick\"),\n            \"pre-bounce ticker output rendered before the banner\"\n        );\n\n        // Honest finish: the ticker exits on the flag → Exit record → the\n        // viewport returns Exited (a FINAL end — never re-driven).\n        std::fs::write(&flag, b\"done\").expect(\"write exit flag\");\n        host2.kill_session().expect(\"kill the ticker session\");\n        let outcome = viewport.join().expect(\"viewport thread joins\");\n        // Reap the successor child (never leak a broker past the test — the\n        // e2e-leaked-daemon class).\n        let _ = _broker_child2.kill();\n        let _ = _broker_child2.wait();\n        match outcome {\n            Ok(PumpEnd::Exited(_)) => {}\n            other => panic!(\"expected the session exit to END the viewport, got {other:?}\"),\n        }\n    }\n\n    /// The reheal fixture's BROKER-HOST child — spawned by the fixture above via\n    /// re-exec with `SPT_RC_FIXTURE_BROKER_CHILD=1` and the ISOLATED home\n    /// inherited through `SPT_HOME`, then KILLED to model a real daemon death\n    /// (an in-process broker cannot die honestly: its per-conn handler threads\n    /// outlive `stop()`, so the viewport never sees an EOF and the parked\n    /// read_frame instances pin the pipe name against the rebind). Binds the\n    /// canonical broker socket + a hermetic net host (loopback scope — the\n    /// net_dial_loopback carrier) and serves until killed. Env-gated: inert in\n    /// every normal sweep.\n    #[test]\n    fn broker_host_child() {\n        if std::env::var(\"SPT_RC_FIXTURE_BROKER_CHILD\").as_deref() != Ok(\"1\") {\n            return;\n        }\n        let name = spt_daemon::endpoint::broker_socket_name();\n        let host = spt_daemon::nethost::NetHost::start(spt_daemon::nethost::NetConfig {\n            identity: spt_proto::identity::Identity::from_seed(&[9u8; 32]),\n            relay: spt_net::net::endpoint::RelayPolicy::Disabled,\n            discovery: spt_net::net::endpoint::LocalDiscovery::Off,\n            scope: spt_net::net::endpoint::BindScope::Loopback,\n            membership: None,\n        })\n        .expect(\"hermetic net host\");\n        let broker = spt_daemon::broker::Broker::bind_in_with_net(\n            &name,\n            spt_store::perch::spt_home().join(\"effects.log\"),\n            Some(host),\n        )\n        .expect(\"bind child broker\");\n        // Serve until killed — the parent's kill IS the bounce.\n        let _ = broker.serve();\n    }\n\n    // [unit->REQ-RC-RECONNECT] B-3 red-first: ONLY the severed-transport ends\n    // re-drive the viewport — BrokerGone (broker bounce) and Severed (remote conn\n    // drop). Session exit / operator detach / displacement / the stall backstop /\n    // an attach refuse are FINAL: re-driving those would re-attach to a session\n    // the operator (or the session itself) deliberately ended.\n    #[test]\n    fn reconnect_class_only_severed_transport_redrives() {\n        assert_eq!(reconnect_class(&PumpEnd::BrokerGone), EndClass::Reconnect);\n        assert_eq!(reconnect_class(&PumpEnd::Severed), EndClass::Reconnect);\n        assert_eq!(reconnect_class(&PumpEnd::Exited(Some(0))), EndClass::Final);\n        assert_eq!(reconnect_class(&PumpEnd::Detached), EndClass::Final);\n        assert_eq!(\n            reconnect_class(&PumpEnd::Displaced(\"node\".into())),\n            EndClass::Final\n        );\n        assert_eq!(reconnect_class(&PumpEnd::Stalled), EndClass::Final);\n        assert_eq!(reconnect_class(&PumpEnd::NoLiveSession), EndClass::Final);\n        assert_eq!(\n            reconnect_class(&PumpEnd::ReconnectGaveUp {\n                detail: \"x\".into(),\n                daemon_down: false\n            }),\n            EndClass::Final\n        );\n    }\n\n    // [unit->REQ-RC-RECONNECT] B-3 red-first vs the pre-fix mislabel: a serve-side\n    // EOF AFTER rendered output is a SEVERED conn (reconnect) — previously a false\n    // \"detached — still running\". A nothing-rendered EOF stays the honest attach\n    // refuse (the #4/D6 stale-row case), NOT a reconnect candidate.\n    #[test]\n    fn serve_eof_severed_vs_refused() {\n        assert!(matches!(serve_eof_end(true), PumpEnd::Severed));\n        assert!(matches!(serve_eof_end(false), PumpEnd::NoLiveSession));\n    }\n\n    // [unit->REQ-RC-RECONNECT] the give-up decision: bounded window, boundary\n    // inclusive — unit-tested without a 30s wait.\n    #[test]\n    fn reconnect_window_expiry_decision() {\n        assert!(!reconnect_expired(Duration::from_secs(0)));\n        assert!(!reconnect_expired(RECONNECT_WINDOW - Duration::from_millis(1)));\n        assert!(reconnect_expired(RECONNECT_WINDOW));\n        assert!(reconnect_expired(RECONNECT_WINDOW + Duration::from_secs(1)));\n    }\n\n    // [unit->REQ-RC-RECONNECT] the reconnect banner: full clear + home, the text\n    // CENTERED on the terminal (row rows/2, col centered on the char count), and\n    // an over-wide text clamps to column 1 instead of underflowing off-screen.\n    #[test]\n    fn reconnect_banner_clears_and_centers() {\n        let bytes = reconnect_banner_bytes(24, 80, \"kitsubito\", 30);\n        let s = String::from_utf8_lossy(&bytes);\n        assert!(s.starts_with(\"\\x1b[2J\\x1b[H\"), \"clear + home first\");\n        // \"Reconnecting to kitsubito… 30s\" = 30 chars → col (80-30)/2+1 = 26, row 12.\n        assert!(s.contains(\"\\x1b[12;26H\"), \"centered position, got {s:?}\");\n        assert!(s.ends_with(\"Reconnecting to kitsubito… 30s\"));\n…\n    }\n…\n}"}],"details":{"displayContent":{"text":"fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {\n…\n    std::thread::spawn(move || {\n…\n        while let Ok(ev) = event::read() {\n            match ev {\n…\n            }\n        }\n    });\n}\n\n/// Attach a local terminal to the broker-held session for `endpoint_id` under\n/// `intent` (REQ-RCVIEW-1 / REQ-KICK-1): `Control` drives a free endpoint (and is\n/// busy-refused with guidance on a controlled one), `Viewer` watches read-only,\n/// `Take` kicks the incumbent controller (loud `Displaced` notice to them) and\n/// takes control. Returns when the session exits, the operator detaches, or an\n/// error occurs.\n// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\npub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, false)\n}\n\n/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\n/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\n/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\n/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND\n/// stamp), yet the confirmed live session IS attachable — \"attach to a live\n/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).\n/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a\n/// session that dies after confirmation, so this is not a blank-hang reopening.\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub fn run_attach_session_confirmed(\n    endpoint_id: &str,\n    intent: AttachIntent,\n) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, true)\n}\n\nfn run_attach_inner(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    session_confirmed: bool,\n) -> Result<(), String> {\n    let view = intent == AttachIntent::Viewer;\n    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\n    // an endpoint a DIFFERENT node already controls gets actionable guidance here\n    // (the broker would also refuse on the wire — this is the friendly path). A\n    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\n    if intent == AttachIntent::Control {\n        if let Some(node) = current_driver(endpoint_id) {\n            println!(\"'{endpoint_id}' is controlled by {node}.\");\n            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\n            println!(\"  spt rc {endpoint_id} --take   to take control (kicks {node})\");\n            return Ok(());\n        }\n    }\n    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\n    // `status=offline` is not attachable — short-circuit with clean guidance\n    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\n    // instead of the broker dance that would block on a dead session (the\n    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\n    // spt-hosted perch offline, so this catches the common case before any IPC. An\n    // ABSENT or status-less perch falls through to the broker path, which already\n    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\n    // is NOT used here: it returns true for an absent info.json by interim parity.)\n    //\n    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\n    // NOT on STATUS_ONLINE (REQ-ENDPOINT-UNBOUND-ATTACH). A STATUS_UNBOUND skeleton\n    // (session spawned, perch not yet bound) is `!= offline`, so it falls through\n    // here and attaches via the live-session resolve below — the headless /\n    // clear-a-bind-prompt path. `offline` is stamped only on session DEATH, so it\n    // never co-occurs with a live session.\n    //\n    // KNOWN BOUNDARY (doyle, W4): a RESUMING perch is still `offline` during its\n    // pre-bind window — the W2 skeleton-write short-circuits on an existing perch,\n    // so resume gets NO UNBOUND stamp. A standalone `spt rc <id>` against it hits\n    // this failfast (\"offline — start it\") rather than waiting for the bind, and\n    // retries fine once the bind flips the perch online. The `endpoint run\n    // --attach` path does NOT hit it: it arrives via run_attach_session_confirmed\n    // (session already proven live by await_endpoint_session), so the gate is\n    // SKIPPED — it attaches to the confirmed live session even while the perch is\n    // still stamped offline mid-rebringup. Accepted, not contorted around.\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    let perch_path =\n        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);\n    if !session_confirmed\n        && spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.status)\n            .as_deref()\n            == Some(spt_store::liveness::STATUS_OFFLINE)\n    {\n        println!(\"Endpoint '{endpoint_id}' is offline — nothing to attach to.\");\n        println!(\"  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it\");\n        return Ok(());\n    }\n    // Establish the attach (broker conn → session resolve → dial → attach-open →\n    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\n    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\n    // keeps today's clean-exit copy on a no-target miss.\n    let est = match establish_attach(endpoint_id, intent) {\n        Ok(est) => est,\n        Err(EstablishFail::NoTarget(msg)) => {\n            println!(\"{msg}\");\n            return Ok(());\n        }\n        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\n        // rc won't birth one. Plain operator language — what's wrong, what to do —\n        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\n        // [impl->REQ-RC-RECONNECT-TRUTH]\n        Err(EstablishFail::DaemonDown) => {\n            println!(\n                \"The spt daemon isn't running — nothing to attach to. \\\n                 Start it with `spt daemon start`, then `spt rc {endpoint_id}`.\"\n            );\n            return Ok(());\n        }\n        Err(EstablishFail::Error(e)) => return Err(e),\n    };\n    let mut remote_node: Option<String> = est.remote_node.clone();\n\n    let _raw = RawGuard::enable();\n    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n    // pump and read by the Windows stdin reader to gate scroll forwarding\n    // (REQ-RC-MOUSE-FORWARD).\n    let mouse_mode = std::sync::Arc::new(MouseMode::default());\n    let rx = if view {\n        None\n    } else {\n        let (tx, rx) = mpsc::channel();\n        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\n        Some(rx)\n    };\n\n    // Controller status row (REQ-RC-IDENTITY): the operator-side identity banner\n    // on the real terminal's reserved row 1 — controller only (a viewer's viewport\n    // is read-only and may letterbox, so it owns no row). Resolved ONCE here.\n    // [impl->REQ-RC-IDMARKER-DISABLE]\n    let mut status = if status_row_active(view) {\n        let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n        Some(StatusRow {\n            text: resolve_identity_line(endpoint_id),\n            rows,\n            cols,\n        })\n    } else {\n        None\n    };\n\n    let mut stdout = std::io::stdout();\n    let outcome = attach_viewport(\n        endpoint_id,\n        intent,\n        view,\n        est,\n        rx.as_ref(),\n        &mouse_mode,\n        status.as_mut(),\n        &mut remote_node,\n        &mut stdout,\n    );\n\n    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\n    // leaving raw mode, on every pump-exit path (REQ-RC-IDENTITY).\n    if status.is_some() {\n        let _ = stdout.write_all(&StatusRow::restore_bytes());\n        let _ = stdout.flush();\n    }\n    // Out of raw mode (guard drops after) before the parting line.\n    drop(_raw);\n    match outcome {\n        Ok(PumpEnd::Exited(code)) => {\n            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Detached) => {\n            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Displaced(by)) => {\n            // The loud kick notice (REQ-KICK-1): a different operator took control.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\n            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\n            // successful re-attach. Plain operator language (Q4 UX rule): what\n            // happened, what it means, what to do next — never op/read-err lingo.\n            if daemon_down {\n                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\n                // went down and never came back — rc did NOT resurrect it. Name that\n                // plainly so the operator restarts the daemon rather than guessing.\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\n                     didn't come back within {}s. rc won't auto-start it. Bring it \\\n                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            } else {\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\n                     within {}s — the session may still be running. Try `spt rc \\\n                     {endpoint_id}` again once the node/daemon is back]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            }\n            Ok(())\n        }\n        // Reconnectable ends never escape the loop (they enter the retry arm and\n        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n        // so a future PumpEnd variant still forces a decision here.\n        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n        }\n        Ok(PumpEnd::Stalled) => {\n            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced\n            // no output at all — a dead/wedged session, not a slow one. A clear\n…\n        }\n…\n    }\n}\n…\nenum EstablishFail {\n…\n    /// retry (the operator may bring the daemon back), never a resurrection.\n    /// [impl->REQ-RC-RECONNECT-TRUTH]\n    DaemonDown,\n    /// A setup error (broker connect / dial / attach-open / subscribe).\n    Error(String),\n}\n\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n///\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\n/// the broker wires an in-process loopback conn whose streams surface as the\n/// SAME net-stream events the cross-node QUIC path produces. From\n/// `request_attach` down this is byte-identical to a cross-node attach — the\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\n/// The loopback peer's origin is this node's own id, so the target's serve gates\n/// same-node.\n///\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\n/// identically to loopback — so from here down the two legs converge. Honest\n/// failure on every resolve/dial miss: a clean message, never a false attach or\n/// a hang (D2/D6).\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n    // launcher. It must NEVER auto-start the broker — the historical\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\n    // broker cold-connect below is already deadline-bounded, so a down daemon\n    // never hangs.\n    // [impl->REQ-RC-RECONNECT-TRUTH]\n    if !spt_daemon::daemon::is_running() {\n        return Err(EstablishFail::DaemonDown);\n    }\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\n    // pipes), so the single-threaded pump interleaves stdin + broker events\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\n    // sessions, attach-open ack); the pump itself passes its own short slice.\n    let mut brain = Brain::cold_start_pump(\n        &spt_daemon::endpoint::broker_socket_name(),\n        now_ms(),\n        Duration::from_secs(10),\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\n\n    let ops = OpMinter::new();\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    // resolve the OWNING node from the registry and dial it, letting THAT node\n    // resolve endpoint→its session server-side.\n    let local_session = resolve_session(&mut brain, endpoint_id)\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\n    let mut remote_node: Option<String> = None;\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\n        Some(sid) => {\n            let dialed = brain\n                .net_dial_loopback()\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\n            (dialed.conn_id, sid, None)\n        }\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\n                remote_node = Some(node);\n                // session_id is a placeholder the owning node ignores — it\n                // resolves the endpoint to its OWN session id server-side.\n                (conn_id, 0, Some(endpoint_id))\n            }\n            crate::wansend::OwnerDial::NotFound => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"No live session for '{endpoint_id}' — not running on this node \\\n                     or any visible subnet.\"\n                )))\n            }\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\n                return Err(EstablishFail::NoTarget(msg))\n            }\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\n                )))\n            }\n        },\n    };\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\n    // ONCE more. The first attempt uses the conn already established above (happy\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\n    // drops the CONN too, not just the stream, so opening against the dead conn\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\n    let mut established: Option<u64> = Some(conn_id);\n    let stream_id = spt_daemon::with_tracing_retry(\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\n        |op| {\n            let cid = match established.take() {\n                Some(c) => c,\n                None => match local_session {\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\n                    Some(_) => brain\n                        .net_dial_loopback()\n                        .map_err(|e| {\n                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\n                        })?\n                        .conn_id,\n                    // Remote: re-resolve + re-dial the owning node.\n                    None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n                        crate::wansend::OwnerDial::Dialed { conn_id, .. } => conn_id,\n                        _ => {\n                            return Err(std::io::Error::other(format!(\n                                \"'{endpoint_id}' is no longer reachable\"\n                            )))\n                        }\n                    },\n                },\n            };\n            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\n        },\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n    // Subscribe our side of the stream so the target's `Output` records surface\n    // as `NetStreamData` events (the open + Request do not auto-subscribe).\n    brain\n        .net_stream_subscribe(stream_id, 0)\n        .map_err(|e| EstablishFail::Error(format!(\"stream subscribe: {e}\")))?;\n    Ok(Established {\n        brain,\n        ops,\n        stream_id,\n        remote_node,\n    })\n}\n\n/// Drive an established viewport to its FINAL end: the B-3 reconnect loop\n/// (REQ-RC-RECONNECT) — each pass is one full viewport (initial resize → pump).\n/// A severed viewport (broker bounce, remote conn drop — the reconnectable\n/// class) paints a centered reconnect banner and RE-DRIVES [`establish_attach`]\n/// inside a bounded window; every other end is FINAL and returns to the\n/// caller's parting copy. The FAULT-MATRIX row-9 promise (\"rc reconnects\") is\n/// made TRUE here, not edited down (triage F-3).\n///\n…\nenum PumpEnd {\n…\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n    NoLiveSession,\n}\n\n/// Generous first-event backstop for the attach pump (REQ-HAZARD-RC-ATTACH-FAILFAST\n/// path b): if NO broker event arrives within this window after attach, the\n/// session is treated as dead/wedged. Long on purpose — a fresh, mid-init alive\n/// session (empty OutputLog, still painting) MUST pass, so this only catches a\n/// true hang, never the working fresh-attach.\nconst FIRST_EVENT_GRACE: Duration = Duration::from_secs(30);\n\n/// The path-b stall decision (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach has\n/// stalled iff NO broker event has ever arrived AND the generous grace has\n/// elapsed. Pulled out so the decision is unit-testable without a 30s pump run.\n// [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\nfn first_event_stalled(seen_any: bool, elapsed: Duration) -> bool {\n    !seen_any && elapsed >= FIRST_EVENT_GRACE\n}\n\n/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ReadDisposition {\n    /// Transient poll-slice timeout — loop and poll again.\n    Retry,\n    /// Severed broker stream (broker bounced / hung up) — recoverable, surface\n    /// gracefully.\n    BrokerGone,\n    /// A genuine read failure — propagate.\n    Fatal,\n}\n\n/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\n/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\n/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\n/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\n/// gracefully rather than crashing the PTY with the raw io text (the same EOF\n/// family the v0.9.1 seed fix handles); anything else is fatal.\n// [impl->REQ-HAZARD-RC-EOF]\nfn classify_read_err(kind: std::io::ErrorKind) -> ReadDisposition {\n    use std::io::ErrorKind::*;\n    match kind {\n        WouldBlock | TimedOut => ReadDisposition::Retry,\n        UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {\n            ReadDisposition::BrokerGone\n        }\n        _ => ReadDisposition::Fatal,\n    }\n}\n\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n#[allow(clippy::too_many_arguments)]\nfn pump(\n    brain: &mut Brain,\n    stream_id: u64,\n    ops: &OpMinter,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    decoder: &mut AttachDecoder,\n    stdout: &mut impl std::io::Write,\n    controller: bool,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n) -> Result<PumpEnd, String> {\n    // Track the harness's mouse-reporting mode from its output DECSET sequences so\n    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The\n    // scanner is pump-local (single producer) and survives a sequence split across\n    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.\n    let mut mouse_scanner = MouseModeScanner::default();\n    // The reserved-row re-assert scanner (REQ-RC-IDENTITY): watches the harness\n    // output for an alt-screen enter / DECSTBM reset that would destroy our\n    // scroll-region margin, so we can re-paint the status row. Pump-local, single\n    // producer, survives a trigger split across output chunks (like mouse_scanner).\n    let mut reassert_scanner = ReassertScanner::default();\n    // Reserve the top row + paint the identity banner up front (REQ-RC-IDENTITY).\n    // Controller-only (the caller passes `Some` only for a control attach).\n    if let Some(status) = status.as_deref_mut() {\n        stdout\n            .write_all(&status.assert_bytes())\n            .map_err(|e| format!(\"status assert: {e}\"))?;\n        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\n    }\n    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n    // (the session outlives the viewport). Best-effort: an already-finished or\n    // exited stream send is harmless to attempt.\n    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\n        let _ = brain.net_stream_send(stream_id, &[], None, true);\n        PumpEnd::Detached\n    }\n    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n    // drop any `seq` already rendered (the operator render cursor, matching the\n    // local `OutputLog` replay discipline).\n    let mut cursor = 0u64;\n    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size\n    // each slice and forward a Resize when it changes (cross-platform without\n    // hooking SIGWINCH / the crossterm event loop, which would conflict with the\n    // raw-stdin reader). `None` until the first read.\n    let mut last_size: Option<(u16, u16)> = None;\n    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown\n    // as a one-line indicator when it differs from our own terminal.\n    let mut pty_size: Option<(u16, u16)> = None;\n    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged\n    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE\n    // rather than an infinite blank. Any broker event clears it.\n    let started = Instant::now();\n    let mut seen_any = false;\n    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\n    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n    // Any live session replays its buffered ring on attach, so a real attach sets\n    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\n    let mut rendered_any = false;\n    loop {\n        // ── controller window-change → resize ──────────────────────────────\n        if controller {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                if last_size != Some((rows, cols)) {\n                    last_size = Some((rows, cols));\n                    match status.as_deref_mut() {\n                        // Status active: the harness PTY owns rows-1 (we keep row 1\n                        // for the banner), and we re-assert the margin + repaint on\n                        // the new size (REQ-RC-IDENTITY). Never resize the PTY to 0.\n                        Some(status) => {\n                            status.update_size(rows, cols);\n                            let pty_rows = rows.saturating_sub(1).max(1);\n                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\n                            if stdout.write_all(&status.assert_bytes()).is_ok() {\n                                let _ = stdout.flush();\n                            }\n                        }\n                        None => {\n                            let _ = send_attach_resize(brain, stream_id, rows, cols);\n                        }\n                    }\n                }\n            }\n        }\n\n        // ── stdin → session ────────────────────────────────────────────────\n        if let Some(rx) = rx {\n            loop {\n                match rx.try_recv() {\n                    Ok(StdinMsg::Bytes(bytes)) => {\n                        send_attach_input(brain, stream_id, &bytes, ops.next())\n                            .map_err(|e| format!(\"send input: {e}\"))?;\n                    }\n                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\n                    Err(mpsc::TryRecvError::Empty) => break,\n                    Err(mpsc::TryRecvError::Disconnected) => {\n                        return Ok(detach(brain, stream_id))\n                    }\n                }\n            }\n        }\n\n        // ── session → terminal ─────────────────────────────────────────────\n        let ev = match brain.read_event_until(Some(Instant::now() + POLL_SLICE)) {\n            Ok(ev) => ev,\n            Err(e) => match classify_read_err(e.kind()) {\n                // A poll-slice timeout: no event this slice. Before looping, the\n                // first-event backstop — if NOTHING has ever arrived past the\n                // generous grace, the attach is dead/wedged (path b).\n                ReadDisposition::Retry => {\n                    if first_event_stalled(seen_any, started.elapsed()) {\n                        return Ok(PumpEnd::Stalled);\n                    }\n                    continue;\n                }\n                // A severed broker stream (broker bounce / hang-up) — surface it\n                // gracefully, never as a raw RC_FAIL crash (REQ-HAZARD-RC-EOF).\n                ReadDisposition::BrokerGone => return Ok(PumpEnd::BrokerGone),\n                ReadDisposition::Fatal => return Err(format!(\"read event: {e}\")),\n            },\n        };\n        seen_any = true; // any broker event proves the attach stream is live\n        match ev {\n            BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Output { seq, data_b64 } => {\n                            if seq < cursor {\n                                continue; // already rendered — re-serve dedup\n                            }\n                            cursor = seq + 1;\n                            rendered_any = true;\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\n                            // Track the harness's mouse-reporting mode from its output\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\n                            mouse_scanner.feed(mouse_mode, &bytes);\n                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\n                            // Re-assert the reserved row if the harness output just\n                            // destroyed our scroll region — alt-screen enter or a\n                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\n                            // margin is gone (alt-screen has its own full-screen\n                            // margins; a DECSTBM reset clears ours), so re-set the\n                            // MARGIN + repaint (assert_bytes), not just the text —\n                            // else the harness scrolls back over row 1. Scanned on the\n                            // SAME bytes we rendered, after writing them.\n                            if let Some(status) = status.as_deref_mut() {\n                                if reassert_scanner.feed(&bytes) {\n                                    stdout\n                                        .write_all(&status.assert_bytes())\n                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\n                                }\n                            }\n                            stdout.flush().map_err(|e| format!(\"flush: {e}\"))?;\n                        }\n                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\n                        AttachRecord::Displaced { by } => {\n                            return Ok(PumpEnd::Displaced(by));\n                        }\n                        // Viewer letterbox feed (REQ-RCVIEW-1): on a size change,\n                        // show a one-line indicator when our terminal differs from\n                        // the new PTY size (rc is a byte pump — true clip/pad needs\n                        // a grid model, deferred; the honest size cue renders here).\n                        AttachRecord::Size { rows, cols } if pty_size != Some((rows, cols)) => {\n                            pty_size = Some((rows, cols));\n                            match crossterm::terminal::size() {\n                                Ok((mycols, myrows)) if (myrows, mycols) != (rows, cols) => {\n                                    let _ = write!(\n                                        stdout,\n                                        \"\\r\\n[viewing {cols}x{rows} — your terminal is {mycols}x{myrows}]\\r\\n\"\n                                    );\n                                    let _ = stdout.flush();\n                                }\n                                _ => {}\n                            }\n                        }\n                        _ => {} // Request/Input echoed back: protocol noise, ignore.\n                    }\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                return Ok(serve_eof_end(rendered_any));\n            }\n            BrokerEvent::Error { message } => return Err(message),\n            _ => {} // acks + unrelated events: not ours.\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn parse(armed: &mut bool, input: &[u8]) -> (Vec<u8>, bool) {\n        let p = parse_stdin_chunk(armed, input);\n        (p.forward, p.detach)\n    }\n\n    // [unit->REQ-RC-WIN-VT-OUTPUT] the rc attach path composes the OUTPUT console\n    // mode that renders ANSI escapes instead of printing them literally (#12): it\n    // adds VT + processed-output, preserves the prior bits, and is idempotent.\n    #[cfg(windows)]\n    #[test]\n    fn with_vt_output_enables_virtual_terminal_processing() {\n        use windows_sys::Win32::System::Console::{\n            ENABLE_PROCESSED_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WRAP_AT_EOL_OUTPUT,\n        };\n        let prior = ENABLE_WRAP_AT_EOL_OUTPUT;\n        let got = with_vt_output(prior);\n        assert!(\n            got & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0,\n            \"VT-output processing enabled\"\n        );\n        assert!(got & ENABLE_PROCESSED_OUTPUT != 0, \"processed output enabled\");\n        assert!(\n            got & ENABLE_WRAP_AT_EOL_OUTPUT != 0,\n            \"prior console bits preserved\"\n        );\n        assert_eq!(with_vt_output(got), got, \"idempotent\");\n    }\n\n    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (a): attaching to a perch stamped\n    // status=offline (the B2-reconcile / endpoint-stop state) SHORT-CIRCUITS with\n    // clean guidance — it returns Ok WITHOUT any broker dance (no daemon needed),\n    // so it can never hang on a dead session.\n    #[test]\n    fn attach_offline_endpoint_short_circuits_clean() {\n        let _home = crate::testutil::isolated_home();\n        let id = \"stopped-ep\";\n…\n    }\n…\n    // auto-reconnects — the capture shows pre-bounce output, then the centered\n    // reconnect banner, then post-bounce output from the re-established serve,\n    // and the viewport still ends HONESTLY (session exit → Exited). Everything\n    // is in-process against an isolated home: the broker + seed sockets are\n    // home-tagged (no live-box collision), and the seed channel stays UP through\n    // the bounce so establish_attach's CONNECT-ONLY is_running() probe stays\n    // satisfied (REQ-RC-RECONNECT-TRUTH: rc never auto-starts a daemon — the seed\n    // channel up lets the retry re-connect the broker child; a seed death would\n    // instead yield DaemonDown + a bounded retry with NO resurrection — the gated\n    // step's remote leg owns the real-daemon `spt daemon stop` shape at B).\n    #[test]\n    // Both broker children are kill()+wait()ed on the assert path; a panicking\n    // test leaks them only until process teardown (their pipes die with us).\n    #[allow(clippy::zombie_processes)]\n    fn attach_viewport_reconnects_across_a_broker_bounce() {\n        let _home = crate::testutil::isolated_home();\n        let _seed = SeedGuard::start();\n        let broker_name = spt_daemon::endpoint::broker_socket_name();\n        let flag = spt_store::perch::spt_home().join(\"reheal.flag\");\n\n        #[cfg(unix)]\n        let (prog, args) = (\n            \"sh\".to_string(),\n            vec![\n                \"-c\".to_string(),\n                format!(\n                    \"while [ ! -f '{}' ]; do echo tick; sleep 0.2; done\",\n                    flag.display()\n                ),\n            ],\n        );\n        #[cfg(windows)]\n        let (prog, args) = (\n            \"cmd\".to_string(),\n            vec![\n                \"/c\".to_string(),\n                format!(\n                    \"for /l %i in (0,0,1) do @(if exist \\\"{}\\\" (exit) else (echo tick & ping -n 2 127.0.0.1 >nul))\",\n                    flag.display()\n                ),\n            ],\n        );\n        // Returns the OWNING brain too — the finish kills the session through it\n        // (KIND_KILL requires the spawner's session binding).\n        let spawn_ticker = |broker_name: &str| -> (spt_daemon::brain::Brain, u64) {\n            let mut host = spt_daemon::brain::Brain::cold_start(broker_name, 1)\n                .expect(\"host brain connects\");\n            let sid = host.spawn_session(spt_daemon::msg::SpawnReq {\n                program: prog.clone(),\n                args: args.clone(),\n                rows: 24,\n                cols: 80,\n                endpoint: \"reheal\".to_string(),\n                cwd: None,\n                env: Default::default(),\n                translation_binary: None,\n                adapter: String::new(),\n                install_dir: None,\n            })\n            .expect(\"spawn ticker session\");\n            (host, sid)\n        };\n\n        // The broker lives in a KILLABLE CHILD (the B-2 attacher re-exec\n        // pattern, inverted): an in-process broker can't model a daemon death —\n        // Broker::stop halts the ACCEPT loop, but per-conn handler threads in\n        // THIS process keep answering open conns (the viewport never sees an\n        // EOF, so never BrokerGone) AND their parked read_frame instances pin\n        // the pipe name against the rebind. A child-process broker dies like\n        // the real daemon: every pipe breaks at once, the name frees, the\n        // viewport's poll errors into the reconnect loop honestly.\n        let spawn_broker_child = || {\n            let exe = std::env::current_exe().expect(\"current test exe\");\n            let child = std::process::Command::new(&exe)\n                .args([\"rc::tests::broker_host_child\", \"--exact\", \"--nocapture\"])\n                .env(\"SPT_RC_FIXTURE_BROKER_CHILD\", \"1\")\n                .spawn()\n                .expect(\"spawn broker-host child\");\n            // Barrier: the broker answers a brain connect (bounded).\n            let end = Instant::now() + Duration::from_secs(30);\n            loop {\n                if let Ok(b) = spt_daemon::brain::Brain::cold_start(&broker_name, 1) {\n                    drop(b);\n                    break child;\n                }\n                assert!(Instant::now() < end, \"broker child never came up\");\n                std::thread::sleep(Duration::from_millis(50));\n            }\n        };\n        // The DISPATCHER serves the attach streams (the broker alone answers no\n        // stream Requests — production's daemon runs this loop; without it the\n        // viewport sees zero records and honestly Stalls). One per broker\n        // generation, stop-flagged across the bounce.\n        let start_dispatch = |name: String| {\n            let registry = std::sync::Arc::new(spt_daemon::registryhost::RegistryHost::new_at(\n                \"fixturenode\",\n                spt_store::epoch::EpochSource::load_from(&spt_store::perch::epoch_file()),\n                spt_daemon::presence::registry_snapshot_dir(),\n            ));\n            let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));\n            let flag = std::sync::Arc::clone(&stop);\n            let paths = spt_daemon::dispatch::DispatchPaths {\n                owlery: spt_store::perch::owlery_dir(),\n                releases: spt_store::perch::spt_home().join(\"releases\"),\n                scratch: spt_store::perch::spt_home().join(\"dispatch-scratch\"),\n            };\n            std::thread::spawn(move || {\n                let _ = spt_daemon::dispatch::run_dispatch_loop(\n                    &name,\n                    &paths,\n                    registry,\n                    &flag,\n                    Duration::from_millis(50),\n                );\n            });\n            stop\n        };\n\n        let mut broker_child1 = spawn_broker_child();\n        let dispatch1_stop = start_dispatch(broker_name.clone());\n        let (_host1, _sid1) = spawn_ticker(&broker_name);\n\n        // Drive the REAL loop headless: viewer intent (no stdin reader, no\n        // status row), the capture buffer as the out sink.\n        let cap = Capture::default();\n        let mut cap_out = cap.clone();\n        let viewport = std::thread::spawn(move || {\n            let est = establish_attach(\"reheal\", AttachIntent::Viewer).map_err(|e| {\n                let msg = match e {\n                    EstablishFail::NoTarget(m) => format!(\"establish no-target: {m}\"),\n                    EstablishFail::DaemonDown => \"establish daemon-down\".to_string(),\n                    EstablishFail::Error(m) => format!(\"establish error: {m}\"),\n                };\n                msg\n            })?;\n            let mouse = MouseMode::default();\n            let mut remote = est.remote_node.clone();\n            attach_viewport(\n                \"reheal\",\n                AttachIntent::Viewer,\n                true,\n                est,\n                None,\n                &mouse,\n                None,\n                &mut remote,\n                &mut cap_out,\n            )\n        });\n\n        let wait_for = |viewport: &std::thread::JoinHandle<Result<PumpEnd, String>>,\n                        what: &str,\n                        probe: &dyn Fn(&[u8]) -> bool| {\n            let end = Instant::now() + Duration::from_secs(60);\n            loop {\n                let snap = cap.snapshot();\n                if probe(&snap) {\n                    return snap;\n                }\n                assert!(\n                    Instant::now() < end,\n                    \"never observed: {what} (viewport finished: {}; {} bytes captured: {:?})\",\n                    viewport.is_finished(),\n                    snap.len(),\n                    String::from_utf8_lossy(&snap[..snap.len().min(200)])\n                );\n                std::thread::sleep(Duration::from_millis(50));\n            }\n        };\n        let pre = wait_for(&viewport, \"pre-bounce ticker output\", &|s| {\n            String::from_utf8_lossy(s).contains(\"tick\")\n        });\n\n        // BOUNCE: the broker CHILD dies (seed stays up) — every pipe breaks at\n        // once, exactly like a daemon death. The viewport's poll errors →\n        // BrokerGone → the reconnect loop paints the banner and re-drives\n        // establish; the successor child + respawn land well inside the 30s\n        // window.\n        dispatch1_stop.store(true, std::sync::atomic::Ordering::Relaxed);\n        broker_child1.kill().expect(\"kill broker child 1\");\n        let _ = broker_child1.wait();\n        let mut _broker_child2 = spawn_broker_child();\n        let _dispatch2_stop = start_dispatch(broker_name.clone());\n        let (mut host2, _sid2) = spawn_ticker(&broker_name);\n\n        let banner_needle = b\"Reconnecting to \";\n        let full = wait_for(&viewport, \"the reconnect banner + post-bounce output\", &|s| {\n            s.windows(banner_needle.len())\n                .position(|w| w == banner_needle)\n                .is_some_and(|i| String::from_utf8_lossy(&s[i..]).contains(\"tick\"))\n        });\n        // Ordering proof: output BEFORE the banner (the live pre-bounce\n        // viewport), the banner, then output AFTER it (the re-established one).\n        let banner_at = full\n            .windows(banner_needle.len())\n            .position(|w| w == banner_needle)\n            .expect(\"banner present\");\n        assert!(\n            banner_at >= pre.len(),\n            \"the banner paints after pre-bounce output\"\n        );\n        assert!(\n            String::from_utf8_lossy(&full[..banner_at]).contains(\"tick\"),\n            \"pre-bounce ticker output rendered before the banner\"\n        );\n\n        // Honest finish: the ticker exits on the flag → Exit record → the\n        // viewport returns Exited (a FINAL end — never re-driven).\n        std::fs::write(&flag, b\"done\").expect(\"write exit flag\");\n        host2.kill_session().expect(\"kill the ticker session\");\n        let outcome = viewport.join().expect(\"viewport thread joins\");\n        // Reap the successor child (never leak a broker past the test — the\n        // e2e-leaked-daemon class).\n        let _ = _broker_child2.kill();\n        let _ = _broker_child2.wait();\n        match outcome {\n            Ok(PumpEnd::Exited(_)) => {}\n            other => panic!(\"expected the session exit to END the viewport, got {other:?}\"),\n        }\n    }\n\n    /// The reheal fixture's BROKER-HOST child — spawned by the fixture above via\n    /// re-exec with `SPT_RC_FIXTURE_BROKER_CHILD=1` and the ISOLATED home\n    /// inherited through `SPT_HOME`, then KILLED to model a real daemon death\n    /// (an in-process broker cannot die honestly: its per-conn handler threads\n    /// outlive `stop()`, so the viewport never sees an EOF and the parked\n    /// read_frame instances pin the pipe name against the rebind). Binds the\n    /// canonical broker socket + a hermetic net host (loopback scope — the\n    /// net_dial_loopback carrier) and serves until killed. Env-gated: inert in\n    /// every normal sweep.\n    #[test]\n    fn broker_host_child() {\n        if std::env::var(\"SPT_RC_FIXTURE_BROKER_CHILD\").as_deref() != Ok(\"1\") {\n            return;\n        }\n        let name = spt_daemon::endpoint::broker_socket_name();\n        let host = spt_daemon::nethost::NetHost::start(spt_daemon::nethost::NetConfig {\n            identity: spt_proto::identity::Identity::from_seed(&[9u8; 32]),\n            relay: spt_net::net::endpoint::RelayPolicy::Disabled,\n            discovery: spt_net::net::endpoint::LocalDiscovery::Off,\n            scope: spt_net::net::endpoint::BindScope::Loopback,\n            membership: None,\n        })\n        .expect(\"hermetic net host\");\n        let broker = spt_daemon::broker::Broker::bind_in_with_net(\n            &name,\n            spt_store::perch::spt_home().join(\"effects.log\"),\n            Some(host),\n        )\n        .expect(\"bind child broker\");\n        // Serve until killed — the parent's kill IS the bounce.\n        let _ = broker.serve();\n    }\n\n    // [unit->REQ-RC-RECONNECT] B-3 red-first: ONLY the severed-transport ends\n    // re-drive the viewport — BrokerGone (broker bounce) and Severed (remote conn\n    // drop). Session exit / operator detach / displacement / the stall backstop /\n    // an attach refuse are FINAL: re-driving those would re-attach to a session\n    // the operator (or the session itself) deliberately ended.\n    #[test]\n    fn reconnect_class_only_severed_transport_redrives() {\n        assert_eq!(reconnect_class(&PumpEnd::BrokerGone), EndClass::Reconnect);\n        assert_eq!(reconnect_class(&PumpEnd::Severed), EndClass::Reconnect);\n        assert_eq!(reconnect_class(&PumpEnd::Exited(Some(0))), EndClass::Final);\n        assert_eq!(reconnect_class(&PumpEnd::Detached), EndClass::Final);\n        assert_eq!(\n            reconnect_class(&PumpEnd::Displaced(\"node\".into())),\n            EndClass::Final\n        );\n        assert_eq!(reconnect_class(&PumpEnd::Stalled), EndClass::Final);\n        assert_eq!(reconnect_class(&PumpEnd::NoLiveSession), EndClass::Final);\n        assert_eq!(\n            reconnect_class(&PumpEnd::ReconnectGaveUp {\n                detail: \"x\".into(),\n                daemon_down: false\n            }),\n            EndClass::Final\n        );\n    }\n\n    // [unit->REQ-RC-RECONNECT] B-3 red-first vs the pre-fix mislabel: a serve-side\n    // EOF AFTER rendered output is a SEVERED conn (reconnect) — previously a false\n    // \"detached — still running\". A nothing-rendered EOF stays the honest attach\n    // refuse (the #4/D6 stale-row case), NOT a reconnect candidate.\n    #[test]\n    fn serve_eof_severed_vs_refused() {\n        assert!(matches!(serve_eof_end(true), PumpEnd::Severed));\n        assert!(matches!(serve_eof_end(false), PumpEnd::NoLiveSession));\n    }\n\n    // [unit->REQ-RC-RECONNECT] the give-up decision: bounded window, boundary\n    // inclusive — unit-tested without a 30s wait.\n    #[test]\n    fn reconnect_window_expiry_decision() {\n        assert!(!reconnect_expired(Duration::from_secs(0)));\n        assert!(!reconnect_expired(RECONNECT_WINDOW - Duration::from_millis(1)));\n        assert!(reconnect_expired(RECONNECT_WINDOW));\n        assert!(reconnect_expired(RECONNECT_WINDOW + Duration::from_secs(1)));\n    }\n\n    // [unit->REQ-RC-RECONNECT] the reconnect banner: full clear + home, the text\n    // CENTERED on the terminal (row rows/2, col centered on the char count), and\n    // an over-wide text clamps to column 1 instead of underflowing off-screen.\n    #[test]\n    fn reconnect_banner_clears_and_centers() {\n        let bytes = reconnect_banner_bytes(24, 80, \"kitsubito\", 30);\n        let s = String::from_utf8_lossy(&bytes);\n        assert!(s.starts_with(\"\\x1b[2J\\x1b[H\"), \"clear + home first\");\n        // \"Reconnecting to kitsubito… 30s\" = 30 chars → col (80-30)/2+1 = 26, row 12.\n        assert!(s.contains(\"\\x1b[12;26H\"), \"centered position, got {s:?}\");\n        assert!(s.ends_with(\"Reconnecting to kitsubito… 30s\"));\n…\n    }\n…\n}","startLine":1015,"lineNumbers":[1015,null,1018,null,1020,1021,null,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,null,1294,null,1314,1315,null,1351,null,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,null,1720,null,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2035,2036,2037,2038,2039,2040,null,2050,null,2130,2131,2132,2133,2134,2135,2136,2137,2138,2139,2140,2141,2142,2143,2144,2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2155,2156,2157,2158,2159,2160,2161,2162,2163,2164,2165,2166,2167,2168,2169,2170,2171,2172,2173,2174,2175,2176,2177,2178,2179,2180,2181,2182,2183,2184,2185,2186,2187,2188,2189,2190,2191,2192,2193,2194,2195,2196,2197,2198,2199,2200,2201,2202,2203,2204,2205,2206,2207,2208,2209,2210,2211,2212,2213,2214,2215,2216,2217,2218,2219,2220,2221,2222,2223,2224,2225,2226,2227,2228,2229,2230,2231,2232,2233,2234,2235,2236,2237,2238,2239,2240,2241,2242,2243,2244,2245,2246,2247,2248,2249,2250,2251,2252,2253,2254,2255,2256,2257,2258,2259,2260,2261,2262,2263,2264,2265,2266,2267,2268,2269,2270,2271,2272,2273,2274,2275,2276,2277,2278,2279,2280,2281,2282,2283,2284,2285,2286,2287,2288,2289,2290,2291,2292,2293,2294,2295,2296,2297,2298,2299,2300,2301,2302,2303,2304,2305,2306,2307,2308,2309,2310,2311,2312,2313,2314,2315,2316,2317,2318,2319,2320,2321,2322,2323,2324,2325,2326,2327,2328,2329,2330,2331,2332,2333,2334,2335,2336,2337,2338,2339,2340,2341,2342,2343,2344,2345,2346,2347,2348,2349,2350,2351,2352,2353,2354,2355,2356,2357,2358,2359,2360,2361,2362,2363,2364,2365,2366,2367,2368,2369,2370,2371,2372,2373,2374,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,2400,2401,2402,2403,2404,2405,2406,2407,2408,2409,2410,2411,2412,2413,2414,2415,2416,2417,2418,2419,2420,2421,2422,2423,2424,2425,2426,2427,2428,2429,2430,2431,2432,2433,2434,2435,2436,2437,2438,2439,2440,null,2445,null,3213]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"}}},"isError":false,"timestamp":1784247652582}}
{"type":"message","id":"6d55b1c9","parentId":"8e68dfaf","timestamp":"2026-07-17T00:20:52.583Z","message":{"role":"toolResult","toolCallId":"call_5XBaNO7r9bAosYsrATmOqg2C|fc_0996e78cf50e3160016a597563ac608199be025e7031bf1146","toolName":"read","content":[{"type":"text","text":"struct StreamEntry {\n…\n    /// Whether THIS node opened the stream (`open_stream`) vs the peer\n    /// (accept loop). The inbound dispatcher (D9-1) claims only\n    /// peer-initiated streams; without this fact the table is ambiguous —\n    /// it holds locally-opened outbound streams too (gotcha: a dispatcher\n    /// that served its own requester stream would deadlock on itself).\n    initiated_locally: bool,\n    /// The send half, serialized behind an async mutex (writes happen on the\n    /// host's runtime via `block_on`).\n    send: tokio::sync::Mutex<SendHalf>,\n    log: Arc<Mutex<StreamLog>>,\n    /// Wakes the read pump when a retentive (tunnel) stream's ring drains below\n    /// its backpressure cap (M11-W3, REQ-SHELL-4) — see [`StreamLog::retentive_cap`].\n    /// Unused by ordinary (non-retentive) streams.\n    room: Arc<tokio::sync::Notify>,\n    /// Retired = invisible to dispatch eligibility (ADR-0038 A), NOT torn down:\n    /// retire must never sever the transport — the worker's reply send is\n    /// fire-and-forget, so a retire that dropped this entry could overtake the\n    /// still-queued reply inside the broker and reset the never-finished QUIC\n    /// send half (requester sees torn-EOF, no reply). The reply-flush ordering\n    /// is load-bearing; physical teardown belongs to the conn-close sweep only.\n    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n    retired: std::sync::atomic::AtomicBool,\n}\n\n/// Tables shared between the host's sync surface and its runtime tasks\n/// (accept loop, per-conn stream acceptors, per-stream read pumps).\nstruct NetShared {\n    next_conn_id: AtomicU64,\n    conns: Mutex<HashMap<u64, ConnEntry>>,\n    next_stream_id: AtomicU64,\n    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,\n    ring_cap: usize,\n    /// The conn-liveness event log (D4c) — one per host, all conns interleaved.\n    presence: Mutex<PresenceLog>,\n}\n\nimpl NetShared {\n    fn new() -> Self {\n        NetShared {\n            next_conn_id: AtomicU64::new(1),\n            conns: Mutex::new(HashMap::new()),\n            next_stream_id: AtomicU64::new(1),\n            streams: Mutex::new(HashMap::new()),\n            ring_cap: ring_cap_from_env(),\n            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),\n        }\n    }\n}\n…\nimpl NetHost {\n…\n    pub fn send_stream(&self, stream_id: u64, bytes: &[u8], finish: bool) -> io::Result<()> {\n…\n        self.bounded_block_on(async move {\n…\n            if finish {\n                send.finish().await?;\n            }\n            Ok(())\n        })\n    }\n\n    /// Every held stream's info — the query a re-attaching brain uses to find\n    /// streams (including peer-initiated ones that arrived while it was down).\n    /// Retired rows are hidden (ADR-0038 A): their exchange is over, and this\n    /// enumeration is exactly what a future dispatcher generation would replay.\n    pub fn stream_infos(&self) -> Vec<NetStreamInfo> {\n        let streams = self.shared.streams.lock().unwrap();\n        streams\n            .iter()\n            .filter(|(_, e)| !e.retired.load(std::sync::atomic::Ordering::SeqCst))\n            .map(|(id, e)| {\n                let log = e.log.lock().unwrap();\n                NetStreamInfo {\n                    stream_id: *id,\n                    conn_id: e.conn_id,\n                    remote_id_hex: e.remote_id_hex.clone(),\n                    next_seq: log.next_seq,\n                    finished: log.finished,\n                    initiated_locally: e.initiated_locally,\n                    floor_seq: log.floor_seq(),\n                }\n            })\n            .collect()\n    }\n\n    /// Retire a stream row (ADR-0038 A): the dispatcher reports a stream whose\n    /// serve loop ran to completion — its exchange is over, so no future\n    /// dispatcher generation may rediscover and replay it (the control-steal\n    /// hazard). Retirement is ELIGIBILITY-VISIBILITY, not teardown: the row is\n    /// hidden from [`stream_infos`](Self::stream_infos) and the opener query\n    /// and its ring is freed, but the entry (send half, subscriber, pump) stays\n    /// alive so an in-flight reply always flushes — retire raced fire-and-forget\n    /// reply sends when it removed the entry (see the `retired` field note).\n    /// Physical teardown stays with the conn-close sweep. Returns whether this\n    /// call newly retired the row (idempotent; absent row = false).\n    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n    pub fn retire_stream(&self, stream_id: u64) -> bool {\n        let Some(entry) = self\n            .shared\n            .streams\n            .lock()\n            .unwrap()\n            .get(&stream_id)\n            .map(Arc::clone)\n        else {\n            return false;\n        };\n        let newly = !entry\n            .retired\n            .swap(true, std::sync::atomic::Ordering::SeqCst);\n        if newly {\n            // A retired exchange's request bytes are dead weight — free the\n            // ring; the pinned opener and cursors stay (cheap, bounded).\n            entry.log.lock().unwrap().ring.clear();\n        }\n        newly\n    }\n\n    /// The stream's pinned opener line (ADR-0038 B) — the restart-durable\n    /// classification fact, independent of the evictable data ring. Outer\n    /// `None` = no such stream; inner `None` = no opener pinned (first `\\n`\n    /// not yet arrived, or capture gave up bounded).\n    // [impl->REQ-STREAM-OPENER-DURABLE]\n    pub fn stream_opener(&self, stream_id: u64) -> Option<Option<Vec<u8>>> {\n        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n        // A retired row answers no-such-stream: classification exists only to\n        // serve, and a retired exchange must never be served again.\n        if entry.retired.load(std::sync::atomic::Ordering::SeqCst) {\n            return None;\n        }\n…\n    }\n…\n\n    /// Gracefully close the endpoint: every held connection flushes its QUIC\n    /// CLOSE so peers' closed-watchers fire promptly (a bare host drop kills\n    /// the runtime first and leaves peers to an idle timeout).\n    pub fn close(&self) {\n        let endpoint = Arc::clone(&self.endpoint);\n        self.runtime.handle().block_on(async move {\n            endpoint.close().await;\n        });\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn hermetic(identity: &Identity) -> NetConfig {\n        NetConfig {\n            identity: Identity::from_seed(&identity.seed()),\n            relay: RelayPolicy::Disabled,\n            discovery: LocalDiscovery::Off,\n            scope: BindScope::Loopback,\n            membership: None,\n        }\n    }\n\n    // [unit->REQ-DAEMON-2] the endpoint is constructed broker-side (inside the\n    // host's dedicated runtime) with relays disabled, bound to the node's own\n    // identity, with an empty conn table.\n    #[test]\n    fn host_binds_endpoint_to_node_identity() {\n        let id = Identity::generate();\n        let host = NetHost::start(hermetic(&id)).expect(\"host start\");\n        assert_eq!(host.node_id_hex(), id.public_key().to_hex());\n        assert_eq!(host.conn_count(), 0);\n        assert!(!host.addr_json().is_null(), \"dialable address serializes\");\n    }\n\n    // [unit->REQ-OPID-MINTER-NAMESPACE] the dial/stream op maps are minter-namespaced\n    // (ADR-0034): two minters that mint the SAME op integer (every counter starts at\n    // 1) must record + look up DISTINCT connections/streams — no clobber. RED-first:\n    // under the old `HashMap<u64, u64>` the second record overwrote the first, so both\n    // lookups returned the SAME (last-written) value — a cross-minter clobber.\n    #[test]\n    fn op_maps_are_minter_namespaced() {\n        let host = NetHost::start(hermetic(&Identity::generate())).expect(\"host start\");\n        let op = 3u64; // the same integer from two independent counters.\n\n        host.record_dial_op(Minter::Rc, op, 100);\n        host.record_dial_op(Minter::Pump, op, 200);\n        host.record_dial_op(Minter::Shell, op, 300);\n        assert_eq!(host.dial_op_conn(Minter::Rc, op), Some(100));\n        assert_eq!(host.dial_op_conn(Minter::Pump, op), Some(200));\n        assert_eq!(host.dial_op_conn(Minter::Shell, op), Some(300));\n        assert_eq!(\n            host.dial_op_conn(Minter::Cli, op),\n            None,\n            \"an unrecorded minter's op is absent, not another minter's conn\"\n        );\n\n        host.record_stream_op(Minter::Rc, op, 11);\n        host.record_stream_op(Minter::Pump, op, 22);\n        assert_eq!(host.stream_op_id(Minter::Rc, op), Some(11));\n        assert_eq!(host.stream_op_id(Minter::Pump, op), Some(22));\n    }\n\n    // [unit->REQ-HAZARD-RESTART-IDEMPOTENT] StreamLog.next_seq counts read-pump\n    // APPENDS (transport reads), NOT logical sender chunks: each append() bumps it\n    // by one regardless of byte size. Under QUIC coalescing, N logical sender\n    // writes can arrive as M < N appends (one read_chunk carrying several writes'\n    // bytes), so next_seq == M, not N. This documents WHY the netstream int test\n    // must gate on BYTES (byte-exact + exactly-once), never on\n    // seq == logical-chunk-count (the Windows coalescing flake). The bytes are\n    // retained + drained in arrival order, independent of the seq count.\n    #[test]\n    fn next_seq_counts_appends_not_logical_chunks() {\n        let mut log = StreamLog::new(7, 1024);\n        assert_eq!(log.next_seq, 0);\n        // Two appends of wildly different sizes (a coalesced read carrying many\n        // logical writes vs a 1-byte read) each bump next_seq by exactly one.\n        log.append(b\"x\");\n        log.append(b\"a-much-larger-coalesced-read-carrying-many-logical-sender-writes\");\n        assert_eq!(\n            log.next_seq, 2,\n            \"two appends -> next_seq 2, regardless of per-append byte size\"\n        );\n        // The BYTES are retained + drained in arrival order, framing-agnostic.\n        let (bytes, _finished) = log.drain();\n        assert_eq!(\n            bytes,\n            b\"xa-much-larger-coalesced-read-carrying-many-logical-sender-writes\".to_vec()\n        );\n    }\n\n    // [unit->REQ-DAEMON-2] a broker-owned host dials a second in-process host\n    // over hermetic loopback QUIC; both sides register the connection — the\n    // dialer in its table, the acceptor via its accept loop.\n    #[test]\n    fn two_hosts_dial_over_loopback() {\n        let a = NetHost::start(hermetic(&Identity::generate())).expect(\"host a\");\n        let b_id = Identity::generate();\n        let b = NetHost::start(hermetic(&b_id)).expect(\"host b\");\n\n        let (conn_id, remote) = a.dial(b.addr()).expect(\"dial\");\n        assert_eq!(conn_id, 1, \"first broker-minted conn id\");\n        assert_eq!(\n…\n        );\n…\n    }\n…\n}"}],"details":{"displayContent":{"text":"struct StreamEntry {\n…\n    /// Whether THIS node opened the stream (`open_stream`) vs the peer\n    /// (accept loop). The inbound dispatcher (D9-1) claims only\n    /// peer-initiated streams; without this fact the table is ambiguous —\n    /// it holds locally-opened outbound streams too (gotcha: a dispatcher\n    /// that served its own requester stream would deadlock on itself).\n    initiated_locally: bool,\n    /// The send half, serialized behind an async mutex (writes happen on the\n    /// host's runtime via `block_on`).\n    send: tokio::sync::Mutex<SendHalf>,\n    log: Arc<Mutex<StreamLog>>,\n    /// Wakes the read pump when a retentive (tunnel) stream's ring drains below\n    /// its backpressure cap (M11-W3, REQ-SHELL-4) — see [`StreamLog::retentive_cap`].\n    /// Unused by ordinary (non-retentive) streams.\n    room: Arc<tokio::sync::Notify>,\n    /// Retired = invisible to dispatch eligibility (ADR-0038 A), NOT torn down:\n    /// retire must never sever the transport — the worker's reply send is\n    /// fire-and-forget, so a retire that dropped this entry could overtake the\n    /// still-queued reply inside the broker and reset the never-finished QUIC\n    /// send half (requester sees torn-EOF, no reply). The reply-flush ordering\n    /// is load-bearing; physical teardown belongs to the conn-close sweep only.\n    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n    retired: std::sync::atomic::AtomicBool,\n}\n\n/// Tables shared between the host's sync surface and its runtime tasks\n/// (accept loop, per-conn stream acceptors, per-stream read pumps).\nstruct NetShared {\n    next_conn_id: AtomicU64,\n    conns: Mutex<HashMap<u64, ConnEntry>>,\n    next_stream_id: AtomicU64,\n    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,\n    ring_cap: usize,\n    /// The conn-liveness event log (D4c) — one per host, all conns interleaved.\n    presence: Mutex<PresenceLog>,\n}\n\nimpl NetShared {\n    fn new() -> Self {\n        NetShared {\n            next_conn_id: AtomicU64::new(1),\n            conns: Mutex::new(HashMap::new()),\n            next_stream_id: AtomicU64::new(1),\n            streams: Mutex::new(HashMap::new()),\n            ring_cap: ring_cap_from_env(),\n            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),\n        }\n    }\n}\n…\nimpl NetHost {\n…\n    pub fn send_stream(&self, stream_id: u64, bytes: &[u8], finish: bool) -> io::Result<()> {\n…\n        self.bounded_block_on(async move {\n…\n            if finish {\n                send.finish().await?;\n            }\n            Ok(())\n        })\n    }\n\n    /// Every held stream's info — the query a re-attaching brain uses to find\n    /// streams (including peer-initiated ones that arrived while it was down).\n    /// Retired rows are hidden (ADR-0038 A): their exchange is over, and this\n    /// enumeration is exactly what a future dispatcher generation would replay.\n    pub fn stream_infos(&self) -> Vec<NetStreamInfo> {\n        let streams = self.shared.streams.lock().unwrap();\n        streams\n            .iter()\n            .filter(|(_, e)| !e.retired.load(std::sync::atomic::Ordering::SeqCst))\n            .map(|(id, e)| {\n                let log = e.log.lock().unwrap();\n                NetStreamInfo {\n                    stream_id: *id,\n                    conn_id: e.conn_id,\n                    remote_id_hex: e.remote_id_hex.clone(),\n                    next_seq: log.next_seq,\n                    finished: log.finished,\n                    initiated_locally: e.initiated_locally,\n                    floor_seq: log.floor_seq(),\n                }\n            })\n            .collect()\n    }\n\n    /// Retire a stream row (ADR-0038 A): the dispatcher reports a stream whose\n    /// serve loop ran to completion — its exchange is over, so no future\n    /// dispatcher generation may rediscover and replay it (the control-steal\n    /// hazard). Retirement is ELIGIBILITY-VISIBILITY, not teardown: the row is\n    /// hidden from [`stream_infos`](Self::stream_infos) and the opener query\n    /// and its ring is freed, but the entry (send half, subscriber, pump) stays\n    /// alive so an in-flight reply always flushes — retire raced fire-and-forget\n    /// reply sends when it removed the entry (see the `retired` field note).\n    /// Physical teardown stays with the conn-close sweep. Returns whether this\n    /// call newly retired the row (idempotent; absent row = false).\n    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n    pub fn retire_stream(&self, stream_id: u64) -> bool {\n        let Some(entry) = self\n            .shared\n            .streams\n            .lock()\n            .unwrap()\n            .get(&stream_id)\n            .map(Arc::clone)\n        else {\n            return false;\n        };\n        let newly = !entry\n            .retired\n            .swap(true, std::sync::atomic::Ordering::SeqCst);\n        if newly {\n            // A retired exchange's request bytes are dead weight — free the\n            // ring; the pinned opener and cursors stay (cheap, bounded).\n            entry.log.lock().unwrap().ring.clear();\n        }\n        newly\n    }\n\n    /// The stream's pinned opener line (ADR-0038 B) — the restart-durable\n    /// classification fact, independent of the evictable data ring. Outer\n    /// `None` = no such stream; inner `None` = no opener pinned (first `\\n`\n    /// not yet arrived, or capture gave up bounded).\n    // [impl->REQ-STREAM-OPENER-DURABLE]\n    pub fn stream_opener(&self, stream_id: u64) -> Option<Option<Vec<u8>>> {\n        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n        // A retired row answers no-such-stream: classification exists only to\n        // serve, and a retired exchange must never be served again.\n        if entry.retired.load(std::sync::atomic::Ordering::SeqCst) {\n            return None;\n        }\n…\n    }\n…\n\n    /// Gracefully close the endpoint: every held connection flushes its QUIC\n    /// CLOSE so peers' closed-watchers fire promptly (a bare host drop kills\n    /// the runtime first and leaves peers to an idle timeout).\n    pub fn close(&self) {\n        let endpoint = Arc::clone(&self.endpoint);\n        self.runtime.handle().block_on(async move {\n            endpoint.close().await;\n        });\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn hermetic(identity: &Identity) -> NetConfig {\n        NetConfig {\n            identity: Identity::from_seed(&identity.seed()),\n            relay: RelayPolicy::Disabled,\n            discovery: LocalDiscovery::Off,\n            scope: BindScope::Loopback,\n            membership: None,\n        }\n    }\n\n    // [unit->REQ-DAEMON-2] the endpoint is constructed broker-side (inside the\n    // host's dedicated runtime) with relays disabled, bound to the node's own\n    // identity, with an empty conn table.\n    #[test]\n    fn host_binds_endpoint_to_node_identity() {\n        let id = Identity::generate();\n        let host = NetHost::start(hermetic(&id)).expect(\"host start\");\n        assert_eq!(host.node_id_hex(), id.public_key().to_hex());\n        assert_eq!(host.conn_count(), 0);\n        assert!(!host.addr_json().is_null(), \"dialable address serializes\");\n    }\n\n    // [unit->REQ-OPID-MINTER-NAMESPACE] the dial/stream op maps are minter-namespaced\n    // (ADR-0034): two minters that mint the SAME op integer (every counter starts at\n    // 1) must record + look up DISTINCT connections/streams — no clobber. RED-first:\n    // under the old `HashMap<u64, u64>` the second record overwrote the first, so both\n    // lookups returned the SAME (last-written) value — a cross-minter clobber.\n    #[test]\n    fn op_maps_are_minter_namespaced() {\n        let host = NetHost::start(hermetic(&Identity::generate())).expect(\"host start\");\n        let op = 3u64; // the same integer from two independent counters.\n\n        host.record_dial_op(Minter::Rc, op, 100);\n        host.record_dial_op(Minter::Pump, op, 200);\n        host.record_dial_op(Minter::Shell, op, 300);\n        assert_eq!(host.dial_op_conn(Minter::Rc, op), Some(100));\n        assert_eq!(host.dial_op_conn(Minter::Pump, op), Some(200));\n        assert_eq!(host.dial_op_conn(Minter::Shell, op), Some(300));\n        assert_eq!(\n            host.dial_op_conn(Minter::Cli, op),\n            None,\n            \"an unrecorded minter's op is absent, not another minter's conn\"\n        );\n\n        host.record_stream_op(Minter::Rc, op, 11);\n        host.record_stream_op(Minter::Pump, op, 22);\n        assert_eq!(host.stream_op_id(Minter::Rc, op), Some(11));\n        assert_eq!(host.stream_op_id(Minter::Pump, op), Some(22));\n    }\n\n    // [unit->REQ-HAZARD-RESTART-IDEMPOTENT] StreamLog.next_seq counts read-pump\n    // APPENDS (transport reads), NOT logical sender chunks: each append() bumps it\n    // by one regardless of byte size. Under QUIC coalescing, N logical sender\n    // writes can arrive as M < N appends (one read_chunk carrying several writes'\n    // bytes), so next_seq == M, not N. This documents WHY the netstream int test\n    // must gate on BYTES (byte-exact + exactly-once), never on\n    // seq == logical-chunk-count (the Windows coalescing flake). The bytes are\n    // retained + drained in arrival order, independent of the seq count.\n    #[test]\n    fn next_seq_counts_appends_not_logical_chunks() {\n        let mut log = StreamLog::new(7, 1024);\n        assert_eq!(log.next_seq, 0);\n        // Two appends of wildly different sizes (a coalesced read carrying many\n        // logical writes vs a 1-byte read) each bump next_seq by exactly one.\n        log.append(b\"x\");\n        log.append(b\"a-much-larger-coalesced-read-carrying-many-logical-sender-writes\");\n        assert_eq!(\n            log.next_seq, 2,\n            \"two appends -> next_seq 2, regardless of per-append byte size\"\n        );\n        // The BYTES are retained + drained in arrival order, framing-agnostic.\n        let (bytes, _finished) = log.drain();\n        assert_eq!(\n            bytes,\n            b\"xa-much-larger-coalesced-read-carrying-many-logical-sender-writes\".to_vec()\n        );\n    }\n\n    // [unit->REQ-DAEMON-2] a broker-owned host dials a second in-process host\n    // over hermetic loopback QUIC; both sides register the connection — the\n    // dialer in its table, the acceptor via its accept loop.\n    #[test]\n    fn two_hosts_dial_over_loopback() {\n        let a = NetHost::start(hermetic(&Identity::generate())).expect(\"host a\");\n        let b_id = Identity::generate();\n        let b = NetHost::start(hermetic(&b_id)).expect(\"host b\");\n\n        let (conn_id, remote) = a.dial(b.addr()).expect(\"dial\");\n        assert_eq!(conn_id, 1, \"first broker-minted conn id\");\n        assert_eq!(\n…\n        );\n…\n    }\n…\n}","startLine":522,"lineNumbers":[522,null,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,null,912,null,1501,null,1515,null,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,null,1598,null,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,null,1829,null,1845,null,2211]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs"}}},"isError":false,"timestamp":1784247652582}}
{"type":"message","id":"f1c9d71a","parentId":"6d55b1c9","timestamp":"2026-07-17T00:20:52.603Z","message":{"role":"toolResult","toolCallId":"call_6CcYCOP97oW1MVXc59k2vTTh|fc_0996e78cf50e3160016a597563ac588199a848b23f965ca029","toolName":"read","content":[{"type":"text","text":"pub fn serve_attach(\n…\n) -> io::Result<AttachServeOutcome> {\n…\n    loop {\n…\n        match event {\n…\n            BrokerEvent::NetStreamData {\n…\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Request {\n…\n                            endpoint_id: _,\n                        } => {\n                            if let Some(out) = 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                                &mut intent,\n                                &mut attached_session,\n                                &mut attached,\n                            )? {\n                                return Ok(out);\n                            }\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                                    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                            // Controller → PTY resize (controller-EXCLUSIVE; the\n                            // broker rejects it on a viewer's stream). A viewer's\n                            // Resize is dropped here by the intent gate too.\n                            let _ = brain.resize(rows, cols);\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, .. } => {\n                use crate::msg::SubscribeOutcome as O;\n                match outcome {\n                    O::Controller | O::TookControl => {\n                        role = Some(ServeRole::Controller);\n                        // A driver attached — the wake edge (controller-only;\n                        // idempotent on an already-active seat).\n                        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                        for (op, b) in pending_input.drain(..) {\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\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            }\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\n            BrokerEvent::Size { rows, cols, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\n                wire.net_stream_send(stream_id, &line, None, false)?;\n            }\n            // This controller was kicked by a different operator's `--take`.\n            BrokerEvent::Displaced { by, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\n                wire.net_stream_send(stream_id, &line, None, true)?;\n                // No dormant edge: the endpoint stays driven by the taker (whose\n                // own serve loop already fired the wake).\n                return Ok(AttachServeOutcome::Displaced);\n            }\n\n            // This VIEWER overflowed its broker subscription queue and was evicted\n            // (it forwards slower than the drain fans out under flood). SKIP TO\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\n            // spin. The next live Output carries a forward seq jump; the operator's\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n            BrokerEvent::ViewerEvicted { session_id: sid }\n                if attached && Some(sid) == attached_session =>\n            {\n                if let Some(prev) = last_resubscribe {\n                    let since = prev.elapsed();\n                    if since < RESUBSCRIBE_INTERVAL {\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\n                    }\n                }\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\n                last_resubscribe = Some(Instant::now());\n            }\n\n            // The operator closed its send side: the viewport is over.\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                wire.net_stream_send(stream_id, &[], None, true)?;\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\n                // info.json, carrying the current driven_by through), THEN release\n                // the role over IPC so the broker's driven_by=None is the LAST\n                // write — otherwise a concurrent resting RMW restores the stale\n                // marker (the clear-race the broker-single-writer model averts).\n                if role == Some(ServeRole::Controller) {\n                    // The driver detached — the endpoint rests warm (controller-\n                    // only; a viewer leaving is wake-neutral).\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                if let Some(sid) = attached_session {\n                    let _ = brain.detach_session(sid); // release the role (clears markers)\n                }\n                return Ok(AttachServeOutcome::Detached);\n            }\n\n            // ── Session → wire: output and exit. ────────────────────────────\n            BrokerEvent::Output { seq, bytes, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Output {\n                    seq,\n                    data_b64: encode_bytes(&bytes),\n                });\n                // Unjournaled by design (see module docs): `seq` rides inside\n                // the record, and the operator's render cursor is the dedup.\n                wire.net_stream_send(stream_id, &line, None, false)?;\n            }\n            BrokerEvent::Exit { code, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Exit { code });\n                wire.net_stream_send(stream_id, &line, None, true)?;\n                if role == Some(ServeRole::Controller) {\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                return Ok(AttachServeOutcome::Exited(code));\n            }\n\n            BrokerEvent::Error { message } => {\n                if role == Some(ServeRole::Controller) {\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                return Err(io::Error::other(message));\n            }\n            // Applied/NetSent acks and unrelated events: not ours to handle.\n            _ => {}\n        }\n    }\n}\n\n/// Operator-side: open an attach stream on a dialed connection and send the\n/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\n/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\n/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\n/// durable id for the exactly-once stream open (safe to block for its ack —\n/// no attach traffic can be in flight before the stream exists); the Request\n/// send itself is unjournaled (see module docs — a replayed Request re-gates\n/// and re-attaches idempotently).\n// [impl->REQ-INST-8]\npub fn request_attach(\n    brain: &mut Brain,\n    conn_id: u64,\n    session_id: u64,\n    from_seq: u64,\n    open_op: MintedOp,\n    intent: AttachIntent,\n) -> io::Result<u64> {\n    request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)\n}\n\n\n[Showing lines 429-628 of 692. Use :629 to continue]"}],"details":{"truncation":{"content":"                            endpoint_id: _,\r\n                        } => {\r\n                            if let Some(out) = handle_attach_request(\r\n                                brain,\r\n                                &mut wire,\r\n                                stream_id,\r\n                                endpoint,\r\n                                origin_node,\r\n                                remote_session,\r\n                                session_id,\r\n                                from_seq,\r\n                                req_intent,\r\n                                &mut intent,\r\n                                &mut attached_session,\r\n                                &mut attached,\r\n                            )? {\r\n                                return Ok(out);\r\n                            }\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                            match role {\r\n                                // Confirmed controller — exactly-once at the PTY\r\n                                // (the operator-minted id rides the B5 journal). NO-ACK\r\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\r\n                                // fire-and-forward (the ack is discarded here) and can\r\n                                // flood N input frames on this one conn; an acked write\r\n                                // back would fill the return direction and deadlock the\r\n                                // broker's per-conn handler. Dedup still holds at the\r\n                                // broker (op_id applied-set).\r\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n                                Some(ServeRole::Controller) => {\r\n                                    // The op_id is operator-minted (rc attach OpMinter),\r\n                                    // so it carries the `rc` journal tag (ADR-0034).\r\n                                    brain.send_effect_no_ack(\r\n                                        MintedOp::new(Minter::Rc, op_id),\r\n                                        &bytes,\r\n                                    )?\r\n                                }\r\n                                // Outcome not yet known — buffer; flushed on a\r\n                                // controller confirmation, discarded on busy/viewer.\r\n                                None => pending_input.push((op_id, bytes)),\r\n                                // A viewer never reaches here (intent gate).\r\n                                Some(ServeRole::Viewer) => {}\r\n                            }\r\n                        }\r\n                        AttachRecord::Resize { rows, cols }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\r\n                            // broker rejects it on a viewer's stream). A viewer's\r\n                            // Resize is dropped here by the intent gate too.\r\n                            let _ = brain.resize(rows, cols);\r\n                        }\r\n                        // Input/Resize before Request, a viewer's input/resize,\r\n                        // or target-direction records echoed back: noise — ignore.\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n\r\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\r\n            BrokerEvent::Subscribed { outcome, .. } => {\r\n                use crate::msg::SubscribeOutcome as O;\r\n                match outcome {\r\n                    O::Controller | O::TookControl => {\r\n                        role = Some(ServeRole::Controller);\r\n                        // A driver attached — the wake edge (controller-only;\r\n                        // idempotent on an already-active seat).\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\r\n                        // Flush any input that raced ahead of the reply (operator-\r\n                        // minted ops carry the `rc` tag).\r\n                        for (op, b) in pending_input.drain(..) {\r\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\r\n                        }\r\n                    }\r\n                    O::Viewer => {\r\n                        role = Some(ServeRole::Viewer);\r\n                        // Wake-neutral: a viewer never wakes the endpoint, never\r\n                        // keeps it awake (Gap B). Discard any racing input.\r\n                        pending_input.clear();\r\n                    }\r\n                    O::BusyControlled { .. } => {\r\n                        // A different operator controls it — refuse (no PTY input\r\n                        // ever reached it; the buffer is discarded). The rc\r\n                        // renders the busy guidance client-side from driven_by.\r\n                        wire.net_stream_send(stream_id, &[], None, true)?;\r\n                        return Ok(AttachServeOutcome::Refused);\r\n                    }\r\n                }\r\n            }\r\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\r\n            BrokerEvent::Size { rows, cols, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            // This controller was kicked by a different operator's `--take`.\r\n            BrokerEvent::Displaced { by, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                // No dormant edge: the endpoint stays driven by the taker (whose\r\n                // own serve loop already fired the wake).\r\n                return Ok(AttachServeOutcome::Displaced);\r\n            }\r\n\r\n            // This VIEWER overflowed its broker subscription queue and was evicted\r\n            // (it forwards slower than the drain fans out under flood). SKIP TO\r\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\r\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\r\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\r\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\r\n            // spin. The next live Output carries a forward seq jump; the operator's\r\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\r\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\r\n            BrokerEvent::ViewerEvicted { session_id: sid }\r\n                if attached && Some(sid) == attached_session =>\r\n            {\r\n                if let Some(prev) = last_resubscribe {\r\n                    let since = prev.elapsed();\r\n                    if since < RESUBSCRIBE_INTERVAL {\r\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\r\n                    }\r\n                }\r\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\r\n                last_resubscribe = Some(Instant::now());\r\n            }\r\n\r\n            // The operator closed its send side: the viewport is over.\r\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\r\n                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\r\n                // info.json, carrying the current driven_by through), THEN release\r\n                // the role over IPC so the broker's driven_by=None is the LAST\r\n                // write — otherwise a concurrent resting RMW restores the stale\r\n                // marker (the clear-race the broker-single-writer model averts).\r\n                if role == Some(ServeRole::Controller) {\r\n                    // The driver detached — the endpoint rests warm (controller-\r\n                    // only; a viewer leaving is wake-neutral).\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                if let Some(sid) = attached_session {\r\n                    let _ = brain.detach_session(sid); // release the role (clears markers)\r\n                }\r\n                return Ok(AttachServeOutcome::Detached);\r\n            }\r\n\r\n            // ── Session → wire: output and exit. ────────────────────────────\r\n            BrokerEvent::Output { seq, bytes, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Output {\r\n                    seq,\r\n                    data_b64: encode_bytes(&bytes),\r\n                });\r\n                // Unjournaled by design (see module docs): `seq` rides inside\r\n                // the record, and the operator's render cursor is the dedup.\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            BrokerEvent::Exit { code, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Exit { code });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Ok(AttachServeOutcome::Exited(code));\r\n            }\r\n\r\n            BrokerEvent::Error { message } => {\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Err(io::Error::other(message));\r\n            }\r\n            // Applied/NetSent acks and unrelated events: not ours to handle.\r\n            _ => {}\r\n        }\r\n    }\r\n}\r\n\r\n/// Operator-side: open an attach stream on a dialed connection and send the\r\n/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\r\n/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\r\n/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\r\n/// durable id for the exactly-once stream open (safe to block for its ack —\r\n/// no attach traffic can be in flight before the stream exists); the Request\r\n/// send itself is unjournaled (see module docs — a replayed Request re-gates\r\n/// and re-attaches idempotently).\r\n// [impl->REQ-INST-8]\r\npub fn request_attach(\r\n    brain: &mut Brain,\r\n    conn_id: u64,\r\n    session_id: u64,\r\n    from_seq: u64,\r\n    open_op: MintedOp,\r\n    intent: AttachIntent,\r\n) -> io::Result<u64> {\r\n    request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)\r\n}\r\n\r","truncated":true,"truncatedBy":"lines","totalLines":264,"totalBytes":11125,"outputLines":200,"outputBytes":11125,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"pub fn serve_attach(\n…\n) -> io::Result<AttachServeOutcome> {\n…\n    loop {\n…\n        match event {\n…\n            BrokerEvent::NetStreamData {\n…\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Request {\n…\n                            endpoint_id: _,\r\n                        } => {\r\n                            if let Some(out) = handle_attach_request(\r\n                                brain,\r\n                                &mut wire,\r\n                                stream_id,\r\n                                endpoint,\r\n                                origin_node,\r\n                                remote_session,\r\n                                session_id,\r\n                                from_seq,\r\n                                req_intent,\r\n                                &mut intent,\r\n                                &mut attached_session,\r\n                                &mut attached,\r\n                            )? {\r\n                                return Ok(out);\r\n                            }\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                            match role {\r\n                                // Confirmed controller — exactly-once at the PTY\r\n                                // (the operator-minted id rides the B5 journal). NO-ACK\r\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\r\n                                // fire-and-forward (the ack is discarded here) and can\r\n                                // flood N input frames on this one conn; an acked write\r\n                                // back would fill the return direction and deadlock the\r\n                                // broker's per-conn handler. Dedup still holds at the\r\n                                // broker (op_id applied-set).\r\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n                                Some(ServeRole::Controller) => {\r\n                                    // The op_id is operator-minted (rc attach OpMinter),\r\n                                    // so it carries the `rc` journal tag (ADR-0034).\r\n                                    brain.send_effect_no_ack(\r\n                                        MintedOp::new(Minter::Rc, op_id),\r\n                                        &bytes,\r\n                                    )?\r\n                                }\r\n                                // Outcome not yet known — buffer; flushed on a\r\n                                // controller confirmation, discarded on busy/viewer.\r\n                                None => pending_input.push((op_id, bytes)),\r\n                                // A viewer never reaches here (intent gate).\r\n                                Some(ServeRole::Viewer) => {}\r\n                            }\r\n                        }\r\n                        AttachRecord::Resize { rows, cols }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\r\n                            // broker rejects it on a viewer's stream). A viewer's\r\n                            // Resize is dropped here by the intent gate too.\r\n                            let _ = brain.resize(rows, cols);\r\n                        }\r\n                        // Input/Resize before Request, a viewer's input/resize,\r\n                        // or target-direction records echoed back: noise — ignore.\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n\r\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\r\n            BrokerEvent::Subscribed { outcome, .. } => {\r\n                use crate::msg::SubscribeOutcome as O;\r\n                match outcome {\r\n                    O::Controller | O::TookControl => {\r\n                        role = Some(ServeRole::Controller);\r\n                        // A driver attached — the wake edge (controller-only;\r\n                        // idempotent on an already-active seat).\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\r\n                        // Flush any input that raced ahead of the reply (operator-\r\n                        // minted ops carry the `rc` tag).\r\n                        for (op, b) in pending_input.drain(..) {\r\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\r\n                        }\r\n                    }\r\n                    O::Viewer => {\r\n                        role = Some(ServeRole::Viewer);\r\n                        // Wake-neutral: a viewer never wakes the endpoint, never\r\n                        // keeps it awake (Gap B). Discard any racing input.\r\n                        pending_input.clear();\r\n                    }\r\n                    O::BusyControlled { .. } => {\r\n                        // A different operator controls it — refuse (no PTY input\r\n                        // ever reached it; the buffer is discarded). The rc\r\n                        // renders the busy guidance client-side from driven_by.\r\n                        wire.net_stream_send(stream_id, &[], None, true)?;\r\n                        return Ok(AttachServeOutcome::Refused);\r\n                    }\r\n                }\r\n            }\r\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\r\n            BrokerEvent::Size { rows, cols, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            // This controller was kicked by a different operator's `--take`.\r\n            BrokerEvent::Displaced { by, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                // No dormant edge: the endpoint stays driven by the taker (whose\r\n                // own serve loop already fired the wake).\r\n                return Ok(AttachServeOutcome::Displaced);\r\n            }\r\n\r\n            // This VIEWER overflowed its broker subscription queue and was evicted\r\n            // (it forwards slower than the drain fans out under flood). SKIP TO\r\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\r\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\r\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\r\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\r\n            // spin. The next live Output carries a forward seq jump; the operator's\r\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\r\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\r\n            BrokerEvent::ViewerEvicted { session_id: sid }\r\n                if attached && Some(sid) == attached_session =>\r\n            {\r\n                if let Some(prev) = last_resubscribe {\r\n                    let since = prev.elapsed();\r\n                    if since < RESUBSCRIBE_INTERVAL {\r\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\r\n                    }\r\n                }\r\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\r\n                last_resubscribe = Some(Instant::now());\r\n            }\r\n\r\n            // The operator closed its send side: the viewport is over.\r\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\r\n                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\r\n                // info.json, carrying the current driven_by through), THEN release\r\n                // the role over IPC so the broker's driven_by=None is the LAST\r\n                // write — otherwise a concurrent resting RMW restores the stale\r\n                // marker (the clear-race the broker-single-writer model averts).\r\n                if role == Some(ServeRole::Controller) {\r\n                    // The driver detached — the endpoint rests warm (controller-\r\n                    // only; a viewer leaving is wake-neutral).\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                if let Some(sid) = attached_session {\r\n                    let _ = brain.detach_session(sid); // release the role (clears markers)\r\n                }\r\n                return Ok(AttachServeOutcome::Detached);\r\n            }\r\n\r\n            // ── Session → wire: output and exit. ────────────────────────────\r\n            BrokerEvent::Output { seq, bytes, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Output {\r\n                    seq,\r\n                    data_b64: encode_bytes(&bytes),\r\n                });\r\n                // Unjournaled by design (see module docs): `seq` rides inside\r\n                // the record, and the operator's render cursor is the dedup.\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            BrokerEvent::Exit { code, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Exit { code });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Ok(AttachServeOutcome::Exited(code));\r\n            }\r\n\r\n            BrokerEvent::Error { message } => {\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Err(io::Error::other(message));\r\n            }\r\n            // Applied/NetSent acks and unrelated events: not ours to handle.\r\n            _ => {}\r\n        }\r\n    }\r\n}\r\n\r\n/// Operator-side: open an attach stream on a dialed connection and send the\r\n/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\r\n/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\r\n/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\r\n/// durable id for the exactly-once stream open (safe to block for its ack —\r\n/// no attach traffic can be in flight before the stream exists); the Request\r\n/// send itself is unjournaled (see module docs — a replayed Request re-gates\r\n/// and re-attaches idempotently).\r\n// [impl->REQ-INST-8]\r\npub fn request_attach(\r\n    brain: &mut Brain,\r\n    conn_id: u64,\r\n    session_id: u64,\r\n    from_seq: u64,\r\n    open_op: MintedOp,\r\n    intent: AttachIntent,\r\n) -> io::Result<u64> {\r\n    request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)\r\n}\r\n\r","startLine":293,"lineNumbers":[293,null,302,null,374,null,412,null,414,null,418,419,420,421,null,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,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\attach.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":692,"totalBytes":11125,"outputLines":200,"outputBytes":11125,"shownRange":{"start":429,"end":628},"nextOffset":629}}},"isError":false,"timestamp":1784247652603}}
{"type":"message","id":"df2b5f0b","parentId":"f1c9d71a","timestamp":"2026-07-17T00:20:52.654Z","message":{"role":"toolResult","toolCallId":"call_GTiD30PZNHVTnRtLLSWosFLb|fc_0996e78cf50e3160016a597563ac6881998004f7f8731cf008","toolName":"read","content":[{"type":"text","text":"    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\n        self.sessions\n            .lock()\n            .unwrap()\n            .get(&id)\n            .map(|h| recover_log(&h.log).next_seq)\n    }\n\n    /// Drive one brain connection: handshake, then dispatch its command frames\n    /// until it disconnects. On disconnect, detach this connection from any\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\n    /// restart must not disturb the hosted children).\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n        struct HandlerTally<'a>(&'a AtomicUsize);\n        impl Drop for HandlerTally<'_> {\n            fn drop(&mut self) {\n                self.0.fetch_sub(1, Ordering::AcqRel);\n            }\n        }\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\n        let _tally = HandlerTally(&self.conn_handlers);\n\n        let (mut recv, send) = conn.split();\n        // Round-4: the send half is owned by the bounded/cancelable conn\n        // primitive from birth — every write on this physical connection is\n        // deadline-bounded, and a wedge poisons + retires the conn.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        send.describe(\"role=brain\");\n\n        // Version handshake: this connection must be a brain (2.3).\n        recv_hello(&mut recv, Role::Brain)?;\n\n        let mut my_subs: Vec<u64> = Vec::new();\n        let mut my_stream_subs: Vec<u64> = Vec::new();\n        let mut my_presence_sub = false;\n\n        loop {\n            let env = match read_frame(&mut recv) {\n                Ok(e) => e,\n                // Any read failure (EOF included) ends the connection.\n                Err(_) => break,\n            };\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_UNSUBSCRIBE => {\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_INPUT => {\n                    if let Err(msg) = self.dispatch_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_RESIZE => {\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_KILL => {\n                    if let Err(msg) = self.dispatch_kill(env) {\n                        send_error(&send, &msg);\n                    }\n                }\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\n                KIND_ADAPTER_APPLY => {\n                    if let Err(msg) = self.dispatch_adapter_apply(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STATUS => self.dispatch_net_status(&send),\n                // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\n                KIND_BROKER_IMAGE => self.dispatch_broker_image(&send),\n                KIND_STALL_EVICTS => self.dispatch_stall_evicts(&send),\n                KIND_NET_DIAL => {\n                    if let Err(msg) = self.dispatch_net_dial(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_DIAL_LOOPBACK => {\n                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                // [impl->REQ-PUMP-PEER-ISOLATION]\n                KIND_NET_DIAL_SUBMIT => {\n                    if let Err(msg) = self.dispatch_net_dial_submit(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_JOIN => {\n                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_MEET => {\n                    if let Err(msg) = self.dispatch_pair_meet(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_PAIR_CODE_SUBMIT => {\n                    if let Err(msg) = self.dispatch_pair_code_submit(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_OPEN => {\n                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_SEND => {\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAMS => {\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_RETIRE => {\n                    if let Err(msg) = self.dispatch_net_stream_retire(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_OPENER => {\n                    if let Err(msg) = self.dispatch_net_stream_opener(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_SESSIONS => {\n                    // Clone the (id, endpoint, log) out from under the sessions\n                    // lock, then read each log's cursor under the log lock alone —\n                    // never both locks at once (the dispatch_subscribe discipline).\n                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {\n                        let sessions = recover(&self.sessions);\n                        sessions\n                            .iter()\n                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))\n                            .collect()\n                    };\n                    // Snapshot each session's truth under its OWN log lock (cursor +\n                    // controller identity + the control/viewer stamp truth), doing NO\n                    // file I/O under the lock (the lock-across-effect discipline, KH\n                    // 7.12/5.16). The convergence re-assert + the reply then run OFF\n                    // the lock.\n                    struct SessSnap {\n                        id: u64,\n                        endpoint: String,\n                        resume_seq: u64,\n                        controller_by: Option<String>,\n                        has_controller: bool,\n                        viewers: u32,\n                    }\n                    let snaps: Vec<SessSnap> = rows\n                        .into_iter()\n                        .map(|(id, endpoint, log)| {\n                            let mut log = recover_log(&log);\n                            // B-2 (REQ-CONTROLLER-LIVENESS-REAP): make the controller\n                            // truth HONEST before reading it — reap a dead-writer\n                            // (severed) controller so both the reply's controller_by\n                            // AND the off-lock converge_perch_stamps below see the\n                            // cleared state and the stale info.json stamp clears.\n                            let _ = log.reap_dead_controller();\n                            SessSnap {\n                                id,\n                                endpoint,\n                                resume_seq: log.delivered_through(),\n                                controller_by: log.controller_by().map(str::to_string),\n                                has_controller: log.has_controller(),\n                                viewers: log.live_viewer_count(),\n                            }\n                        })\n                        .collect();\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\n                    // each live session's control/viewer stamps to broker truth,\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\n                    // pre-bind); converges within one reconcile-poll window after bind.\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n                    for s in &snaps {\n                        converge_perch_stamps(\n                            &s.endpoint,\n                            s.controller_by.as_deref(),\n                            s.has_controller,\n                            s.viewers,\n                        );\n                    }\n                    let reply = SessionsReply {\n                        sessions: snaps\n                            .into_iter()\n                            .map(|s| SessionInfo {\n                                session_id: s.id,\n                                endpoint: s.endpoint,\n                                resume_seq: s.resume_seq,\n                                controller_by: s.controller_by,\n                            })\n                            .collect(),\n                    };\n                    let frame = Envelope::new(\n                        KIND_SESSIONS_REPLY,\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\n                    );\n                    send_frame(&send, &frame);\n                }\n                KIND_ENDPOINT_INPUT => {\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n                    Ok(id) => my_stream_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_NET_PRESENCE_SUBSCRIBE => {\n                    match self.dispatch_net_presence_subscribe(env, &send) {\n                        Ok(()) => my_presence_sub = true,\n                        Err(msg) => send_error(&send, &msg),\n                    }\n                }\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n                // An unknown kind is tolerated (forward-compat): report, don't die.\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n            }\n        }\n\n        // Brain gone: detach as subscriber but leave the sessions running.\n        let sessions = recover(&self.sessions);\n        for id in &my_subs {\n            if let Some(h) = sessions.get(id) {\n                recover_log(&h.log).detach_if(&send);\n            }\n        }\n        drop(sessions);\n        // Same for net streams: the streams + their rings persist (D4b).\n        if let Some(host) = self.net.get() {\n            for id in &my_stream_subs {\n                host.detach_stream_sub(*id, &send);\n            }\n            // And presence: the liveness log + its ring persist (D4c).\n            if my_presence_sub {\n                host.detach_presence_sub(&send);\n            }\n        }\n        Ok(())\n    }\n\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\n\n[Showing lines 2969-3228 of 6063. Use :3229 to continue]"}],"details":{"truncation":{"content":"        self.sessions\r\n            .lock()\r\n            .unwrap()\r\n            .get(&id)\r\n            .map(|h| recover_log(&h.log).next_seq)\r\n    }\r\n\r\n    /// Drive one brain connection: handshake, then dispatch its command frames\r\n    /// until it disconnects. On disconnect, detach this connection from any\r\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\r\n    /// restart must not disturb the hosted children).\r\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\r\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\r\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\r\n        struct HandlerTally<'a>(&'a AtomicUsize);\r\n        impl Drop for HandlerTally<'_> {\r\n            fn drop(&mut self) {\r\n                self.0.fetch_sub(1, Ordering::AcqRel);\r\n            }\r\n        }\r\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\r\n        let _tally = HandlerTally(&self.conn_handlers);\r\n\r\n        let (mut recv, send) = conn.split();\r\n        // Round-4: the send half is owned by the bounded/cancelable conn\r\n        // primitive from birth — every write on this physical connection is\r\n        // deadline-bounded, and a wedge poisons + retires the conn.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\r\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\r\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\r\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        send.describe(\"role=brain\");\r\n\r\n        // Version handshake: this connection must be a brain (2.3).\r\n        recv_hello(&mut recv, Role::Brain)?;\r\n\r\n        let mut my_subs: Vec<u64> = Vec::new();\r\n        let mut my_stream_subs: Vec<u64> = Vec::new();\r\n        let mut my_presence_sub = false;\r\n\r\n        loop {\r\n            let env = match read_frame(&mut recv) {\r\n                Ok(e) => e,\r\n                // Any read failure (EOF included) ends the connection.\r\n                Err(_) => break,\r\n            };\r\n            match env.kind.as_str() {\r\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_UNSUBSCRIBE => {\r\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_INPUT => {\r\n                    if let Err(msg) = self.dispatch_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_RESIZE => {\r\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_KILL => {\r\n                    if let Err(msg) = self.dispatch_kill(env) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n                KIND_ADAPTER_APPLY => {\r\n                    if let Err(msg) = self.dispatch_adapter_apply(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STATUS => self.dispatch_net_status(&send),\r\n                // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\r\n                KIND_BROKER_IMAGE => self.dispatch_broker_image(&send),\r\n                KIND_STALL_EVICTS => self.dispatch_stall_evicts(&send),\r\n                KIND_NET_DIAL => {\r\n                    if let Err(msg) = self.dispatch_net_dial(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_DIAL_LOOPBACK => {\r\n                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-PUMP-PEER-ISOLATION]\r\n                KIND_NET_DIAL_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_net_dial_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_JOIN => {\r\n                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_MEET => {\r\n                    if let Err(msg) = self.dispatch_pair_meet(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_CODE_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_pair_code_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_OPEN => {\r\n                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SEND => {\r\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAMS => {\r\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_RETIRE => {\r\n                    if let Err(msg) = self.dispatch_net_stream_retire(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_OPENER => {\r\n                    if let Err(msg) = self.dispatch_net_stream_opener(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_SESSIONS => {\r\n                    // Clone the (id, endpoint, log) out from under the sessions\r\n                    // lock, then read each log's cursor under the log lock alone —\r\n                    // never both locks at once (the dispatch_subscribe discipline).\r\n                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {\r\n                        let sessions = recover(&self.sessions);\r\n                        sessions\r\n                            .iter()\r\n                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))\r\n                            .collect()\r\n                    };\r\n                    // Snapshot each session's truth under its OWN log lock (cursor +\r\n                    // controller identity + the control/viewer stamp truth), doing NO\r\n                    // file I/O under the lock (the lock-across-effect discipline, KH\r\n                    // 7.12/5.16). The convergence re-assert + the reply then run OFF\r\n                    // the lock.\r\n                    struct SessSnap {\r\n                        id: u64,\r\n                        endpoint: String,\r\n                        resume_seq: u64,\r\n                        controller_by: Option<String>,\r\n                        has_controller: bool,\r\n                        viewers: u32,\r\n                    }\r\n                    let snaps: Vec<SessSnap> = rows\r\n                        .into_iter()\r\n                        .map(|(id, endpoint, log)| {\r\n                            let mut log = recover_log(&log);\r\n                            // B-2 (REQ-CONTROLLER-LIVENESS-REAP): make the controller\r\n                            // truth HONEST before reading it — reap a dead-writer\r\n                            // (severed) controller so both the reply's controller_by\r\n                            // AND the off-lock converge_perch_stamps below see the\r\n                            // cleared state and the stale info.json stamp clears.\r\n                            let _ = log.reap_dead_controller();\r\n                            SessSnap {\r\n                                id,\r\n                                endpoint,\r\n                                resume_seq: log.delivered_through(),\r\n                                controller_by: log.controller_by().map(str::to_string),\r\n                                has_controller: log.has_controller(),\r\n                                viewers: log.live_viewer_count(),\r\n                            }\r\n                        })\r\n                        .collect();\r\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\r\n                    // each live session's control/viewer stamps to broker truth,\r\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\r\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\r\n                    // pre-bind); converges within one reconcile-poll window after bind.\r\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\n                    for s in &snaps {\r\n                        converge_perch_stamps(\r\n                            &s.endpoint,\r\n                            s.controller_by.as_deref(),\r\n                            s.has_controller,\r\n                            s.viewers,\r\n                        );\r\n                    }\r\n                    let reply = SessionsReply {\r\n                        sessions: snaps\r\n                            .into_iter()\r\n                            .map(|s| SessionInfo {\r\n                                session_id: s.id,\r\n                                endpoint: s.endpoint,\r\n                                resume_seq: s.resume_seq,\r\n                                controller_by: s.controller_by,\r\n                            })\r\n                            .collect(),\r\n                    };\r\n                    let frame = Envelope::new(\r\n                        KIND_SESSIONS_REPLY,\r\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\r\n                    );\r\n                    send_frame(&send, &frame);\r\n                }\r\n                KIND_ENDPOINT_INPUT => {\r\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\r\n                    Ok(id) => my_stream_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_NET_PRESENCE_SUBSCRIBE => {\r\n                    match self.dispatch_net_presence_subscribe(env, &send) {\r\n                        Ok(()) => my_presence_sub = true,\r\n                        Err(msg) => send_error(&send, &msg),\r\n                    }\r\n                }\r\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\r\n                // An unknown kind is tolerated (forward-compat): report, don't die.\r\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\r\n            }\r\n        }\r\n\r\n        // Brain gone: detach as subscriber but leave the sessions running.\r\n        let sessions = recover(&self.sessions);\r\n        for id in &my_subs {\r\n            if let Some(h) = sessions.get(id) {\r\n                recover_log(&h.log).detach_if(&send);\r\n            }\r\n        }\r\n        drop(sessions);\r\n        // Same for net streams: the streams + their rings persist (D4b).\r\n        if let Some(host) = self.net.get() {\r\n            for id in &my_stream_subs {\r\n                host.detach_stream_sub(*id, &send);\r\n            }\r\n            // And presence: the liveness log + its ring persist (D4c).\r\n            if my_presence_sub {\r\n                host.detach_presence_sub(&send);\r\n            }\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\r","truncated":true,"truncatedBy":"lines","totalLines":3095,"totalBytes":12437,"outputLines":260,"outputBytes":12437,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\n        self.sessions\r\n            .lock()\r\n            .unwrap()\r\n            .get(&id)\r\n            .map(|h| recover_log(&h.log).next_seq)\r\n    }\r\n\r\n    /// Drive one brain connection: handshake, then dispatch its command frames\r\n    /// until it disconnects. On disconnect, detach this connection from any\r\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\r\n    /// restart must not disturb the hosted children).\r\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\r\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\r\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\r\n        struct HandlerTally<'a>(&'a AtomicUsize);\r\n        impl Drop for HandlerTally<'_> {\r\n            fn drop(&mut self) {\r\n                self.0.fetch_sub(1, Ordering::AcqRel);\r\n            }\r\n        }\r\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\r\n        let _tally = HandlerTally(&self.conn_handlers);\r\n\r\n        let (mut recv, send) = conn.split();\r\n        // Round-4: the send half is owned by the bounded/cancelable conn\r\n        // primitive from birth — every write on this physical connection is\r\n        // deadline-bounded, and a wedge poisons + retires the conn.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\r\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\r\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\r\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        send.describe(\"role=brain\");\r\n\r\n        // Version handshake: this connection must be a brain (2.3).\r\n        recv_hello(&mut recv, Role::Brain)?;\r\n\r\n        let mut my_subs: Vec<u64> = Vec::new();\r\n        let mut my_stream_subs: Vec<u64> = Vec::new();\r\n        let mut my_presence_sub = false;\r\n\r\n        loop {\r\n            let env = match read_frame(&mut recv) {\r\n                Ok(e) => e,\r\n                // Any read failure (EOF included) ends the connection.\r\n                Err(_) => break,\r\n            };\r\n            match env.kind.as_str() {\r\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_UNSUBSCRIBE => {\r\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_INPUT => {\r\n                    if let Err(msg) = self.dispatch_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_RESIZE => {\r\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_KILL => {\r\n                    if let Err(msg) = self.dispatch_kill(env) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n                KIND_ADAPTER_APPLY => {\r\n                    if let Err(msg) = self.dispatch_adapter_apply(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STATUS => self.dispatch_net_status(&send),\r\n                // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\r\n                KIND_BROKER_IMAGE => self.dispatch_broker_image(&send),\r\n                KIND_STALL_EVICTS => self.dispatch_stall_evicts(&send),\r\n                KIND_NET_DIAL => {\r\n                    if let Err(msg) = self.dispatch_net_dial(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_DIAL_LOOPBACK => {\r\n                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-PUMP-PEER-ISOLATION]\r\n                KIND_NET_DIAL_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_net_dial_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_JOIN => {\r\n                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_MEET => {\r\n                    if let Err(msg) = self.dispatch_pair_meet(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_CODE_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_pair_code_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_OPEN => {\r\n                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SEND => {\r\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAMS => {\r\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_RETIRE => {\r\n                    if let Err(msg) = self.dispatch_net_stream_retire(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_OPENER => {\r\n                    if let Err(msg) = self.dispatch_net_stream_opener(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_SESSIONS => {\r\n                    // Clone the (id, endpoint, log) out from under the sessions\r\n                    // lock, then read each log's cursor under the log lock alone —\r\n                    // never both locks at once (the dispatch_subscribe discipline).\r\n                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {\r\n                        let sessions = recover(&self.sessions);\r\n                        sessions\r\n                            .iter()\r\n                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))\r\n                            .collect()\r\n                    };\r\n                    // Snapshot each session's truth under its OWN log lock (cursor +\r\n                    // controller identity + the control/viewer stamp truth), doing NO\r\n                    // file I/O under the lock (the lock-across-effect discipline, KH\r\n                    // 7.12/5.16). The convergence re-assert + the reply then run OFF\r\n                    // the lock.\r\n                    struct SessSnap {\r\n                        id: u64,\r\n                        endpoint: String,\r\n                        resume_seq: u64,\r\n                        controller_by: Option<String>,\r\n                        has_controller: bool,\r\n                        viewers: u32,\r\n                    }\r\n                    let snaps: Vec<SessSnap> = rows\r\n                        .into_iter()\r\n                        .map(|(id, endpoint, log)| {\r\n                            let mut log = recover_log(&log);\r\n                            // B-2 (REQ-CONTROLLER-LIVENESS-REAP): make the controller\r\n                            // truth HONEST before reading it — reap a dead-writer\r\n                            // (severed) controller so both the reply's controller_by\r\n                            // AND the off-lock converge_perch_stamps below see the\r\n                            // cleared state and the stale info.json stamp clears.\r\n                            let _ = log.reap_dead_controller();\r\n                            SessSnap {\r\n                                id,\r\n                                endpoint,\r\n                                resume_seq: log.delivered_through(),\r\n                                controller_by: log.controller_by().map(str::to_string),\r\n                                has_controller: log.has_controller(),\r\n                                viewers: log.live_viewer_count(),\r\n                            }\r\n                        })\r\n                        .collect();\r\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\r\n                    // each live session's control/viewer stamps to broker truth,\r\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\r\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\r\n                    // pre-bind); converges within one reconcile-poll window after bind.\r\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\n                    for s in &snaps {\r\n                        converge_perch_stamps(\r\n                            &s.endpoint,\r\n                            s.controller_by.as_deref(),\r\n                            s.has_controller,\r\n                            s.viewers,\r\n                        );\r\n                    }\r\n                    let reply = SessionsReply {\r\n                        sessions: snaps\r\n                            .into_iter()\r\n                            .map(|s| SessionInfo {\r\n                                session_id: s.id,\r\n                                endpoint: s.endpoint,\r\n                                resume_seq: s.resume_seq,\r\n                                controller_by: s.controller_by,\r\n                            })\r\n                            .collect(),\r\n                    };\r\n                    let frame = Envelope::new(\r\n                        KIND_SESSIONS_REPLY,\r\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\r\n                    );\r\n                    send_frame(&send, &frame);\r\n                }\r\n                KIND_ENDPOINT_INPUT => {\r\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\r\n                    Ok(id) => my_stream_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_NET_PRESENCE_SUBSCRIBE => {\r\n                    match self.dispatch_net_presence_subscribe(env, &send) {\r\n                        Ok(()) => my_presence_sub = true,\r\n                        Err(msg) => send_error(&send, &msg),\r\n                    }\r\n                }\r\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\r\n                // An unknown kind is tolerated (forward-compat): report, don't die.\r\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\r\n            }\r\n        }\r\n\r\n        // Brain gone: detach as subscriber but leave the sessions running.\r\n        let sessions = recover(&self.sessions);\r\n        for id in &my_subs {\r\n            if let Some(h) = sessions.get(id) {\r\n                recover_log(&h.log).detach_if(&send);\r\n            }\r\n        }\r\n        drop(sessions);\r\n        // Same for net streams: the streams + their rings persist (D4b).\r\n        if let Some(host) = self.net.get() {\r\n            for id in &my_stream_subs {\r\n                host.detach_stream_sub(*id, &send);\r\n            }\r\n            // And presence: the liveness log + its ring persist (D4c).\r\n            if my_presence_sub {\r\n                host.detach_presence_sub(&send);\r\n            }\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\r","startLine":2968,"lineNumbers":[2968,2969,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3113,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156,3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,3170,3171,3172,3173,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3184,3185,3186,3187,3188,3189,3190,3191,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212,3213,3214,3215,3216,3217,3218,3219,3220,3221,3222,3223,3224,3225,3226,3227,3228]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6063,"totalBytes":12437,"outputLines":260,"outputBytes":12437,"shownRange":{"start":2969,"end":3228},"nextOffset":3229}}},"isError":false,"timestamp":1784247652653}}
{"type":"message","id":"17570048","parentId":"df2b5f0b","timestamp":"2026-07-17T00:20:52.654Z","message":{"role":"user","content":"Your current interruptible wait was interrupted because an IRC message arrived from your parent agent `Main`.\n\nParent IRC message:\n\nIs your stale finished Attach replay finding from this exact v0.34 five-PTY 20–30s regression investigation, and does it supersede W1 retry/ANY-Err fallback amplification? Please send exact source/log refs and explain automatic recovery timing without manual detach/fresh rc.","attribution":"agent","timestamp":1784247651237,"steering":true}}
{"type":"message","id":"56278c4b","parentId":"17570048","timestamp":"2026-07-17T00:20:58.262Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Researching W1 retry error handling**"},{"type":"toolCall","id":"call_46Dh3i7AOsAFhnMBAsJSdMCE|fc_0996e78cf50e3160016a597567f7c88199832f75da0e878249","name":"grep","arguments":{"i":"Finding W1 retry fallback analysis","pattern":"ANY.Err|ANY-Err|fallback|retry|amplification|20.?30|five.PTY|5.PTY","path":"docs;.claude/reports;crates/spt-daemon/src;crates/spt-daemon/tests","selector":null,"case":false,"gitignore":false,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":24166,"output":155,"cacheRead":125440,"cacheWrite":0,"totalTokens":149761,"reasoningTokens":59,"cost":{"input":0.12083,"output":0.0046500000000000005,"cacheRead":0.06272,"cacheWrite":0,"total":0.1882}},"stopReason":"toolUse","timestamp":1784247652662,"responseId":"resp_0996e78cf50e3160016a597565028081998c7b4f4a90ead0cc","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a5975667bc88199aa77b4b553b2fd94","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXVnwt68EnVmgcBcJPtC0g45NTX97YT3FPeiF3caCKmohAMb9xLATFjQhtVcFwP7WGwfxpxocpljBLlo8bpGVhf_MEykQTHyl7KAFNik8ZI_Bi_ncVANTf8HLC2DZPXHMWUF-qlxqAlTHdX_10DCpfhY9v-zebDbv0FpewVvHxaQkd94zuMs73-yZ_ao5ZyshLoy0pqOvBnc0L0KfRnXsksKzfsW91xIAIvh-xWlFDaxazirEE9pOPF6G9NDUJOr2A05OG3XBVUJokG0k8sVNijW_99jiqQRjqLMkKD-E844eSP26Cz-BKXD5Z4nDv11ANSkrN45n_ZFZxplJ45RE9lGXfXN5N4wG0gzt1E11PFwYAUrPqHwwE1DCohMgeAx9kQd0qEGYj4ltjr51KAvS_CpweRRFB5oTklKaJ4N9w5eFiiNrBQHJjcZtBE2N6DfMxC4mRlTkL4oLw7twcT_LaMUeGbWnGnSkIZDVzOarqZJJHEI0PINv4BrMelthlNHZU6F06lgeJyOcQtwTxGv8KC1cLWxfwwxTfo9w1oz7MZkZsJfLFn1dl3j5HcdqgaTrt_MaRfS9eWuUuvEY-DrPS43VH5a46sKTZd0yQ8j6uSFn5AVhfy5AGFM-P5Q6rZIxv8co01luBoasoSQ1iPt5IPK_B9RVWs7nCQgLdVTsNkPvjQl718TPIHjxmnzC_EEO-4XgFLpoxUtyo6jEn2-ktrgMOy0idV5gd_Zsm469yrSFdq5qfqOIobGtOWPh9YKO5SnZdn1BG2sVNdgpsjwIBHRBOtR_hw6eI70DHD8XtSNJIqGuCOlZGaFm_DYRdW8tq5K5pbDl4np5e4TlQJGUaYz3CXijm0dnjHKIJIsrd8Cagrfr0fWIlAIsvW4wslVT6mBTvdm1XCfWzwAuaALfBj1CPV0-MPa8zEJBL1bDzsl1N9y0Iid6qlKbHUXQhZusZWC5uAsCk6FepAshb4qrj4SjoXvGTuC-89hMy9CSQ4PMAi4J0DnzJusTvwuTSXi4C8h99EgHw2VKGO5UHBbLMBdBTgmwV7IIvEAgFWtSoX4qNvGi6uW376V35MrxSofRZWGUq_ty-uQVRWdjs-MF5HIxks69di6pR7gKt8t5iAcsekgTw5KXZWyO43iFDOMqtHNACt1Ao_VnFz4gm5Y5vOrnO174cNDq8moU1N_Slq0NhaPpbeR2xDtLnIx2TzSAzENcHkbCwDKg5xvf5LlbVFnydw0TieUz0saWetXfMlzNi0=","summary":[{"type":"summary_text","text":"**Researching W1 retry error handling**"}]},{"id":"fc_0996e78cf50e3160016a597567f7c88199832f75da0e878249","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding W1 retry fallback analysis\",\"pattern\":\"ANY.Err|ANY-Err|fallback|retry|amplification|20.?30|five.PTY|5.PTY\",\"path\":\"docs;.claude/reports;crates/spt-daemon/src;crates/spt-daemon/tests\",\"selector\":null,\"case\":false,\"gitignore\":false,\"skip\":null}","call_id":"call_46Dh3i7AOsAFhnMBAsJSdMCE","name":"grep"}]},"duration":5600.058100000024,"ttft":1835.035300001502,"contextSnapshot":{"promptTokens":149606,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_46Dh3i7AOsAFhnMBAsJSdMCE|fc_0996e78cf50e3160016a597567f7c88199832f75da0e878249","toolName":"grep","startedAt":"2026-07-17T00:20:58.262Z","args":{"path":"docs;.claude/reports;crates/spt-daemon/src;crates/spt-daemon/tests"},"intent":"Finding W1 retry fallback analysis"},"id":"458a667d","parentId":"56278c4b","timestamp":"2026-07-17T00:20:58.262Z"}
{"type":"message","id":"596f4c28","parentId":"458a667d","timestamp":"2026-07-17T00:20:58.332Z","message":{"role":"toolResult","toolCallId":"call_46Dh3i7AOsAFhnMBAsJSdMCE|fc_0996e78cf50e3160016a597567f7c88199832f75da0e878249","toolName":"grep","content":[{"type":"text","text":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n 141|**Decision.** Apply ADR-0004 §B's rule — *a resource is broker-owned iff a live consumer would lose continuity on a brain restart* — to every component that today shares the in-process `Arc<Broker>`:\n*142|- **Net bring-up + REQ-DAEMON-9 self-heal retry → broker** (the brain must not own net bring-up). *Audit correction:* the NetHost is **already** broker-owned (`broker.rs:175` `OnceLock`, §B M4-D4a), and net bring-up + the self-heal retry already live in the `daemon.rs` entry that *becomes* the broker entry — so this item is **near-free**, not a migration.\n 143|- **Digest hub → broker** for the live projections + subscriber sockets; **the parse that feeds it → brain** (pushes parsed digests over IPC).\n 144|- **Seed lock + liveness → broker** (Q2).\n 145|- **Every other brain→broker call becomes a versioned IPC verb** — in the two-process world there is no shared `Arc`, so each current direct call is a latent IPC verb on the same versioned contract that already carries `SPAWN`/`INPUT`/`NET_*`/`SESSIONS`.\n...\n 167|\n*168|### Q7 — Failure atomicity: bounded retry → auto-rollback to last-known-good\n 169|\n 170|**Decision.** If the new brain fails to reach a **ready** signal (re-attached all sessions + resumed loops) within a bounded number of boots / a healthy-run window, the broker **rolls back to the last-known-good binary** (the `spt.exe.old-N` backup), **quarantines** the bad version (no auto re-apply/re-fetch), and surfaces a loud consent-style notification. The broker holds **last-known-good + candidate** paths and chooses which to spawn (no file manipulation at failure time). Endpoints survive throughou...\n 171|\n...\n 178|\n*179|**Alternative rejected.** *Bounded retry → halt and wait for manual intervention* — safer-feeling but leaves logic dead until a human shows up; inconsistent with the self-healing posture.\n 180|\n 181|### Q8 — Cross-platform uniformity (confirmation, not a fork)\n 182|\n...\n 228|- **Spikes** — `docs/spikes/SPIKE-01-broker-handoff.md`, `-03-quic-survival`, `-04-forkpty-parity`, `-05-restart-stress`, `-06-idempotent-boundary` (proofs that the two-process handoff works on both OSes).\n*229|- **Key source** — `daemon.rs` (`run`, net consumers, self-heal, digest hub, shellwake, seed-control), `broker.rs` (`OutputLog`, `bind`, `EffectJournal`), `brain.rs` (`BrainState`, `cold_start`/`handoff`/`snapshot`), `applyhost.rs` (`apply_staged`, `connect_retry`, record-before-handoff), `update.rs` (classes, `classify`, `apply_brain_only`), `peerloop.rs` (cadences, supervise-backoff), `lifecycle.rs` (pulse loop), `registryhost.rs`.\n 230|\n 231|---\n 232|\n## DEBUG-CONVERGE-PLAN.md\n 137|   `DEBUG-ROLLOUT.md` with `xtask debug-converge`, keeping the manual path as\n*138|   the fallback.\n 139|\n 140|## Traceability\n 141|\n## DEBUG-ROLLOUT.md\n 98|otherwise, `2` on usage). Spec + state model: `docs/DEBUG-CONVERGE-PLAN.md`.\n*99|The manual walk below remains the fallback.\n 100|\n 101|Useful outcomes:\n 102|\n## DEFERRED.md\n 29|| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache...\n*30|| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |\n 31|| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`w...\n 32|| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version ...\n 33|| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred \"option 1\")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but no...\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n 127|   (Field-confirmed adapter shape: claude-spt declares\n*128|   `[env.CLAUDE_CONFIG_DIR] direction=\"read\"` with `~/.claude` fallback —\n 129|   adapter reads, never sets.)\n 130|2. **Install-dir-first resolution (REQ-INSTALL-11):** the program path resolves\n 131|   via `resolve_program_in_dir` like every other adapter-manifest program.\n...\n 219|   Adapter shape confirmed: `[env.CLAUDE_CONFIG_DIR] direction=\"read\"`,\n*220|   fallback `~/.claude`, adapter reads and never sets.\n## FAULT-MATRIX.md\n 27|| 8 | **Net disabled / endpoint bind fails at boot** | No WAN; net frames answer `enabled:false` / typed error | PTY hosting (daemon degrades to net-less broker) | Fix config, restart daemon | `broker.rs::dispatch_net_status` (None arm); `Daemon::run` degrade path |\n*28|| 9 | **Broker process restart (journal survives, conns don't)** | Live conns/streams lost | Effect journal (durable) | Deduped op whose resource is gone → typed \"retry with a fresh op_id\" error; brain re-dials; an `spt rc` operator viewport RECONNECTS on its own — centered banner, full establish re-drive inside a bounded window, then a plain-language give-up (REQ-RC-RECONNECT) <!-- [doc->REQ-RC-RECONNECT] --> | `broker.rs::dispatch_net_dial` / `dispatch_net_stream_open` restart arms; `rc.rs` reconne...\n 29|| 10 | **WAN msg feed replayed** (sender redrive, or receiver brain restart resubscribing from a stale cursor) | None — replayed records dedup | Spool state (each op exactly one row); already-delivered ops | Durable `wan_seen` op-id claims absorb the replay; spool path claims atomically with the row | `tests/wanmsg.rs::receiver_restart_replays_feed_without_double_delivery`; `spool.rs` wan unit tests |\n 30|| 11 | **WAN msg record forges its origin** (payload `origin_node`/`node` field) | None — forgery inert (unknown field) | Access-gate subject (handshake-proven `remote_id_hex`) | None needed (records carry no origin field by design — KH 7.5) | `tests/wanmsg.rs::wan_message_lands_exactly_once_under_transport_origin`; `wanmsg.rs::forged_origin_field_is_inert` |\n 31|| 12 | **Target brain killed mid-remote-attach** (operator typing into the dead window) | Pumping pauses until handoff | Broker-held QUIC stream, session + child, output log, effect journal; the operator's viewport | Successor re-serves the same stream from seq 0 (worst case): replayed input dedups at the PTY-write journal; re-transmitted output dedups at the operator's render cursor — every byte exactly once | `tests/attach.rs::attach_survives_target_brain_restart_exactly_once` |\n...\n 33|| 14 | **Stream lost mid-context-bundle-sync** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's context store (an incomplete bundle is never fetched — length-gated, scratch file deleted); the responder's store untouched | No resume protocol by design: re-pull fresh — the apply is ancestry-idempotent (an already-joined tip short-circuits; fetched tips quarantine under `refs/spt-sync/` so a partial fetch never clobbers `refs/heads/`) | `tests/sync.rs...\n*34|| 15 | **Partition: concurrent context writes on two nodes** | The file's auto-propagation pauses (conflict surfaced, not propagated) | BOTH versions — local file untouched on each node + the other's version as a durable tracked artifact (hazard 6.6); every other file keeps syncing | Elected reconciler (Active instance's node; lowest-id fallback) runs one bounded Psyche turn → merged write `join(vA,vB)+bump` dominates both parents → propagates as a plain accept, clears artifacts subnet-wide | `test...\n*35|| 16 | **Reconcile turn fails** (harness absent / timeout / garbage or empty output / 6.5-suppressed write) | That file stays in conflict-holding state | Both versions (artifacts + local file) — exactly where they were; the store, the sync loop | Retry at the next sync/activation; a fresher direct write re-merges against the new local version | `spt-live reconcile.rs::failed_turn_preserves_everything` / `suppressed_write_preserves_artifacts`; `turn.rs::empty_stdout_is_an_error` |\n*36|| 17 | **Registry ambiguity during reconciler election** (partition: two nodes both see Active / no Active anywhere) | Two textually-different merges of the same pair may both mint | Neither version is ever lost — merged writes carry joined vectors, so the two merges classify **concurrent** and re-surface as a new conflict pair (detected, not silently last-wins) | Deterministic tiebreak bounds the storm (lowest node id among Actives / non-Offline holders); the re-surfaced pair reconciles on the next pa...\n 37|| 18 | **Compromised relay node serves a tampered update artifact** (bytes corrupted at rest after the relay's own verification) | That one pull ends `Rejected(ArtifactMismatch)` at the puller | The puller's staged release + running binary (nothing staged, nothing applied — no code path from wire bytes to the cache except through `plan_verified`); the rest of the subnet pulls from honest peers | Pull from another peer; the per-node gate (REQ-UPD-2) runs at **every** hop, so one poisoned node never re-p...\n 38|| 19 | **Non-conforming peer offers a rollback/expired/off-channel release** | None — rejected pre-fetch, zero artifact bytes move | The puller's version (monotonic floor holds); the wire (no transfer ever starts) | None needed — the offer-then-fetch shape gates metadata before bytes (REQ-HAZARD-UPDATE-ROLLBACK) | `tests/propagate.rs::rollback_offer_is_rejected_before_any_fetch` |\n 39|| 20 | **Untrusted node queries for the staged update** | None — refused by not offering (fail-closed, the up-to-date shape: learns nothing, not even whether a release exists) | The staged release; the serve loop (one refusal, no state) | None needed; pair the node to entitle it | `tests/propagate.rs::untrusted_origin_gets_no_offer` |\n...\n 43|| 23 | **Psyche replies with no inbound sender to answer** (alarm-fired turn emits a reply) | That one intent drops with a typed outcome (fail-closed — never broadcast, never guessed) | Every perch (zero deliveries); the notify leg (independent) | The Psyche's next answered turn replies normally | `psyrelay.rs::reply_without_target_is_dropped` |\n*44|| 24 | **Live-Psyche driver loses stdout** (the 7.3 null-stdout shape: detached/`Stdio::null()` regression or a turn that produced nothing) | That turn fails loudly (`TurnError::EmptyOutput`); nothing relays | The relay boundary (no silent success — indistinguishable-from-bug shapes are errors); the spool | Fix the driver / retry the turn | `psyrelay.rs::null_stdout_driver_fails_the_guard`; `spt-live turn.rs::empty_stdout_is_an_error` |\n 45|| 25 | **Runaway Psyche dumps an oversize body** (> 64 KiB in one intent) | That intent drops with a typed outcome | Delivery substrate (never asked to carry it); other intents in the same turn relay normally | None needed (legitimate long bodies chunk at delivery — T3 `EVENT-PART`) | `psyrelay.rs::empty_and_oversize_bodies_drop_typed` |\n 46|| 26 | **Notif feed replayed whole** (re-presented rows after loss/restart — the push-feed's normal recovery) | None — the semilattice join no-ops (`Unchanged` per row, nothing written) | The spool (no duplicates, no state regression) | None needed: replay IS the recovery protocol (full-row re-presentation, no delta bookkeeping) | `tests/notifsync.rs::notif_spools_converge_over_the_wire_and_dismiss_replicates` (replay leg); `notifsync.rs::feed_converges_two_stores_and_dismiss_replicates` |\n 47|| 27 | **Concurrent dismiss + surface on two nodes** (the same notif dismissed on A while B marks it seen/surfaced) | None — the writes commute through the join (OR/union/max); both survive the bidirectional exchange | Both stores converge to identical rows; the dismiss latch (a stale undismissed copy can never un-dismiss) | None needed (no conflict exists to surface — every field is monotone) | `notifsync.rs::concurrent_dismiss_and_surface_commute_across_the_feed`; `spt-store notif.rs::merge_is_idem...\n## FLAKE-LEDGER.md\n 16|| 7 | `spt-daemon::attach attach_survives_target_brain_restart_exactly_once` | 1× kitsubito (2026-06-16 run 27595180782 attempt 1, M11-W3) | **TIMEOUT, not an assertion fail** — SLOW>60>120>180>TERMINATING, nextest TIMEOUT at the 240s cap (`attach.rs:600`). A heavy multi-process brain-restart-exactly-once test on the slow shared kitsubito box under concurrent load (n1-gate on the same runner + a parallel `ci.yml` docs-publish run firing simultaneously). W3-INDEPENDENT: Windows green end-to-end; ALL tu...\n*17|| 8 | 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). H...\n 18|| 9 | `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 b...\n*19|| 10 | `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: gre...\n 20|| 11 | `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...\n 21|| 12 | `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-...\n 22|| 13 | `spt::bin cli::tests::probe_all_cap_batches_into_windows` | 1× hfenduleam local gate (2026-07-06, stack matrix @0f08fa9 leg-3 rerun) | fast unit (0.52s natural) FAILED at 1.7s under the 1614-test full-parallel pool — probe-batching window assert with a timing component starved under load; subsystem disjoint from the triggering stack (F-2 reporting / E-2 grid title / picker rider). Isolated ×5: green, 0.52–0.55s. Same night as #12 — the full-parallel local matrix under a busy box is the com...\n## GATEWAY-LIVENESS-DISPATCH.md\n 6|At the SAME instant, for the SAME pid-alive locally-hosted gateway (mobile-gw, status field ABSENT in info.json, pid alive):\n*7|- **human `spt endpoint list`** → **ONLINE** (renders local perches via `roster::enumerate`, roster.rs:72 = `is_perch_alive` WITH the pid-fallback: status-absent → `is_process_alive(pid)` → alive).\n 8|- **`spt endpoint list --json`** → **Suspended** (renders the endpoint via `resource_projection` over the registry snapshot, cli.rs:2855 → `EndpointRowJson.status` = the registry row status).\n 9|- flynn confirmed mobile-gw IS advertised in all 3 subnet registry files (NOT a re-advertise gap — his earlier \"empty by_id\" was a walker error, retracted).\n 10|\n...\n 13|## Key source facts (already traced)\n*14|- `advertised_status` (registryhost.rs:822-833) DOES feed `effective_rest_state` the pid-fallback `is_perch_alive` (line 825). So the DAEMON's advertised value should be Active for a pid-alive gateway — the divergence is NOT the advertise computation.\n*15|- So the `--json` Suspended comes from the projection/rendering path dropping the pid-fallback the human local-roster applies. **Candidate sites to pin (STEP 1):**\n 16|  1. `resource_projection` (spt-net registry.rs:566) — does it re-derive/filter liveness (e.g. `is_registry_entry_alive`, status-based: liveness.rs:152, which for a status-ABSENT row = not ONLINE||UNBOUND → stale/offline) and DOWNGRADE the row to Suspended/offline, ignoring the advertised Active? (its test `resource_projection_filters_hidden_and_offline` @registry.rs:1821 says it DOES filter on liveness.)\n 17|  2. OR the registry ROW's advertised status is genuinely Active but the `--json` render maps it wrong.\n 18|  3. OR the human path applies a `reconcile_self_owned` local-truth override for locally-owned endpoints that the `--json` path skips (roster.rs:52 mentions reconcile_self_owned overriding stale subnet gossip).\n 19|- **DEFECT A (separate, CONFIRMED, stands):** `establish_perch`'s build (spt/src/api/startup.rs:214-302) carries `cwd/controllable/adapter/read_env` forward from the prior record but NOT `rest_state` → a re-bind writes a fresh InfoJson that WIPES the wake intent (flynn's tick11 rest_state:active vanish).\n*20|- **DEFECT B (RECLASSIFIED to LATENT — do NOT make it the primary fix):** the `status=online` write in `cmd_listen` (startup.rs:486-491) is gated on `live_capable` (manifest `[session.psyche_init]`); a gateway has no psyche_init so it never writes status=online. BUT the pid-fallback is DESIGNED to cover an absent status and DOES for the roster + advertised_status. The operator's ready_agent counterexample proved this: ready_agents work because claude-spt declares psyche_init (live_capable-by-manifest),...\n 21|\n 22|## The fix (after STEP-1 pin)\n*23|**Reader parity:** `endpoint list --json` must report the SAME status for a locally-hosted endpoint as the human `endpoint list` does — i.e., honor the pid-fallback / local-owned truth (`roster::enumerate` / `reconcile_self_owned`) so a machine consumer never sees a different liveness than a human. A pid-alive locally-hosted gateway must read the same (ONLINE/Active) on both surfaces.\n*24|- Fix at the pinned site (most likely: apply the local-roster / self-owned reconcile to the `--json` local-endpoint rows, OR make `resource_projection`'s liveness for a locally-owned row honor the pid-fallback instead of a status-only check).\n*25|- Do NOT reach for DEFECT B (forcing status=online for gateways) as the fix — it risks the seed-#5 orphan-listener falsely-ONLINE amplification; the reader-parity is the correct, minimal fix. (If STEP-1 shows the cleanest fix genuinely is establishing status for a relay-holding listener, flag it back to me before taking it — that's a design call, not a silent choice.)\n 26|- **DEFECT A:** preserve `prior.rest_state` on re-bind in `establish_perch`'s build (same carry-forward discipline as cwd/controllable).\n 27|\n 28|## REQs (mint at build-start, activate-don't-pre-fail)\n## GATEWAY-RCA-STEP1.md\n 20|row whose id matches a LOCAL (self-hosted) perch row, it **overrides the subnet\n*21|row's status with the local roster ground truth** (the `is_perch_alive` pid-fallback,\n 22|`roster::enumerate` → `liveness.rs:136` → status-absent + pid-alive ⇒ ONLINE).\n 23|\n 24|**So for a locally-hosted, pid-alive, status-absent gateway whose snapshot row is a\n...\n 42|Note: `advertised_status` (registryhost.rs:822) DOES compute Active via the\n*43|pid-fallback, so the daemon's *live* advertise is Active — consistent with your\n 44|trace. The divergence is that the `--json` reader shows the **persisted snapshot**\n 45|row (which can carry a stale/crash-time Suspended) without the local-owned reconcile\n 46|that the human/picker reader applies. Reader parity is the fix, not the advertise.\n...\n 52|(`roster::enumerate` / is_local), override its rendered status with the local\n*53|`is_perch_alive` pid-fallback truth (mirroring `reconcile_self_owned`). Result: a\n 54|locally-hosted endpoint reports the SAME status on human `endpoint list` and\n 55|`--json`. Minimal + does NOT touch the advertise or force status=online (avoids the\n*56|seed-#5 orphan-listener false-ONLINE amplification you flagged).\n 57|\n 58|`REQ-HAZARD-BIND-REST-STATE-CARRY` (DEFECT A, confirmed, independent): preserve\n 59|`prior.rest_state` on re-bind in `establish_perch`'s build (spt/src/api/startup.rs\n## KNOWN-HAZARDS.md\n 18|- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\n*19|- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).\n 20|- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\n 21|- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\n 22|\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...\n 116|- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n*117|- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n*118|- **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 119|- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n 120|\n 121|### 4.4 Deferred rows survive poll drain\n...\n 183|- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\n*184|- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\n 185|- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\n 186|- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\n 187|\n...\n 252|- **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*253|- **Invariant:** spt-term resolves the program ITSELF before handing it to `CommandBuilder`, bypassing portable-pty's `which`. A bare name is searched over `PATH` × `PATHEXT` (whose default order already prefers `.EXE`/`.COM` over `.BAT`/`.CMD`), then an extensionless fallback. A non-PE target is wrapped in its interpreter: `.cmd`/`.bat` → `cmd.exe /d /c <path>`, `.ps1` → `powershell -NoProfile -File <path>` (the wrap args precede the caller's args); a real executable spawns directly; an unresolvabl...\n 254|- **spt-core mapping:** `spt_term::winprog::{resolve_for_pty, resolve_in}` (the pure PATHEXT-precedence kernel + the Windows env wiring), wired into `spt_term::pty::PtySession::spawn_program_in`. Unit: `resolve_in` precedence (`.cmd`-over-shim, `.exe`-direct, explicit-extension, path-order, passthrough) [`winprog.rs`].\n 255|- **Source:** field diagnosis 2026-06-16 (operator dogfood, `claude-spt:ccs` bringup) — doyle.\n 256|\n...\n 259|- **Failure:** `atomic_write_bytes` was `fs::write(tmp)` + `rename(tmp, path)` with no `fsync`. The rename's directory **metadata** is journaled durable, but the tmp file's **data blocks** are still in the page cache. A hard reset (power loss, forced reboot) between the two flushes lands the rename but loses the data → the file reappears at its **full length filled with NUL**. Field incident: after a machine restart `owlery/hall-a/info.json` was 360 bytes of all-NUL (the nested psyche record 125 bytes ...\n*260|- **Invariant:** for records whose loss is **unrecoverable**, flush data to stable storage **before** the rename publishes the name — `File::create(tmp)` → `write_all` → `sync_all()` → `rename_with_retry`. Then a crash yields the complete old file OR the complete new file, never a NUL husk. (A file of correct length that is ALL-NUL is the diagnostic tell of this class — a non-fsync'd write caught by a hard reset.) **Durability is SCOPED, not blanket.** `fsync`ing *every* atomic write serializes...\n 261|- **spt-core mapping:** `spt_store::atomic::atomic_write_bytes_durable` / `atomic_write_string_durable` are the opt-in durable siblings; the default `atomic_write_bytes` / `atomic_write_string` stay non-durable. Durable callers are exactly `spt_store::info::write_info` (the perch record — the hall-a surface), `spt_store::nodeid` (the node seed — corrupt is node-bricking, never regenerated), and `spt_daemon::machineid` (minted-once machine id — a NUL husk would silently re-mint a different id). Read...\n 262|- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; blanket-fsync perf regression pinned + scoped by the gate rig (doyle), same date.\n 263|\n...\n 266|- **Failure:** three readers each collapsed a **corrupt** (present-but-unparseable) `info.json` into their fail-open ABSENT default, so a NUL-wiped perch (5.13) read as permanently live: `is_perch_alive` returned `true` (unreadable ⇒ interim-alive), `advertised_status` then saw alive + no resting record ⇒ `Active`, and the daemon self-gossiped that Active row every round (epoch 173k+). Result: `hall-a`, dead since a machine restart, showed ONLINE in `spt whoami` and on every remote picker.\n*267|- **Invariant:** CORRUPT ≠ ABSENT. A record that EXISTS but stays unparseable across the retry budget is a **destroyed** record — never a live endpoint. `is_perch_alive` reads corrupt ⇒ **not alive** (ABSENT keeps interim-alive parity — the ONE case that stays true); `advertised_status` then lands corrupt in the cold arm ⇒ **Suspended**, never Active/Dormant. Readers that decide liveness/status must branch on the tri-state (present / absent / corrupt), not an `Option` that fuses the last two.\n 268|- **spt-core mapping:** `spt_store::liveness::{read_raw_state, is_perch_alive}` (the tri-state kernel) and `spt_daemon::registryhost::advertised_status` (cascades to Suspended off the alive fix). `list_self_perch_ids` deliberately still lists a corrupt dir by existence — that is the *visibility* the local-roster fix (counter-39 #3) relies on, not a liveness claim. Candidate sibling seam `is_registry_entry_alive` (corrupt hosted row falls to a daemon-pid probe) is parked — it does not produce the ONLI...\n 269|- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; sequel to the v0.17.0 W4 presence-truth fix (cold ⇒ Suspended) which a corrupt perch bypassed via false-alive.\n 270|\n...\n 272|### 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file)  `[REQ-HAZARD-ATOMIC-TMP-COLLISION]`\n*273|- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `Bind...\n*274|- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind t...\n 275|- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\n 276|- **Source:** RCA 2026-07-01 (doyle gate rig) — write-trace side-channel + `cmd_bind` step probes pinned `PROBE cmd_bind establish ERR: Io(os error 2)`; explains the counter-39 gate-1 blanket failure, the dummy_harness ONLINE-race DIAGs, and the nondeterminism. Lesson: a widened timing window doesn't create a race, it reveals one.\n 277|\n...\n 403|- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop...\n*404|- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a heal...\n 405|- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the ...\n 406|- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.\n 407|\n...\n 431|- **Failure (Windows):** `spt endpoint run` autostarts the daemon (`ensure_running` → `detached_no_inherit`), which INHERITS the launching terminal's Windows Job Object. Windows Terminal / VS Code place the shell AND every descendant in a Job with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; closing the tab drops the job's last handle → the OS reaps the daemon + its broker-spawned ConPTY harness subtree. The `rc` pump detaching should end ONLY the viewport; the harness must keep running + stay re-attachable...\n*432|- **Invariant:** both daemon spawn paths request `CREATE_BREAKAWAY_FROM_JOB` (0x0100_0000), best-effort with an in-job FALLBACK on `ERROR_ACCESS_DENIED`(5) / `ERROR_INVALID_PARAMETER`(87) so a breakaway-denying job NEVER regresses the spawn. **A real job CAN deny breakaway** (proven: the cargo/CI runner's own ancestor job ACCESS_DENIES it), so efficacy against the real terminal is UNKNOWN until measured. **int stage = OPERATOR MANUAL ACCEPTANCE, not CI** (doyle ruling 2026-06-18): the runner sits inside ...\n*433|- **spt-core mapping:** `daemon.rs::detached_no_inherit` + `deelevate.rs::create_with_token` (the breakaway flag + fallback). Units: `detached_no_inherit_falls_back_under_a_breakaway_denying_job` (no-regression — the shared runner exercises the fallback directly), `breakaway_spawn_escapes_a_kill_on_close_job` (the OS escape mechanism — self-SKIPS where the ancestor job forbids breakaway). Unix: the daemon's `setsid` session-detach already keeps a closing terminal's SIGHUP off its children (guard test...\n 434|- **Source:** v0.12.0 real-harness defect (operator) → v0.12.1 L1 @5ae68f8; doyle design+int ruling 2026-06-18.\n 435|\n 436|### 7.11 A dead PTY child + a dropped operator pump must NOT wedge the broker for other clients  `[REQ-HAZARD-ATTACH-WEDGE]`\n...\n 505|- **Failure (F-019 post-mortem):** the v0.11.0 path raw-injected `payload+\\r` into the spt-hosted PTY whenever no working translation binary handled an inbound message — none declared, spawn-failed, FAULTED (commit-deadline miss / binary death), or its inject-worker channel dropped — AND acked `delivered=true`. But a bare `payload+\\r` does NOT submit on a modern TUI (Claude Code): the message was TYPED into the harness but never sent, a silent pseudo-delivery reported as success. That silent degrade-...\n*506|- **Invariant:** spt-hosted idle delivery is translation-binary-ONLY (ADR-0022 amendment). `dispatch_endpoint_input` with no working binary replies `endpoint_injected_envelope(ep, delivered=false)` and writes NOTHING to the PTY; the caller (`try_broker_inject` → `cmd_send`) reads `delivered=false` and falls through to `deliver::send` = SPOOL (poll-fed, never lost), reporting an honest `QUEUED`, never a confident-but-false `SENT`. The failure is LOUD (`ENDPOINT_INJECT:<ep>: no working translation binary...\n*507|- **Boundary — what this does NOT cover (the STATE-vs-transient precision):** the guarantee is the steady FAILED STATE — once a binary IS faulted/absent/worker-gone, subsequent deliveries spool (`faulted` is MONOTONIC: set once, never respawns, so the state is reached deterministically). It does NOT cover the FAULT-TRANSIENT: a delivery that lands in the worker's commit window — BEFORE `event_rx` is dropped / `faulted` is set — can be optimistically enqueue-acked (`delivered=true` the instant `ev...\n*508|- **spt-core mapping:** impl `broker.rs` `dispatch_endpoint_input` (no-working-binary → `delivered=false` + loud log, no `input.enqueue`; the `input` writer is no longer resolved from the sessions table) + `build_translation`/`fault_translation` (`None`/fault now MEAN spool); unit `msg.rs` `endpoint_injected_envelope_carries_delivered_both_ways` (the `delivered=false` spool signal round-trips); int (real broker+PTY) `tests/broker.rs` `endpoint_keyed_inject_without_binary_spools_not_pty` + `tests/inject...\n 509|- **Source:** v0.14.3 (ADR-0022 amendment @1eaeef1, operator-ruled; doyle-scoped, the F-019 follow-up). CHANGE-5 (`cli.rs` honest report) verified a NO-OP — the spool fall-through already reported `QUEUED`, never `SENT`.\n 510|<!-- [doc->REQ-HAZARD-IDLE-SILENT-NONDELIVERY] -->\n 511|\n...\n 515|- **Failure (F-023, RCA @ `f023-f024-wan-idle-starvation`):** a message that has already landed in a node's spool — WAN-arrived, or locally spooled-while-active — stranded INDEFINITELY on an spt-hosted, relay-LESS endpoint whenever the adapter's hooks went quiet (an idle session with no user turns). An spt-hosted perch has no `api listen` relay to wake it, and NOTHING in the daemon re-offered a spooled message once the arrival moment passed — delivery silently depended on the adapter happening to c...\n*516|- **Invariant:** the daemon DRIVES delivery of an already-spooled message on the events IT owns — never on hook cadence — for a relay-less spt-hosted endpoint. TWO daemon triggers feed ONE shared spt-hosted inject leg (`spt_daemon::inject::try_spt_hosted_inject`, translation-binary-ONLY): (1) **WAN ingress** — `receive_wan` injects via the live binary BEFORE the spool fallback (`[REQ-WAN-SPT-HOSTED-DELIVERY]`, leg 1); (2) **the ACTIVE→IDLE edge** — `drain_idle_window` (fired by `api state idle`...\n 517|- **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 518|- **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).\n 519|<!-- [doc->REQ-HAZARD-DELIVERY-STARVATION] -->\n...\n 523|### 7.24 A delegated live adapter apply must NEVER report success without swapping, and the live-update seam must use ONE parent-aware adapter matcher  `[REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP]`\n*524|- **Failure (F015B / BUILD-F015B-APPLYMATCH):** a live adapter update to a PROFILE-COMPOSITE endpoint (`--adapter cc:ccs`) silently no-oped — `LIVE` then `DONE` printed, but the version-of-truth stayed OLD. TWO defects on one seam. **(D1, matcher skew):** the broker's `dispatch_adapter_apply` filtered sessions by EXACT `s.adapter == req.adapter`, but a `--adapter <adapter>:<profile>` endpoint stores the COMPOSITE `cc:ccs` (set at `cmd_endpoint_run` → `launch_harness_brokered_in` → `SpawnReq.adapter...\n 525|- **Invariant:** (1) ONE shared parent-aware matcher `spt_runtime::profile::adapter_parent_matches(session_adapter, parent)` (`split_option(session_adapter).0 == parent`) is used at EVERY live-update comparator — the CLI live-gate, the broker apply-filter, AND `select_endpoints_running_adapter`; NO exact `==` against a record name at any live-update seam (a profile-composite endpoint always resolves to its parent). (2) Once the apply is delegated the daemon owns the WHOLE apply: the CRC swap runs UNCON...\n 526|- **spt-core mapping:** impl `spt-runtime/src/profile.rs` `adapter_parent_matches` wired at `cli.rs` `adapter_has_live_endpoint` + `broker.rs` `dispatch_adapter_apply` session filter + `broker.rs` `select_endpoints_running_adapter`; `broker.rs` `dispatch_adapter_apply` swaps unconditionally (empty-affected early-return removed). unit `profile.rs` `adapter_parent_matches_on_parent_not_composite` (matcher truth table) + `broker.rs` `select_endpoints_running_adapter_filters_dedups_sorts` (extended with a `:...\n 527|- **Source:** F015B (operator escalation, doyle RCA code-verified end-to-end 2026-07-02; todlando build). ADR-0025 amendment records both invariants.\n...\n 534|- **Invariant:** every adapter-manifest program path resolves against the adapter install dir BEFORE PATH (REQ-INSTALL-11, `resolve_program_in_dir`) — at the harness session spawn (`harnesshost::launch_harness_brokered_in`: the idle-translation binary AND the session program), the W3d live-update RESPAWN (`broker::read_translation_path`), and the notif command (`with_install_dir`, both the daemon `notif.rs` and the api `reporting.rs` render paths). **Diagnosis rule:** before any deep behavioral analysi...\n*535|- **Source:** F-019 (v0.14.2); root confirmed on real claude-spt (perri + doyle). The raw-inject FALLBACK itself (degenerate no-binary floor) is unchanged by F-019 — its removal (message stays SPOOLED + poll-fed, never raw PTY inject) rides a separate ADR-0022 amendment.\n 536|<!-- [doc->REQ-INSTALL-11] -->\n 537|\n 538|---\n...\n 541|- **Failure (F-024C/F-024D, ENLYZEAM field + clean-room repro 2026-07-02):** `authenticate()` (auth.rs) gates `api poll`/`state`/`boundary` on `proof.session_id == info.json.session_id`. If ONE boundary rotation is LOST — the departing session dies (crash / tab-close) or its `/clear`-era `boundary` call never lands — the perch stays PINNED to the dead sid, and every id-scoped hook call thereafter AUTH_REFUSES *including `boundary` itself* (the very call that would re-pin presents the NEW sid). A perm...\n*542|- **Invariant:** `authenticate()` gains a DEAD-OWNER fallback — when the caller's sid MISMATCHES the pin **AND** the perch's recorded owner pid is DEAD (`proc::is_process_alive == false`), ACCEPT the caller's sid and RE-PIN (rotate `info.json.session_id` via the locked `mutate_info` RMW + a LOUD `SESSION_REPIN` stderr line). Same trust model as `establish_perch`'s conflict gate, which already allows a rebind exactly when `owner_alive == false` (an orphaned perch accepts a new LOCAL owner). This is **AD...\n 543|- **spt-core mapping:** impl `spt/src/api/auth.rs` `authenticate` dead-owner-mismatch branch (`read_pid` → `proc::is_process_alive` → `mutate_info` rotate + `SESSION_REPIN` log); unit `auth.rs` `pinned_to_dead_sid_mismatched_poll_repins` (AUTH_REFUSED → drains+re-pins, RED pre-fix) + `live_owner_mismatch_still_refuses` (the `/clear` live-rotation squat guard, unwidened) + `token_auth_path_unchanged` (correct token wins first, no rotation).\n 544|- **Source:** F-024C/F-024D (perri field RCA on ENLYZEAM + doyle code-confirm 2026-07-02; the wedge latches on `/clear` even in a CLEAN env — the env-corruption domino was sufficient but not necessary). COVERAGE: dead-owner re-pin rescues CRASHED/DEAD owners ONLY; live-pid rotation is correctly refused without departed-session proof. Cross-refs ADR-0032 (the 3-layer rotation-credential model) + `[REQ-BOUNDARY-ROTATION-CREDENTIAL]` (layer 1, the adapter/harness-contract half).\n 545|<!-- [doc->REQ-HAZARD-SESSION-PIN-WEDGE] -->\n## MANIFEST.md\n 15|- **Config knobs** (pulse period, echo-commune window, route-guard window, daily-refresh) — spt-core **global settings**, with optional **per-endpoint override** for endpoints with unique needs. Not per-adapter.\n*16|- **Cross-adapter fallback** (psyche/echo invocation falls back to another adapter on rate-limit) — a **node-wide setting**, not a manifest field, because it references *multiple* adapters (under spt-core, `ccs` is its own adapter, not a binary-swap within one). Adapter-agnostic IDs make this safe — an endpoint's Psyche can run under a fallback adapter temporarily.\n 17|- **Plugin-cache / version-trampoline layout** — none of SPT's concern. A CC-plugin adapter updates via `claude plugin update` (delegated command).\n 18|\n 19|<!-- [doc->REQ-MANIFEST-1] The sections below are the authoritative schema; the\n...\n 93|reads = []\n*94|can_inject = false                 # no hookSpecificOutput schema → sentinel/relay fallback\n 95|```\n 96|\n 97|### `[session.<role>]` — outbound session role templates\n...\n 115|[session.psyche_resume]\n*116|command = \"claude -p --resume {session_id} --agent owl-psyche --model sonnet --fallback-model opus --output-format json\"\n 117|env_remove = [\"OWL_SESSION_ID\"]\n 118|recursion_guard_env = \"SPT_ECHO_COMMUNE\"   # set on summarizer children so their own hooks bail\n 119|keys = [\"session_id\", \"parent_session_id\", \"psyche_context_file\", \"subnet\"]   # the psyche keys spt-core fills\n...\n 178||---|---|---|\n*179|| `{node}` | this node's **advertised label** — the label store value (OS-hostname default, re-checked at daemon startup), OS-hostname fallback when no store label is known | **SINGLE-TOKEN fill only** — tokenize-then-fill cannot produce a space-carrying argv element, so a composite display name like `<id> @ <node>` stays adapter-shim territory (`{node}` complements the shim for tokenizable args, e.g. `--remote-control {id}--{node}`, it does not replace it). **Caveat:** the daemon-side lifecycle reso...\n 180|\n 181|<!-- [doc->REQ-MANIFEST-NODE-KEY] -->\n 182|```toml\n...\n 220|direction = \"read\"\n*221|value = \"~/.claude\"                # FALLBACK when the var is unset (the base harness)\n 222|```\n 223|\n 224|- **Allowlist, not the whole env** — only the vars you declare `direction = \"read\"`\n...\n 226|- **Resolution order at digest time**: the value **captured** from the ambient env\n*227|  at bind (the relocating profile) → the directive's **`value`** fallback (the base\n 228|  harness) → **omitted**, so the `{VAR}` fails to fill and the digest diagnoses\n 229|  \"not set\" rather than resolving a wrong path (the same skip-diagnose posture as a\n 230|  missing `[digest]` source).\n 231|- **`~` expansion** — a leading `~/` (or bare `~`) in the captured value or the\n*232|  `value` fallback expands to the OS home (`USERPROFILE` on Windows, `HOME` on\n 233|  Unix). Any other `~` passes through verbatim.\n 234|- spt-core names **no** harness variable and bakes **no** default root — the\n 235|  adapter declares which var to capture and how its transcript path consumes it\n...\n 298|  adapter-declared **defaults** any consumer may override; spt-core ships\n*299|  fallbacks (`3` / `25` / collapse-on).\n 300|\n 301|Prove your extractor before shipping: `spt adapter digest-proof <adapter>\n 302|--sample <real-log>` runs it and prints the parsed records, the rendered digest,\n...\n 364|- **`{\"commit\":true}` is the mandatory sequence TERMINATOR, required for EVERY `{\"type\":\"event\"}`.** While a sequence is in flight spt-core buffers the live `spt rc` controller's keystrokes (the *inject floor*) and applies the emitted commands atomically; the trailing `{\"commit\":true}` releases the floor and flushes the buffered controller input *after* the sequence. The submit keystroke is NOT the terminator (a choreography may keep typing after `{\"key\":\"enter\"}`). **An empty response is a protocol viol...\n*365|- **Commit deadline + miss tolerance (F029 C-1, ADR-0022 amendment).** If no `{\"commit\":true}` arrives within the **commit deadline (5 s, `INJECT_COMMIT_DEADLINE`)**, spt-core flushes the buffered operator input (never stranded) and re-spools that message once — but a SINGLE miss does NOT fault a healthy binary (it is preserved; the next event delivers through it). Only **3 consecutive** missed commits, or a real binary death, faults the binary — and a real fault gets a **bounded eager respawn** (a h...\n 366|- **Known keys today:** `command` (preferred) and the deprecated `path`. spt-core does **not** `deny_unknown_fields` here — a newer adapter declaring a future key against an older spt-core parses fine (the unknown key is ignored), so the lifecycle-binary contract degrades gracefully.\n*367|- A minimal binary just emits `{\"text\":payload}{\"key\":\"enter\"}{\"commit\":true}` with no choreography (spt-hosted idle delivery is translation-binary-only since v0.14.3 — no raw-inject fallback).\n 368|\n 369|### `[session]` watched dirs (file-drop)\n 370|```toml\n...\n 499|repo = \"user/repo\"\n*500|message = \"Run `/reload-plugins` in any ongoing sessions.\"   # the static fallback notice\n 501|\n 502|[update.post]\n 503|command = \"{adapter_dir}/claude-spt post-update\"   # REQUIRED, non-empty; install-dir + adapter-static subst\n...\n 580||---|---|---|\n*581|| 7 CC hooks | hooks.json → `owl.exe <subcmd>` | `[hooks.*]` table; `Stop.can_inject=false` drives the echo-gate fallback |\n 582|| `cc`/`cc <id>` launcher | hand-written shell wrappers | `[adapter] shortcut_basename = \"cc\"` → the picker bakes `cc-<id>` (spt-core default is `spt-<id>`) |\n 583|| Psyche go-live gate | *(declaration only — never spawned)* | `[session.psyche_init]` present ⇒ LiveAgent |\n 584|| Psyche turn (the driven role) | `claude -p --resume <psyche-own-uuid> --model sonnet …` | `[session.psyche_resume]` + `{session_id}`/`{parent_session_id}`/`{psyche_context_file}`/`{subnet}` |\n...\n 586|| Echo-commune | `claude -p --model haiku --effort low …`, `OWL_ECHO_COMMUNE=1` guard | `[session.echo_commune]` + `recursion_guard_env` |\n*587|| `ccs` fallback | tier-2 binary swap | **node-wide cross-adapter fallback** (not manifest; `ccs` is its own adapter) |\n 588|| Transcript read | `.claude/projects/<enc-cwd>/<uuid>.jsonl` | `[history]` (fetcher or locate_normalize) |\n 589|| Session digest | extract `.claude/…/<uuid>.jsonl` → `{role,text,tool,ts}` | `[digest].extractor` (own seam, ADR-0019; not `[history]`) |\n 590|| Commune/signoff | `.claude/<id>-commune.md` watched | `[session].commune_dir/signoff_dir`; filenames fixed |\n## MESH-RECOVERY-TRIAGE.md\n 79|   discriminator required.\n*80|5. **Migration preserves fallback.** The W2 rebuild touches invalid rows\n\n[Showing lines 1-295 of 430 (49.9KB limit). Use :296 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":"docs, .claude/reports, crates/spt-daemon/src, crates/spt-daemon/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":86,"fileCount":20,"files":["docs/BROKER-BRAIN-SPLIT-RESTORATION.md","docs/DEBUG-CONVERGE-PLAN.md","docs/DEBUG-ROLLOUT.md","docs/DEFERRED.md","docs/F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md","docs/FAULT-MATRIX.md","docs/FLAKE-LEDGER.md","docs/GATEWAY-LIVENESS-DISPATCH.md","docs/GATEWAY-RCA-STEP1.md","docs/KNOWN-HAZARDS.md","docs/MANIFEST.md","docs/MESH-RECOVERY-TRIAGE.md","docs/MSG-IDENTITY-DISPATCH.md","docs/MUSL-W1-DISPATCH-RULING.md","docs/NEXT-MILESTONE-BUG-TRIAGE.md","docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","docs/NEXT-MILESTONE-MUSL-TIER-TRIAGE.md","docs/NEXT-MILESTONE-PICKER-POLISH-TRIAGE.md","docs/NEXT-MILESTONE-PICKER-TRIAGE.md","docs/NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md"],"fileMatches":[{"path":"docs/BROKER-BRAIN-SPLIT-RESTORATION.md","count":4},{"path":"docs/DEBUG-CONVERGE-PLAN.md","count":1},{"path":"docs/DEBUG-ROLLOUT.md","count":1},{"path":"docs/DEFERRED.md","count":1},{"path":"docs/F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md","count":2},{"path":"docs/FAULT-MATRIX.md","count":5},{"path":"docs/FLAKE-LEDGER.md","count":2},{"path":"docs/GATEWAY-LIVENESS-DISPATCH.md","count":7},{"path":"docs/GATEWAY-RCA-STEP1.md","count":4},{"path":"docs/KNOWN-HAZARDS.md","count":20},{"path":"docs/MANIFEST.md","count":13},{"path":"docs/MESH-RECOVERY-TRIAGE.md","count":1},{"path":"docs/MSG-IDENTITY-DISPATCH.md","count":1},{"path":"docs/MUSL-W1-DISPATCH-RULING.md","count":2},{"path":"docs/NEXT-MILESTONE-BUG-TRIAGE.md","count":3},{"path":"docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","count":1},{"path":"docs/NEXT-MILESTONE-MUSL-TIER-TRIAGE.md","count":2},{"path":"docs/NEXT-MILESTONE-PICKER-POLISH-TRIAGE.md","count":1},{"path":"docs/NEXT-MILESTONE-PICKER-TRIAGE.md","count":3},{"path":"docs/NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md","count":12}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n 141│**Decision.** Apply ADR-0004 §B's rule — *a resource is broker-owned iff a live consumer would lose continuity on a brain restart* — to every component that today shares the in-process `Arc<Broker>`:\n*142│- **Net bring-up + REQ-DAEMON-9 self-heal retry → broker** (the brain must not own net bring-up). *Audit correction:* the NetHost is **already** broker-owned (`broker.rs:175` `OnceLock`, §B M4-D4a), and net bring-up + the self-heal retry already live in the `daemon.rs` entry that *becomes* the broker entry — so this item is **near-free**, not a migration.\n 143│- **Digest hub → broker** for the live projections + subscriber sockets; **the parse that feeds it → brain** (pushes parsed digests over IPC).\n 144│- **Seed lock + liveness → broker** (Q2).\n 145│- **Every other brain→broker call becomes a versioned IPC verb** — in the two-process world there is no shared `Arc`, so each current direct call is a latent IPC verb on the same versioned contract that already carries `SPAWN`/`INPUT`/`NET_*`/`SESSIONS`.\n    │...\n 167│\n*168│### Q7 — Failure atomicity: bounded retry → auto-rollback to last-known-good\n 169│\n 170│**Decision.** If the new brain fails to reach a **ready** signal (re-attached all sessions + resumed loops) within a bounded number of boots / a healthy-run window, the broker **rolls back to the last-known-good binary** (the `spt.exe.old-N` backup), **quarantines** the bad version (no auto re-apply/re-fetch), and surfaces a loud consent-style notification. The broker holds **last-known-good + candidate** paths and chooses which to spawn (no file manipulation at failure time). Endpoints survive throughou...\n 171│\n    │...\n 178│\n*179│**Alternative rejected.** *Bounded retry → halt and wait for manual intervention* — safer-feeling but leaves logic dead until a human shows up; inconsistent with the self-healing posture.\n 180│\n 181│### Q8 — Cross-platform uniformity (confirmation, not a fork)\n 182│\n    │...\n 228│- **Spikes** — `docs/spikes/SPIKE-01-broker-handoff.md`, `-03-quic-survival`, `-04-forkpty-parity`, `-05-restart-stress`, `-06-idempotent-boundary` (proofs that the two-process handoff works on both OSes).\n*229│- **Key source** — `daemon.rs` (`run`, net consumers, self-heal, digest hub, shellwake, seed-control), `broker.rs` (`OutputLog`, `bind`, `EffectJournal`), `brain.rs` (`BrainState`, `cold_start`/`handoff`/`snapshot`), `applyhost.rs` (`apply_staged`, `connect_retry`, record-before-handoff), `update.rs` (classes, `classify`, `apply_brain_only`), `peerloop.rs` (cadences, supervise-backoff), `lifecycle.rs` (pulse loop), `registryhost.rs`.\n 230│\n 231│---\n 232│\n## DEBUG-CONVERGE-PLAN.md\n 137│   `DEBUG-ROLLOUT.md` with `xtask debug-converge`, keeping the manual path as\n*138│   the fallback.\n 139│\n 140│## Traceability\n 141│\n## DEBUG-ROLLOUT.md\n  98│otherwise, `2` on usage). Spec + state model: `docs/DEBUG-CONVERGE-PLAN.md`.\n *99│The manual walk below remains the fallback.\n 100│\n 101│Useful outcomes:\n 102│\n## DEFERRED.md\n 29│| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache...\n*30│| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |\n 31│| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`w...\n 32│| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version ...\n 33│| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred \"option 1\")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but no...\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n 127│   (Field-confirmed adapter shape: claude-spt declares\n*128│   `[env.CLAUDE_CONFIG_DIR] direction=\"read\"` with `~/.claude` fallback —\n 129│   adapter reads, never sets.)\n 130│2. **Install-dir-first resolution (REQ-INSTALL-11):** the program path resolves\n 131│   via `resolve_program_in_dir` like every other adapter-manifest program.\n    │...\n 219│   Adapter shape confirmed: `[env.CLAUDE_CONFIG_DIR] direction=\"read\"`,\n*220│   fallback `~/.claude`, adapter reads and never sets.\n## FAULT-MATRIX.md\n 27│| 8 | **Net disabled / endpoint bind fails at boot** | No WAN; net frames answer `enabled:false` / typed error | PTY hosting (daemon degrades to net-less broker) | Fix config, restart daemon | `broker.rs::dispatch_net_status` (None arm); `Daemon::run` degrade path |\n*28│| 9 | **Broker process restart (journal survives, conns don't)** | Live conns/streams lost | Effect journal (durable) | Deduped op whose resource is gone → typed \"retry with a fresh op_id\" error; brain re-dials; an `spt rc` operator viewport RECONNECTS on its own — centered banner, full establish re-drive inside a bounded window, then a plain-language give-up (REQ-RC-RECONNECT) <!-- [doc->REQ-RC-RECONNECT] --> | `broker.rs::dispatch_net_dial` / `dispatch_net_stream_open` restart arms; `rc.rs` reconne...\n 29│| 10 | **WAN msg feed replayed** (sender redrive, or receiver brain restart resubscribing from a stale cursor) | None — replayed records dedup | Spool state (each op exactly one row); already-delivered ops | Durable `wan_seen` op-id claims absorb the replay; spool path claims atomically with the row | `tests/wanmsg.rs::receiver_restart_replays_feed_without_double_delivery`; `spool.rs` wan unit tests |\n 30│| 11 | **WAN msg record forges its origin** (payload `origin_node`/`node` field) | None — forgery inert (unknown field) | Access-gate subject (handshake-proven `remote_id_hex`) | None needed (records carry no origin field by design — KH 7.5) | `tests/wanmsg.rs::wan_message_lands_exactly_once_under_transport_origin`; `wanmsg.rs::forged_origin_field_is_inert` |\n 31│| 12 | **Target brain killed mid-remote-attach** (operator typing into the dead window) | Pumping pauses until handoff | Broker-held QUIC stream, session + child, output log, effect journal; the operator's viewport | Successor re-serves the same stream from seq 0 (worst case): replayed input dedups at the PTY-write journal; re-transmitted output dedups at the operator's render cursor — every byte exactly once | `tests/attach.rs::attach_survives_target_brain_restart_exactly_once` |\n   │...\n 33│| 14 | **Stream lost mid-context-bundle-sync** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's context store (an incomplete bundle is never fetched — length-gated, scratch file deleted); the responder's store untouched | No resume protocol by design: re-pull fresh — the apply is ancestry-idempotent (an already-joined tip short-circuits; fetched tips quarantine under `refs/spt-sync/` so a partial fetch never clobbers `refs/heads/`) | `tests/sync.rs...\n*34│| 15 | **Partition: concurrent context writes on two nodes** | The file's auto-propagation pauses (conflict surfaced, not propagated) | BOTH versions — local file untouched on each node + the other's version as a durable tracked artifact (hazard 6.6); every other file keeps syncing | Elected reconciler (Active instance's node; lowest-id fallback) runs one bounded Psyche turn → merged write `join(vA,vB)+bump` dominates both parents → propagates as a plain accept, clears artifacts subnet-wide | `test...\n*35│| 16 | **Reconcile turn fails** (harness absent / timeout / garbage or empty output / 6.5-suppressed write) | That file stays in conflict-holding state | Both versions (artifacts + local file) — exactly where they were; the store, the sync loop | Retry at the next sync/activation; a fresher direct write re-merges against the new local version | `spt-live reconcile.rs::failed_turn_preserves_everything` / `suppressed_write_preserves_artifacts`; `turn.rs::empty_stdout_is_an_error` |\n*36│| 17 | **Registry ambiguity during reconciler election** (partition: two nodes both see Active / no Active anywhere) | Two textually-different merges of the same pair may both mint | Neither version is ever lost — merged writes carry joined vectors, so the two merges classify **concurrent** and re-surface as a new conflict pair (detected, not silently last-wins) | Deterministic tiebreak bounds the storm (lowest node id among Actives / non-Offline holders); the re-surfaced pair reconciles on the next pa...\n 37│| 18 | **Compromised relay node serves a tampered update artifact** (bytes corrupted at rest after the relay's own verification) | That one pull ends `Rejected(ArtifactMismatch)` at the puller | The puller's staged release + running binary (nothing staged, nothing applied — no code path from wire bytes to the cache except through `plan_verified`); the rest of the subnet pulls from honest peers | Pull from another peer; the per-node gate (REQ-UPD-2) runs at **every** hop, so one poisoned node never re-p...\n 38│| 19 | **Non-conforming peer offers a rollback/expired/off-channel release** | None — rejected pre-fetch, zero artifact bytes move | The puller's version (monotonic floor holds); the wire (no transfer ever starts) | None needed — the offer-then-fetch shape gates metadata before bytes (REQ-HAZARD-UPDATE-ROLLBACK) | `tests/propagate.rs::rollback_offer_is_rejected_before_any_fetch` |\n 39│| 20 | **Untrusted node queries for the staged update** | None — refused by not offering (fail-closed, the up-to-date shape: learns nothing, not even whether a release exists) | The staged release; the serve loop (one refusal, no state) | None needed; pair the node to entitle it | `tests/propagate.rs::untrusted_origin_gets_no_offer` |\n   │...\n 43│| 23 | **Psyche replies with no inbound sender to answer** (alarm-fired turn emits a reply) | That one intent drops with a typed outcome (fail-closed — never broadcast, never guessed) | Every perch (zero deliveries); the notify leg (independent) | The Psyche's next answered turn replies normally | `psyrelay.rs::reply_without_target_is_dropped` |\n*44│| 24 | **Live-Psyche driver loses stdout** (the 7.3 null-stdout shape: detached/`Stdio::null()` regression or a turn that produced nothing) | That turn fails loudly (`TurnError::EmptyOutput`); nothing relays | The relay boundary (no silent success — indistinguishable-from-bug shapes are errors); the spool | Fix the driver / retry the turn | `psyrelay.rs::null_stdout_driver_fails_the_guard`; `spt-live turn.rs::empty_stdout_is_an_error` |\n 45│| 25 | **Runaway Psyche dumps an oversize body** (> 64 KiB in one intent) | That intent drops with a typed outcome | Delivery substrate (never asked to carry it); other intents in the same turn relay normally | None needed (legitimate long bodies chunk at delivery — T3 `EVENT-PART`) | `psyrelay.rs::empty_and_oversize_bodies_drop_typed` |\n 46│| 26 | **Notif feed replayed whole** (re-presented rows after loss/restart — the push-feed's normal recovery) | None — the semilattice join no-ops (`Unchanged` per row, nothing written) | The spool (no duplicates, no state regression) | None needed: replay IS the recovery protocol (full-row re-presentation, no delta bookkeeping) | `tests/notifsync.rs::notif_spools_converge_over_the_wire_and_dismiss_replicates` (replay leg); `notifsync.rs::feed_converges_two_stores_and_dismiss_replicates` |\n 47│| 27 | **Concurrent dismiss + surface on two nodes** (the same notif dismissed on A while B marks it seen/surfaced) | None — the writes commute through the join (OR/union/max); both survive the bidirectional exchange | Both stores converge to identical rows; the dismiss latch (a stale undismissed copy can never un-dismiss) | None needed (no conflict exists to surface — every field is monotone) | `notifsync.rs::concurrent_dismiss_and_surface_commute_across_the_feed`; `spt-store notif.rs::merge_is_idem...\n## FLAKE-LEDGER.md\n 16│| 7 | `spt-daemon::attach attach_survives_target_brain_restart_exactly_once` | 1× kitsubito (2026-06-16 run 27595180782 attempt 1, M11-W3) | **TIMEOUT, not an assertion fail** — SLOW>60>120>180>TERMINATING, nextest TIMEOUT at the 240s cap (`attach.rs:600`). A heavy multi-process brain-restart-exactly-once test on the slow shared kitsubito box under concurrent load (n1-gate on the same runner + a parallel `ci.yml` docs-publish run firing simultaneously). W3-INDEPENDENT: Windows green end-to-end; ALL tu...\n*17│| 8 | 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). H...\n 18│| 9 | `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 b...\n*19│| 10 | `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: gre...\n 20│| 11 | `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...\n 21│| 12 | `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-...\n 22│| 13 | `spt::bin cli::tests::probe_all_cap_batches_into_windows` | 1× hfenduleam local gate (2026-07-06, stack matrix @0f08fa9 leg-3 rerun) | fast unit (0.52s natural) FAILED at 1.7s under the 1614-test full-parallel pool — probe-batching window assert with a timing component starved under load; subsystem disjoint from the triggering stack (F-2 reporting / E-2 grid title / picker rider). Isolated ×5: green, 0.52–0.55s. Same night as #12 — the full-parallel local matrix under a busy box is the com...\n## GATEWAY-LIVENESS-DISPATCH.md\n  6│At the SAME instant, for the SAME pid-alive locally-hosted gateway (mobile-gw, status field ABSENT in info.json, pid alive):\n *7│- **human `spt endpoint list`** → **ONLINE** (renders local perches via `roster::enumerate`, roster.rs:72 = `is_perch_alive` WITH the pid-fallback: status-absent → `is_process_alive(pid)` → alive).\n  8│- **`spt endpoint list --json`** → **Suspended** (renders the endpoint via `resource_projection` over the registry snapshot, cli.rs:2855 → `EndpointRowJson.status` = the registry row status).\n  9│- flynn confirmed mobile-gw IS advertised in all 3 subnet registry files (NOT a re-advertise gap — his earlier \"empty by_id\" was a walker error, retracted).\n 10│\n   │...\n 13│## Key source facts (already traced)\n*14│- `advertised_status` (registryhost.rs:822-833) DOES feed `effective_rest_state` the pid-fallback `is_perch_alive` (line 825). So the DAEMON's advertised value should be Active for a pid-alive gateway — the divergence is NOT the advertise computation.\n*15│- So the `--json` Suspended comes from the projection/rendering path dropping the pid-fallback the human local-roster applies. **Candidate sites to pin (STEP 1):**\n 16│  1. `resource_projection` (spt-net registry.rs:566) — does it re-derive/filter liveness (e.g. `is_registry_entry_alive`, status-based: liveness.rs:152, which for a status-ABSENT row = not ONLINE||UNBOUND → stale/offline) and DOWNGRADE the row to Suspended/offline, ignoring the advertised Active? (its test `resource_projection_filters_hidden_and_offline` @registry.rs:1821 says it DOES filter on liveness.)\n 17│  2. OR the registry ROW's advertised status is genuinely Active but the `--json` render maps it wrong.\n 18│  3. OR the human path applies a `reconcile_self_owned` local-truth override for locally-owned endpoints that the `--json` path skips (roster.rs:52 mentions reconcile_self_owned overriding stale subnet gossip).\n 19│- **DEFECT A (separate, CONFIRMED, stands):** `establish_perch`'s build (spt/src/api/startup.rs:214-302) carries `cwd/controllable/adapter/read_env` forward from the prior record but NOT `rest_state` → a re-bind writes a fresh InfoJson that WIPES the wake intent (flynn's tick11 rest_state:active vanish).\n*20│- **DEFECT B (RECLASSIFIED to LATENT — do NOT make it the primary fix):** the `status=online` write in `cmd_listen` (startup.rs:486-491) is gated on `live_capable` (manifest `[session.psyche_init]`); a gateway has no psyche_init so it never writes status=online. BUT the pid-fallback is DESIGNED to cover an absent status and DOES for the roster + advertised_status. The operator's ready_agent counterexample proved this: ready_agents work because claude-spt declares psyche_init (live_capable-by-manifest),...\n 21│\n 22│## The fix (after STEP-1 pin)\n*23│**Reader parity:** `endpoint list --json` must report the SAME status for a locally-hosted endpoint as the human `endpoint list` does — i.e., honor the pid-fallback / local-owned truth (`roster::enumerate` / `reconcile_self_owned`) so a machine consumer never sees a different liveness than a human. A pid-alive locally-hosted gateway must read the same (ONLINE/Active) on both surfaces.\n*24│- Fix at the pinned site (most likely: apply the local-roster / self-owned reconcile to the `--json` local-endpoint rows, OR make `resource_projection`'s liveness for a locally-owned row honor the pid-fallback instead of a status-only check).\n*25│- Do NOT reach for DEFECT B (forcing status=online for gateways) as the fix — it risks the seed-#5 orphan-listener falsely-ONLINE amplification; the reader-parity is the correct, minimal fix. (If STEP-1 shows the cleanest fix genuinely is establishing status for a relay-holding listener, flag it back to me before taking it — that's a design call, not a silent choice.)\n 26│- **DEFECT A:** preserve `prior.rest_state` on re-bind in `establish_perch`'s build (same carry-forward discipline as cwd/controllable).\n 27│\n 28│## REQs (mint at build-start, activate-don't-pre-fail)\n## GATEWAY-RCA-STEP1.md\n 20│row whose id matches a LOCAL (self-hosted) perch row, it **overrides the subnet\n*21│row's status with the local roster ground truth** (the `is_perch_alive` pid-fallback,\n 22│`roster::enumerate` → `liveness.rs:136` → status-absent + pid-alive ⇒ ONLINE).\n 23│\n 24│**So for a locally-hosted, pid-alive, status-absent gateway whose snapshot row is a\n   │...\n 42│Note: `advertised_status` (registryhost.rs:822) DOES compute Active via the\n*43│pid-fallback, so the daemon's *live* advertise is Active — consistent with your\n 44│trace. The divergence is that the `--json` reader shows the **persisted snapshot**\n 45│row (which can carry a stale/crash-time Suspended) without the local-owned reconcile\n 46│that the human/picker reader applies. Reader parity is the fix, not the advertise.\n   │...\n 52│(`roster::enumerate` / is_local), override its rendered status with the local\n*53│`is_perch_alive` pid-fallback truth (mirroring `reconcile_self_owned`). Result: a\n 54│locally-hosted endpoint reports the SAME status on human `endpoint list` and\n 55│`--json`. Minimal + does NOT touch the advertise or force status=online (avoids the\n*56│seed-#5 orphan-listener false-ONLINE amplification you flagged).\n 57│\n 58│`REQ-HAZARD-BIND-REST-STATE-CARRY` (DEFECT A, confirmed, independent): preserve\n 59│`prior.rest_state` on re-bind in `establish_perch`'s build (spt/src/api/startup.rs\n## KNOWN-HAZARDS.md\n  18│- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\n *19│- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).\n  20│- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\n  21│- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\n  22│\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    │...\n 116│- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n*117│- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n*118│- **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 119│- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n 120│\n 121│### 4.4 Deferred rows survive poll drain\n    │...\n 183│- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\n*184│- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\n 185│- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\n 186│- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\n 187│\n    │...\n 252│- **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*253│- **Invariant:** spt-term resolves the program ITSELF before handing it to `CommandBuilder`, bypassing portable-pty's `which`. A bare name is searched over `PATH` × `PATHEXT` (whose default order already prefers `.EXE`/`.COM` over `.BAT`/`.CMD`), then an extensionless fallback. A non-PE target is wrapped in its interpreter: `.cmd`/`.bat` → `cmd.exe /d /c <path>`, `.ps1` → `powershell -NoProfile -File <path>` (the wrap args precede the caller's args); a real executable spawns directly; an unresolvabl...\n 254│- **spt-core mapping:** `spt_term::winprog::{resolve_for_pty, resolve_in}` (the pure PATHEXT-precedence kernel + the Windows env wiring), wired into `spt_term::pty::PtySession::spawn_program_in`. Unit: `resolve_in` precedence (`.cmd`-over-shim, `.exe`-direct, explicit-extension, path-order, passthrough) [`winprog.rs`].\n 255│- **Source:** field diagnosis 2026-06-16 (operator dogfood, `claude-spt:ccs` bringup) — doyle.\n 256│\n    │...\n 259│- **Failure:** `atomic_write_bytes` was `fs::write(tmp)` + `rename(tmp, path)` with no `fsync`. The rename's directory **metadata** is journaled durable, but the tmp file's **data blocks** are still in the page cache. A hard reset (power loss, forced reboot) between the two flushes lands the rename but loses the data → the file reappears at its **full length filled with NUL**. Field incident: after a machine restart `owlery/hall-a/info.json` was 360 bytes of all-NUL (the nested psyche record 125 bytes ...\n*260│- **Invariant:** for records whose loss is **unrecoverable**, flush data to stable storage **before** the rename publishes the name — `File::create(tmp)` → `write_all` → `sync_all()` → `rename_with_retry`. Then a crash yields the complete old file OR the complete new file, never a NUL husk. (A file of correct length that is ALL-NUL is the diagnostic tell of this class — a non-fsync'd write caught by a hard reset.) **Durability is SCOPED, not blanket.** `fsync`ing *every* atomic write serializes...\n 261│- **spt-core mapping:** `spt_store::atomic::atomic_write_bytes_durable` / `atomic_write_string_durable` are the opt-in durable siblings; the default `atomic_write_bytes` / `atomic_write_string` stay non-durable. Durable callers are exactly `spt_store::info::write_info` (the perch record — the hall-a surface), `spt_store::nodeid` (the node seed — corrupt is node-bricking, never regenerated), and `spt_daemon::machineid` (minted-once machine id — a NUL husk would silently re-mint a different id). Read...\n 262│- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; blanket-fsync perf regression pinned + scoped by the gate rig (doyle), same date.\n 263│\n    │...\n 266│- **Failure:** three readers each collapsed a **corrupt** (present-but-unparseable) `info.json` into their fail-open ABSENT default, so a NUL-wiped perch (5.13) read as permanently live: `is_perch_alive` returned `true` (unreadable ⇒ interim-alive), `advertised_status` then saw alive + no resting record ⇒ `Active`, and the daemon self-gossiped that Active row every round (epoch 173k+). Result: `hall-a`, dead since a machine restart, showed ONLINE in `spt whoami` and on every remote picker.\n*267│- **Invariant:** CORRUPT ≠ ABSENT. A record that EXISTS but stays unparseable across the retry budget is a **destroyed** record — never a live endpoint. `is_perch_alive` reads corrupt ⇒ **not alive** (ABSENT keeps interim-alive parity — the ONE case that stays true); `advertised_status` then lands corrupt in the cold arm ⇒ **Suspended**, never Active/Dormant. Readers that decide liveness/status must branch on the tri-state (present / absent / corrupt), not an `Option` that fuses the last two.\n 268│- **spt-core mapping:** `spt_store::liveness::{read_raw_state, is_perch_alive}` (the tri-state kernel) and `spt_daemon::registryhost::advertised_status` (cascades to Suspended off the alive fix). `list_self_perch_ids` deliberately still lists a corrupt dir by existence — that is the *visibility* the local-roster fix (counter-39 #3) relies on, not a liveness claim. Candidate sibling seam `is_registry_entry_alive` (corrupt hosted row falls to a daemon-pid probe) is parked — it does not produce the ONLI...\n 269│- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; sequel to the v0.17.0 W4 presence-truth fix (cold ⇒ Suspended) which a corrupt perch bypassed via false-alive.\n 270│\n    │...\n 272│### 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file)  `[REQ-HAZARD-ATOMIC-TMP-COLLISION]`\n*273│- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `Bind...\n*274│- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind t...\n 275│- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\n 276│- **Source:** RCA 2026-07-01 (doyle gate rig) — write-trace side-channel + `cmd_bind` step probes pinned `PROBE cmd_bind establish ERR: Io(os error 2)`; explains the counter-39 gate-1 blanket failure, the dummy_harness ONLINE-race DIAGs, and the nondeterminism. Lesson: a widened timing window doesn't create a race, it reveals one.\n 277│\n    │...\n 403│- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop...\n*404│- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a heal...\n 405│- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the ...\n 406│- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.\n 407│\n    │...\n 431│- **Failure (Windows):** `spt endpoint run` autostarts the daemon (`ensure_running` → `detached_no_inherit`), which INHERITS the launching terminal's Windows Job Object. Windows Terminal / VS Code place the shell AND every descendant in a Job with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; closing the tab drops the job's last handle → the OS reaps the daemon + its broker-spawned ConPTY harness subtree. The `rc` pump detaching should end ONLY the viewport; the harness must keep running + stay re-attachable...\n*432│- **Invariant:** both daemon spawn paths request `CREATE_BREAKAWAY_FROM_JOB` (0x0100_0000), best-effort with an in-job FALLBACK on `ERROR_ACCESS_DENIED`(5) / `ERROR_INVALID_PARAMETER`(87) so a breakaway-denying job NEVER regresses the spawn. **A real job CAN deny breakaway** (proven: the cargo/CI runner's own ancestor job ACCESS_DENIES it), so efficacy against the real terminal is UNKNOWN until measured. **int stage = OPERATOR MANUAL ACCEPTANCE, not CI** (doyle ruling 2026-06-18): the runner sits inside ...\n*433│- **spt-core mapping:** `daemon.rs::detached_no_inherit` + `deelevate.rs::create_with_token` (the breakaway flag + fallback). Units: `detached_no_inherit_falls_back_under_a_breakaway_denying_job` (no-regression — the shared runner exercises the fallback directly), `breakaway_spawn_escapes_a_kill_on_close_job` (the OS escape mechanism — self-SKIPS where the ancestor job forbids breakaway). Unix: the daemon's `setsid` session-detach already keeps a closing terminal's SIGHUP off its children (guard test...\n 434│- **Source:** v0.12.0 real-harness defect (operator) → v0.12.1 L1 @5ae68f8; doyle design+int ruling 2026-06-18.\n 435│\n 436│### 7.11 A dead PTY child + a dropped operator pump must NOT wedge the broker for other clients  `[REQ-HAZARD-ATTACH-WEDGE]`\n    │...\n 505│- **Failure (F-019 post-mortem):** the v0.11.0 path raw-injected `payload+\\r` into the spt-hosted PTY whenever no working translation binary handled an inbound message — none declared, spawn-failed, FAULTED (commit-deadline miss / binary death), or its inject-worker channel dropped — AND acked `delivered=true`. But a bare `payload+\\r` does NOT submit on a modern TUI (Claude Code): the message was TYPED into the harness but never sent, a silent pseudo-delivery reported as success. That silent degrade-...\n*506│- **Invariant:** spt-hosted idle delivery is translation-binary-ONLY (ADR-0022 amendment). `dispatch_endpoint_input` with no working binary replies `endpoint_injected_envelope(ep, delivered=false)` and writes NOTHING to the PTY; the caller (`try_broker_inject` → `cmd_send`) reads `delivered=false` and falls through to `deliver::send` = SPOOL (poll-fed, never lost), reporting an honest `QUEUED`, never a confident-but-false `SENT`. The failure is LOUD (`ENDPOINT_INJECT:<ep>: no working translation binary...\n*507│- **Boundary — what this does NOT cover (the STATE-vs-transient precision):** the guarantee is the steady FAILED STATE — once a binary IS faulted/absent/worker-gone, subsequent deliveries spool (`faulted` is MONOTONIC: set once, never respawns, so the state is reached deterministically). It does NOT cover the FAULT-TRANSIENT: a delivery that lands in the worker's commit window — BEFORE `event_rx` is dropped / `faulted` is set — can be optimistically enqueue-acked (`delivered=true` the instant `ev...\n*508│- **spt-core mapping:** impl `broker.rs` `dispatch_endpoint_input` (no-working-binary → `delivered=false` + loud log, no `input.enqueue`; the `input` writer is no longer resolved from the sessions table) + `build_translation`/`fault_translation` (`None`/fault now MEAN spool); unit `msg.rs` `endpoint_injected_envelope_carries_delivered_both_ways` (the `delivered=false` spool signal round-trips); int (real broker+PTY) `tests/broker.rs` `endpoint_keyed_inject_without_binary_spools_not_pty` + `tests/inject...\n 509│- **Source:** v0.14.3 (ADR-0022 amendment @1eaeef1, operator-ruled; doyle-scoped, the F-019 follow-up). CHANGE-5 (`cli.rs` honest report) verified a NO-OP — the spool fall-through already reported `QUEUED`, never `SENT`.\n 510│<!-- [doc->REQ-HAZARD-IDLE-SILENT-NONDELIVERY] -->\n 511│\n    │...\n 515│- **Failure (F-023, RCA @ `f023-f024-wan-idle-starvation`):** a message that has already landed in a node's spool — WAN-arrived, or locally spooled-while-active — stranded INDEFINITELY on an spt-hosted, relay-LESS endpoint whenever the adapter's hooks went quiet (an idle session with no user turns). An spt-hosted perch has no `api listen` relay to wake it, and NOTHING in the daemon re-offered a spooled message once the arrival moment passed — delivery silently depended on the adapter happening to c...\n*516│- **Invariant:** the daemon DRIVES delivery of an already-spooled message on the events IT owns — never on hook cadence — for a relay-less spt-hosted endpoint. TWO daemon triggers feed ONE shared spt-hosted inject leg (`spt_daemon::inject::try_spt_hosted_inject`, translation-binary-ONLY): (1) **WAN ingress** — `receive_wan` injects via the live binary BEFORE the spool fallback (`[REQ-WAN-SPT-HOSTED-DELIVERY]`, leg 1); (2) **the ACTIVE→IDLE edge** — `drain_idle_window` (fired by `api state idle`...\n 517│- **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 518│- **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).\n 519│<!-- [doc->REQ-HAZARD-DELIVERY-STARVATION] -->\n    │...\n 523│### 7.24 A delegated live adapter apply must NEVER report success without swapping, and the live-update seam must use ONE parent-aware adapter matcher  `[REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP]`\n*524│- **Failure (F015B / BUILD-F015B-APPLYMATCH):** a live adapter update to a PROFILE-COMPOSITE endpoint (`--adapter cc:ccs`) silently no-oped — `LIVE` then `DONE` printed, but the version-of-truth stayed OLD. TWO defects on one seam. **(D1, matcher skew):** the broker's `dispatch_adapter_apply` filtered sessions by EXACT `s.adapter == req.adapter`, but a `--adapter <adapter>:<profile>` endpoint stores the COMPOSITE `cc:ccs` (set at `cmd_endpoint_run` → `launch_harness_brokered_in` → `SpawnReq.adapter...\n 525│- **Invariant:** (1) ONE shared parent-aware matcher `spt_runtime::profile::adapter_parent_matches(session_adapter, parent)` (`split_option(session_adapter).0 == parent`) is used at EVERY live-update comparator — the CLI live-gate, the broker apply-filter, AND `select_endpoints_running_adapter`; NO exact `==` against a record name at any live-update seam (a profile-composite endpoint always resolves to its parent). (2) Once the apply is delegated the daemon owns the WHOLE apply: the CRC swap runs UNCON...\n 526│- **spt-core mapping:** impl `spt-runtime/src/profile.rs` `adapter_parent_matches` wired at `cli.rs` `adapter_has_live_endpoint` + `broker.rs` `dispatch_adapter_apply` session filter + `broker.rs` `select_endpoints_running_adapter`; `broker.rs` `dispatch_adapter_apply` swaps unconditionally (empty-affected early-return removed). unit `profile.rs` `adapter_parent_matches_on_parent_not_composite` (matcher truth table) + `broker.rs` `select_endpoints_running_adapter_filters_dedups_sorts` (extended with a `:...\n 527│- **Source:** F015B (operator escalation, doyle RCA code-verified end-to-end 2026-07-02; todlando build). ADR-0025 amendment records both invariants.\n    │...\n 534│- **Invariant:** every adapter-manifest program path resolves against the adapter install dir BEFORE PATH (REQ-INSTALL-11, `resolve_program_in_dir`) — at the harness session spawn (`harnesshost::launch_harness_brokered_in`: the idle-translation binary AND the session program), the W3d live-update RESPAWN (`broker::read_translation_path`), and the notif command (`with_install_dir`, both the daemon `notif.rs` and the api `reporting.rs` render paths). **Diagnosis rule:** before any deep behavioral analysi...\n*535│- **Source:** F-019 (v0.14.2); root confirmed on real claude-spt (perri + doyle). The raw-inject FALLBACK itself (degenerate no-binary floor) is unchanged by F-019 — its removal (message stays SPOOLED + poll-fed, never raw PTY inject) rides a separate ADR-0022 amendment.\n 536│<!-- [doc->REQ-INSTALL-11] -->\n 537│\n 538│---\n    │...\n 541│- **Failure (F-024C/F-024D, ENLYZEAM field + clean-room repro 2026-07-02):** `authenticate()` (auth.rs) gates `api poll`/`state`/`boundary` on `proof.session_id == info.json.session_id`. If ONE boundary rotation is LOST — the departing session dies (crash / tab-close) or its `/clear`-era `boundary` call never lands — the perch stays PINNED to the dead sid, and every id-scoped hook call thereafter AUTH_REFUSES *including `boundary` itself* (the very call that would re-pin presents the NEW sid). A perm...\n*542│- **Invariant:** `authenticate()` gains a DEAD-OWNER fallback — when the caller's sid MISMATCHES the pin **AND** the perch's recorded owner pid is DEAD (`proc::is_process_alive == false`), ACCEPT the caller's sid and RE-PIN (rotate `info.json.session_id` via the locked `mutate_info` RMW + a LOUD `SESSION_REPIN` stderr line). Same trust model as `establish_perch`'s conflict gate, which already allows a rebind exactly when `owner_alive == false` (an orphaned perch accepts a new LOCAL owner). This is **AD...\n 543│- **spt-core mapping:** impl `spt/src/api/auth.rs` `authenticate` dead-owner-mismatch branch (`read_pid` → `proc::is_process_alive` → `mutate_info` rotate + `SESSION_REPIN` log); unit `auth.rs` `pinned_to_dead_sid_mismatched_poll_repins` (AUTH_REFUSED → drains+re-pins, RED pre-fix) + `live_owner_mismatch_still_refuses` (the `/clear` live-rotation squat guard, unwidened) + `token_auth_path_unchanged` (correct token wins first, no rotation).\n 544│- **Source:** F-024C/F-024D (perri field RCA on ENLYZEAM + doyle code-confirm 2026-07-02; the wedge latches on `/clear` even in a CLEAN env — the env-corruption domino was sufficient but not necessary). COVERAGE: dead-owner re-pin rescues CRASHED/DEAD owners ONLY; live-pid rotation is correctly refused without departed-session proof. Cross-refs ADR-0032 (the 3-layer rotation-credential model) + `[REQ-BOUNDARY-ROTATION-CREDENTIAL]` (layer 1, the adapter/harness-contract half).\n 545│<!-- [doc->REQ-HAZARD-SESSION-PIN-WEDGE] -->\n## MANIFEST.md\n  15│- **Config knobs** (pulse period, echo-commune window, route-guard window, daily-refresh) — spt-core **global settings**, with optional **per-endpoint override** for endpoints with unique needs. Not per-adapter.\n *16│- **Cross-adapter fallback** (psyche/echo invocation falls back to another adapter on rate-limit) — a **node-wide setting**, not a manifest field, because it references *multiple* adapters (under spt-core, `ccs` is its own adapter, not a binary-swap within one). Adapter-agnostic IDs make this safe — an endpoint's Psyche can run under a fallback adapter temporarily.\n  17│- **Plugin-cache / version-trampoline layout** — none of SPT's concern. A CC-plugin adapter updates via `claude plugin update` (delegated command).\n  18│\n  19│<!-- [doc->REQ-MANIFEST-1] The sections below are the authoritative schema; the\n    │...\n  93│reads = []\n *94│can_inject = false                 # no hookSpecificOutput schema → sentinel/relay fallback\n  95│```\n  96│\n  97│### `[session.<role>]` — outbound session role templates\n    │...\n 115│[session.psyche_resume]\n*116│command = \"claude -p --resume {session_id} --agent owl-psyche --model sonnet --fallback-model opus --output-format json\"\n 117│env_remove = [\"OWL_SESSION_ID\"]\n 118│recursion_guard_env = \"SPT_ECHO_COMMUNE\"   # set on summarizer children so their own hooks bail\n 119│keys = [\"session_id\", \"parent_session_id\", \"psyche_context_file\", \"subnet\"]   # the psyche keys spt-core fills\n    │...\n 178│|---|---|---|\n*179│| `{node}` | this node's **advertised label** — the label store value (OS-hostname default, re-checked at daemon startup), OS-hostname fallback when no store label is known | **SINGLE-TOKEN fill only** — tokenize-then-fill cannot produce a space-carrying argv element, so a composite display name like `<id> @ <node>` stays adapter-shim territory (`{node}` complements the shim for tokenizable args, e.g. `--remote-control {id}--{node}`, it does not replace it). **Caveat:** the daemon-side lifecycle reso...\n 180│\n 181│<!-- [doc->REQ-MANIFEST-NODE-KEY] -->\n 182│```toml\n    │...\n 220│direction = \"read\"\n*221│value = \"~/.claude\"                # FALLBACK when the var is unset (the base harness)\n 222│```\n 223│\n 224│- **Allowlist, not the whole env** — only the vars you declare `direction = \"read\"`\n    │...\n 226│- **Resolution order at digest time**: the value **captured** from the ambient env\n*227│  at bind (the relocating profile) → the directive's **`value`** fallback (the base\n 228│  harness) → **omitted**, so the `{VAR}` fails to fill and the digest diagnoses\n 229│  \"not set\" rather than resolving a wrong path (the same skip-diagnose posture as a\n 230│  missing `[digest]` source).\n 231│- **`~` expansion** — a leading `~/` (or bare `~`) in the captured value or the\n*232│  `value` fallback expands to the OS home (`USERPROFILE` on Windows, `HOME` on\n 233│  Unix). Any other `~` passes through verbatim.\n 234│- spt-core names **no** harness variable and bakes **no** default root — the\n 235│  adapter declares which var to capture and how its transcript path consumes it\n    │...\n 298│  adapter-declared **defaults** any consumer may override; spt-core ships\n*299│  fallbacks (`3` / `25` / collapse-on).\n 300│\n 301│Prove your extractor before shipping: `spt adapter digest-proof <adapter>\n 302│--sample <real-log>` runs it and prints the parsed records, the rendered digest,\n    │...\n 364│- **`{\"commit\":true}` is the mandatory sequence TERMINATOR, required for EVERY `{\"type\":\"event\"}`.** While a sequence is in flight spt-core buffers the live `spt rc` controller's keystrokes (the *inject floor*) and applies the emitted commands atomically; the trailing `{\"commit\":true}` releases the floor and flushes the buffered controller input *after* the sequence. The submit keystroke is NOT the terminator (a choreography may keep typing after `{\"key\":\"enter\"}`). **An empty response is a protocol viol...\n*365│- **Commit deadline + miss tolerance (F029 C-1, ADR-0022 amendment).** If no `{\"commit\":true}` arrives within the **commit deadline (5 s, `INJECT_COMMIT_DEADLINE`)**, spt-core flushes the buffered operator input (never stranded) and re-spools that message once — but a SINGLE miss does NOT fault a healthy binary (it is preserved; the next event delivers through it). Only **3 consecutive** missed commits, or a real binary death, faults the binary — and a real fault gets a **bounded eager respawn** (a h...\n 366│- **Known keys today:** `command` (preferred) and the deprecated `path`. spt-core does **not** `deny_unknown_fields` here — a newer adapter declaring a future key against an older spt-core parses fine (the unknown key is ignored), so the lifecycle-binary contract degrades gracefully.\n*367│- A minimal binary just emits `{\"text\":payload}{\"key\":\"enter\"}{\"commit\":true}` with no choreography (spt-hosted idle delivery is translation-binary-only since v0.14.3 — no raw-inject fallback).\n 368│\n 369│### `[session]` watched dirs (file-drop)\n 370│```toml\n    │...\n 499│repo = \"user/repo\"\n*500│message = \"Run `/reload-plugins` in any ongoing sessions.\"   # the static fallback notice\n 501│\n 502│[update.post]\n 503│command = \"{adapter_dir}/claude-spt post-update\"   # REQUIRED, non-empty; install-dir + adapter-static subst\n    │...\n 580│|---|---|---|\n*581│| 7 CC hooks | hooks.json → `owl.exe <subcmd>` | `[hooks.*]` table; `Stop.can_inject=false` drives the echo-gate fallback |\n 582│| `cc`/`cc <id>` launcher | hand-written shell wrappers | `[adapter] shortcut_basename = \"cc\"` → the picker bakes `cc-<id>` (spt-core default is `spt-<id>`) |\n 583│| Psyche go-live gate | *(declaration only — never spawned)* | `[session.psyche_init]` present ⇒ LiveAgent |\n 584│| Psyche turn (the driven role) | `claude -p --resume <psyche-own-uuid> --model sonnet …` | `[session.psyche_resume]` + `{session_id}`/`{parent_session_id}`/`{psyche_context_file}`/`{subnet}` |\n    │...\n 586│| Echo-commune | `claude -p --model haiku --effort low …`, `OWL_ECHO_COMMUNE=1` guard | `[session.echo_commune]` + `recursion_guard_env` |\n*587│| `ccs` fallback | tier-2 binary swap | **node-wide cross-adapter fallback** (not manifest; `ccs` is its own adapter) |\n 588│| Transcript read | `.claude/projects/<enc-cwd>/<uuid>.jsonl` | `[history]` (fetcher or locate_normalize) |\n 589│| Session digest | extract `.claude/…/<uuid>.jsonl` → `{role,text,tool,ts}` | `[digest].extractor` (own seam, ADR-0019; not `[history]`) |\n 590│| Commune/signoff | `.claude/<id>-commune.md` watched | `[session].commune_dir/signoff_dir`; filenames fixed |\n## MESH-RECOVERY-TRIAGE.md\n 79│   discriminator required.\n*80│5. **Migration preserves fallback.** The W2 rebuild touches invalid rows\n 81│   only; valid rows and the absent-file/corrupt-degrades-empty behavior are\n 82│   untouched (existing REQ-CONV-1 units must stay green unmodified where\n 83│   behavior is unchanged, amended where drop-on-fail semantics changed —\n## MSG-IDENTITY-DISPATCH.md\n 37│  the refusal-consumption contract in harness-contract/api.md.\n*38│- `REQ-LISTEN-SESSION-ID-FALLBACK` [doc,impl,unit,int] — `listen --session-id <sid>` binds when\n 39│  the ephemeral seed is gone (late go-live / post-daemon-restart). Doc stage pins seed lifetime.\n 40│- Roots pinned (scope F-034-sptc-live-core-legs.md). Field-verify: hertz + perri.\n 41│\n## MUSL-W1-DISPATCH-RULING.md\n 21│- **`KNOWN_TARGET_TRIPLES`** (release.rs:190) → derive from `SUPPORTED_PLATFORMS.map(|p| p.triple)` (keep the const name/type if callers depend on `&[&str]`; a helper or a `once`/const-fn projection).\n*22│- **`current_platform()`** (release.rs:196-211) STAYS a `cfg`→triple fn (a running binary's triple is a **build fact**, cfg-selected — not runtime-derivable). Keep the loud `\"unknown\"` fallback. ADD a unit asserting `current_platform()` is a member of `SUPPORTED_PLATFORMS` for the compiled target (self-identity can't drift from the registry).\n 23│- **`applyhost.rs:740-743`** — the \"the OTHER platform\" binary `if me==windows { linux } else { windows }` is the **N-platform blocker**. Generalize to \"every `SUPPORTED_PLATFORMS` triple except `current_platform()`\" (a filter). Preserve the existing semantic (a node's own platform vs the others) — the test that uses it must stay green.\n 24│- **asset-name ↔ triple map** (`xtask/src/main.rs:598-599`) → derive from the table (lookup by `asset_name`).\n 25│- Test literals (`cli.rs:16922-923`, release.rs/update.rs/net-update.rs) — leave as-is unless they block; they assert on the existing two triples, which are unchanged.\n   │...\n 33│- Diff review: no behaviour change for gnu/windows; `SUPPORTED_PLATFORMS` is the single source; applyhost generalized correctly (N-platform, not hardcoded pair).\n*34│- A musl build is NOT required for W1 (that's W2) — but the tree must still compile for a musl target (the `\"unknown\"` fallback stays until W2 adds the arm).\n 35│\n 36│## Sequencing\n 37│W1 (this) → gate → **W2** (register musl: `current_platform()` musl cfg arm + the musl `PlatformSpec` row + asset map; DNS/crypto/build already proven) → gate → **W3** (CI musl build + sign + update-set publish + self-update E2E on a sub-2.39-glibc box) → gate → release (deployah publishes). doyle drives the whole arc.\n## NEXT-MILESTONE-BUG-TRIAGE.md\n 176│### G-#17 — no digest for eel-a  · STATUS: SHIPPED CLOSED — W6 env-read @9305d19 (REQ-DIGEST-PROFILE-ENV) + W6b fetcher-strategy @b9e5ce9 (REQ-DIGEST-FETCHER-STRATEGY)\n*177│> **W6** shipped the harness-agnostic `[env] direction=\"read\"` seam (capture-at-bind → info.json.read_env → digest fill_template keys → `value` fallback → `~` expansion → skip-diagnose). **W6b** closed the `{project}` design-gate per doyle's CONTEXT-symmetry ruling: `[digest]` gained a `fetcher` strategy mirroring `[history]`'s — the ADAPTER's extractor locates+reads+emits (no spt-core pre-read, no `{project}` slug), fed only harness-neutral `{session_id}`+`{cwd}`(=info.json.cwd)+captured rea...\n 178│`spt endpoint digest eel-a` → `NO_DIGEST:eel-a has no activity buffer (no session-log source / no records yet?)`. **LIVE EVIDENCE:** eel-a has ZERO entries in daemon-effects.log → no records produced. Operator hypothesis: manifest setup vs code, perri-side. Either (a) the claude-spt adapter's session-log source (manifest [digest]/session-log path) isn't wired for eel-a, or (b) eel-a's brain wedged (#16) before producing records. Disentangle adapter-manifest-wiring vs spt-core-digest-source vs downstr...\n 179│- **Root cause (agent G, conf H): ADAPTER-MANIFEST CONFIG (perri-side), NOT spt-core, NOT downstream of #16.** digest reads ONLY two sources, neither auto-populated by spt-core: (1) `[digest]` extractor (digest.rs:164-177 → spt_live::extract_digest) resolving `[digest].source` template (else `[history].locate_template`) over the harness's own transcript (CC `~/.claude/projects/{project}/{session_id}.jsonl`); (2) log-less `perch/digest.log` via explicit `api digest-entry`. NO_DIGEST = both empty + never...\n 180│\n    │...\n 188│NEW: hfenduleam→enlyzeam ALSO failed to deliver, BOTH IPs stable. So address-staleness (prior cause (a)) is NOT the whole story, and the no-reply-leg (b) explains silent-LOSS but not WHY nothing delivers AT ALL. Need a DEEPER dive into the actual delivery path on a stable-IP pair: receiver perch-resolution, access gate (Refused?), spool-not-inject, whether the WAN stream even reaches receive_wan, whether the connection/membership-proof actually establishes. The false-SENT(WAN) (b) stands as a separate ...\n*189│- **Root cause (agent F, conf H):** the REAL drop is the DATA-PATH (candidate #4). `spt send` resolves the dial with id-only `addr_for_node_hex` (wansend.rs:79/207/333 → endpoint.rs:538-541 = EndpointAddr::new(id), ZERO direct paths/relay) → EVERY send forces a fresh iroh DISCOVERY round-trip (mDNS/n0-DNS/relay). The gossip PUMP — which makes the nodes \"mutually visible\" — instead uses `dial_seeded` (pump/mod.rs:694-716): tries the durable last-known direct addr from PeerAddrStore FIRST + caches ...\n*190│- **Fix (conf H, effort M):** route WAN dial through the pump's seeded-direct-addr resolution (PeerAddrStore first, id-only fallback — mirror dial_seeded); PAIR with (b) the ack reply-leg (receiver writes WanOutcome back, sender blocks under QUIC deadline → honest CLI + retry next addr). Contributing/lower-now: proactive addr-refresh on local IP change (a). **Confirm field (read-only):** PeerAddrStore peer-addrs.json WILL hold a good direct addr the send never tries; add a WanOutcome eprintln at disp...\n 191│\n 192│### D-#12 — REOPENED: `rc` garbled but `endpoint run --attach` renders FINE (same Win10/pwsh env)\n 193│NEW: operator confirms `spt endpoint run --attach` renders cleanly in the SAME Win10/raw-pwsh environment; ONLY `spt rc` to a pre-running endpoint is garbled (raw color+cursor codes). This REFUTES \"VT-output never enabled\" (if so, --attach would garble too). Real question: what does the `rc`-to-existing path do DIFFERENTLY from `endpoint run --attach` in terminal/VT setup? Diff the two paths' client terminal-mode init.\n## NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\n 28│Unit: relative dir + cwd → resolved; absolute → as-is; relative no-cwd → loud skip.\n*29│Hardening riders (same touch, no separate REQ): bounded EACCES retry on the drop write;\n 30│echo claude spawn gets explicit cwd = endpoint cwd (perri ask).\n 31│\n 32│**REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS** (impl, unit)\n## NEXT-MILESTONE-MUSL-TIER-TRIAGE.md\n 14│`current_platform()` (spt-daemon `release.rs:196-211`) is already the canonical accessor — most callers route through it. Hardcoded/assumption sites:\n*15│- `release.rs:196-211` `current_platform()` — cfg arms for msvc + gnu only; musl → `\"unknown\"` fallback.\n 16│- `release.rs:190-191` `KNOWN_TARGET_TRIPLES` — the 2-triple allow-list (adapter multi-platform archive detection).\n 17│- `applyhost.rs:740-743` — the cross-platform \"other\" binary logic **assumes exactly 2 platforms** (`if me==windows { linux } else { windows }`) — the N-platform blocker.\n 18│- `xtask/src/main.rs:598-599` — asset-name ↔ triple map (`spt-x86_64-linux` → gnu).\n   │...\n 25│One authoritative platform registry; `current_platform()`, `KNOWN_TARGET_TRIPLES`, the applyhost \"other\" logic, and the asset↔triple map all derive from it. gnu + windows behaviour **byte-identical** — the existing release/update/apply/propagate test suite stays green (that IS the gate).\n*26│- **DESIGN FORK (doyle to rule before dispatch):** (a) registry shape — `enum SupportedPlatform { WindowsMsvc, LinuxGnu, LinuxMusl }` with triple/asset/cfg methods, vs a `SUPPORTED_PLATFORMS: &[PlatformSpec]` const table. Lean: const table of `{ triple, asset_name }` — data-driven, one row per platform, no match-arm sprawl. (b) `applyhost` \"other platform\" — generalize the binary if/else to \"every registered platform except `current_platform()`\" (a filter over the registry). (c) `current_platform()...\n 27│- **Gate:** clippy --workspace, full nextest both legs (kitsubito Linux), traceable; diff review = no behaviour change for gnu/windows (release/update/apply/propagate suites unchanged & green). No new platform yet.\n 28│\n 29│### W2 — Register the musl platform (self-IDENTIFY) — `REQ-PLATFORM-MUSL`\n## NEXT-MILESTONE-PICKER-POLISH-TRIAGE.md\n 46│**Fix shape:** derive the display for `run_cwd` with the SAME source-derivation\n*47│the history refs use (folder tail; honest fallback to the path when underivable):\n 48│`format!(\"CURRENT DIR --> {display}\")`. Keep `cwd` payload unchanged. Update the\n 49│`starts_with(\"Here: \")` test asserts (3 sites :1876-1889) — they are behavior\n 50│assertions on the OLD label; grep-tests rule applies.\n## NEXT-MILESTONE-PICKER-TRIAGE.md\n  29│  sub-investigation, **SI-1** below — the picker must degrade better regardless.)\n *30│- **1b.** The fallback \"origin project\" (data.rs:207) is derived from `info.json.cwd`,\n  31│  which is **rewritten on every rebind** — it is \"latest boot cwd\", not origin. hall-a's\n  32│  latest rebind was into the f015b-probe **psyche** dir, so that's what displays.\n  33│- **1c.** Psyche-host sessions bind into the agent's perch with owlery-internal cwds\n    │...\n  49│(contextstore write → branch commit) with evidence, and land whatever fix the RCA names.\n *50│The picker fallback in 1a ships regardless, but the store must ALSO be made to fill.\n  51│\n  52│**SI-1 RCA VERDICT + FIX (todlando 2026-07-03, doyle RCA-gate).** ROOT: the daemon\n  53│used the adapter's RELATIVE `commune_dir`/`signoff_dir` (`.claude`) VERBATIM\n    │...\n 253│\n*254│**Fix shape:** `detect_self_id` gains **leg (c) — PID-ANCESTRY fallback** (after the\n 255│env legs): walk the current process's ancestry, match against roster perches'\n 256│`info.json.pid` (the harness pid; alive-gated, skip corrupt), first match = self.\n 257│**Scope guard:** this is a **from-label / routing default, NOT auth** — do NOT touch\n## NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md\n  16│  `--wait` flag rider.\n *17│- **Q3:** bare-id rest verbs route subnet-wide (send's fallback); one\n  18│  instance → act; many, none active → refuse loud with candidates; one\n  19│  active → no-op naming the node.\n *20│- **Q4 (ADR-0034):** per-minter journal-key namespacing + auto-retry-once on\n  21│  tracing-only op families; typed op errors become internal-only; the\n  22│  public error surface never speaks core lingo.\n  23│- **Q5 (@c248afc):** a session's adapter is recorded truth; resume follows\n    │...\n  58│Root (certain): `cmd_rest` routes remote only on `@`/`:` (cli.rs:3296); no\n *59│local-miss fallback, unlike `cmd_send` (cli.rs:5142 → resolve_across_visible).\n  60│Contradicts CONTEXT:286 (\"a wake must route\"). Fix (Q3): adopt send's\n *61│fallback for rest verbs + the Q3 selection rule (one → act; many-none-active →\n  62│`WAKE_AMBIGUOUS` listing candidates; one-active → no-op naming node).\n  63│Red-first: bare id present only in a remote registry snapshot routes instead\n  64│of `WOKE_FAIL`.\n    │...\n  85│  `resolve_wake`'s live-pid probe — pid alive ⇒ stand down (boot window),\n *86│  pid dead pre-bind ⇒ retry, bind closes the gate. The marker is\n  87│  CUSTODY-ONLY: never a liveness input (A-1 derivation must not read it).\n  88│- **`--wait` deferred** to its own rider seed (F-027 bind-await is\n  89│  design-only today; the core resume leg does not block on it).\n    │...\n 116│  nodes (NOT Ambiguous — nothing is actionable; an ambiguity refusal would\n*117│  just defer the same no-op behind a qualified retry); (2) suspend with a mix\n 118│  (suspended at X, active at Y) is NOT satisfied (∀-goal) — exactly one\n 119│  active ⇒ Act on it; several active ⇒ Ambiguous. Q3's stated wake cases are\n 120│  unchanged; this extends them.\n    │...\n 124│\n*125│**A-4 — op-id collision (`stream-open op already applied … retry with a\n 126│fresh op_id`).** Root (high): three counters (`cli-ops.json`,\n 127│`pump-ops.json`, `epoch.json`) mint from 1 into ONE broker journal keyspace\n 128│`(NET_EFFECT_SESSION, op)` (broker.rs:3021); CLI wake op colliding with an\n    │...\n 132│— F-030 W1's `psyche_ops_epoch_file()` already pre-aligned); old-shape keys\n*133│can't collide with new-shape → migration-free; (ii) auto-retry-once with a\n 134│fresh op on the typed no-longer-held error, scoped to tracing-only families\n 135│(rest, rc attach stream-open); sync/update durable `open_op` semantics\n 136│untouched (their dedup is load-bearing). Red-first: mint a CLI op equal to a\n*137│journaled daemon op → typed error internally → retry succeeds; user sees\n 138│nothing.\n 139│\n 140│**REQ seeds:** `REQ-EFFECTIVE-INSTANCE-STATE` (shared derivation, hazard-class\n 141│— A-1), `REQ-WAKE-RESUME-LEG` + `--wait` (A-2), `REQ-REST-VERB-ROUTING`\n*142│(A-3), `REQ-OPID-MINTER-NAMESPACE` + `REQ-OPID-TRACING-RETRY` (A-4).\n 143│\n 144│## B — update-apply remote wedge (operator #3)\n 145│\n    │...\n 372│  a non-developer can hit must name the observable situation + next action —\n*373│  never journal/op/brain lingo. Known offenders: the op-id retry text (dies\n*374│  inside ADR-0034's retry), `brain IPC read deadline elapsed` (parenthetical\n 375│  lingo in a user-facing line, rc.rs:1222), `WOKE_FAIL … not a hosted perch`.\n 376│  Kin to banked patterns: public --help no internal codes; \"Updated\" not\n 377│  \"trial\"; translation_fault not human-rendered (that seed rides here too:","truncation":{"content":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n 141|**Decision.** Apply ADR-0004 §B's rule — *a resource is broker-owned iff a live consumer would lose continuity on a brain restart* — to every component that today shares the in-process `Arc<Broker>`:\n*142|- **Net bring-up + REQ-DAEMON-9 self-heal retry → broker** (the brain must not own net bring-up). *Audit correction:* the NetHost is **already** broker-owned (`broker.rs:175` `OnceLock`, §B M4-D4a), and net bring-up + the self-heal retry already live in the `daemon.rs` entry that *becomes* the broker entry — so this item is **near-free**, not a migration.\n 143|- **Digest hub → broker** for the live projections + subscriber sockets; **the parse that feeds it → brain** (pushes parsed digests over IPC).\n 144|- **Seed lock + liveness → broker** (Q2).\n 145|- **Every other brain→broker call becomes a versioned IPC verb** — in the two-process world there is no shared `Arc`, so each current direct call is a latent IPC verb on the same versioned contract that already carries `SPAWN`/`INPUT`/`NET_*`/`SESSIONS`.\n...\n 167|\n*168|### Q7 — Failure atomicity: bounded retry → auto-rollback to last-known-good\n 169|\n 170|**Decision.** If the new brain fails to reach a **ready** signal (re-attached all sessions + resumed loops) within a bounded number of boots / a healthy-run window, the broker **rolls back to the last-known-good binary** (the `spt.exe.old-N` backup), **quarantines** the bad version (no auto re-apply/re-fetch), and surfaces a loud consent-style notification. The broker holds **last-known-good + candidate** paths and chooses which to spawn (no file manipulation at failure time). Endpoints survive throughou...\n 171|\n...\n 178|\n*179|**Alternative rejected.** *Bounded retry → halt and wait for manual intervention* — safer-feeling but leaves logic dead until a human shows up; inconsistent with the self-healing posture.\n 180|\n 181|### Q8 — Cross-platform uniformity (confirmation, not a fork)\n 182|\n...\n 228|- **Spikes** — `docs/spikes/SPIKE-01-broker-handoff.md`, `-03-quic-survival`, `-04-forkpty-parity`, `-05-restart-stress`, `-06-idempotent-boundary` (proofs that the two-process handoff works on both OSes).\n*229|- **Key source** — `daemon.rs` (`run`, net consumers, self-heal, digest hub, shellwake, seed-control), `broker.rs` (`OutputLog`, `bind`, `EffectJournal`), `brain.rs` (`BrainState`, `cold_start`/`handoff`/`snapshot`), `applyhost.rs` (`apply_staged`, `connect_retry`, record-before-handoff), `update.rs` (classes, `classify`, `apply_brain_only`), `peerloop.rs` (cadences, supervise-backoff), `lifecycle.rs` (pulse loop), `registryhost.rs`.\n 230|\n 231|---\n 232|\n## DEBUG-CONVERGE-PLAN.md\n 137|   `DEBUG-ROLLOUT.md` with `xtask debug-converge`, keeping the manual path as\n*138|   the fallback.\n 139|\n 140|## Traceability\n 141|\n## DEBUG-ROLLOUT.md\n 98|otherwise, `2` on usage). Spec + state model: `docs/DEBUG-CONVERGE-PLAN.md`.\n*99|The manual walk below remains the fallback.\n 100|\n 101|Useful outcomes:\n 102|\n## DEFERRED.md\n 29|| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache...\n*30|| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |\n 31|| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`w...\n 32|| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version ...\n 33|| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred \"option 1\")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but no...\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n 127|   (Field-confirmed adapter shape: claude-spt declares\n*128|   `[env.CLAUDE_CONFIG_DIR] direction=\"read\"` with `~/.claude` fallback —\n 129|   adapter reads, never sets.)\n 130|2. **Install-dir-first resolution (REQ-INSTALL-11):** the program path resolves\n 131|   via `resolve_program_in_dir` like every other adapter-manifest program.\n...\n 219|   Adapter shape confirmed: `[env.CLAUDE_CONFIG_DIR] direction=\"read\"`,\n*220|   fallback `~/.claude`, adapter reads and never sets.\n## FAULT-MATRIX.md\n 27|| 8 | **Net disabled / endpoint bind fails at boot** | No WAN; net frames answer `enabled:false` / typed error | PTY hosting (daemon degrades to net-less broker) | Fix config, restart daemon | `broker.rs::dispatch_net_status` (None arm); `Daemon::run` degrade path |\n*28|| 9 | **Broker process restart (journal survives, conns don't)** | Live conns/streams lost | Effect journal (durable) | Deduped op whose resource is gone → typed \"retry with a fresh op_id\" error; brain re-dials; an `spt rc` operator viewport RECONNECTS on its own — centered banner, full establish re-drive inside a bounded window, then a plain-language give-up (REQ-RC-RECONNECT) <!-- [doc->REQ-RC-RECONNECT] --> | `broker.rs::dispatch_net_dial` / `dispatch_net_stream_open` restart arms; `rc.rs` reconne...\n 29|| 10 | **WAN msg feed replayed** (sender redrive, or receiver brain restart resubscribing from a stale cursor) | None — replayed records dedup | Spool state (each op exactly one row); already-delivered ops | Durable `wan_seen` op-id claims absorb the replay; spool path claims atomically with the row | `tests/wanmsg.rs::receiver_restart_replays_feed_without_double_delivery`; `spool.rs` wan unit tests |\n 30|| 11 | **WAN msg record forges its origin** (payload `origin_node`/`node` field) | None — forgery inert (unknown field) | Access-gate subject (handshake-proven `remote_id_hex`) | None needed (records carry no origin field by design — KH 7.5) | `tests/wanmsg.rs::wan_message_lands_exactly_once_under_transport_origin`; `wanmsg.rs::forged_origin_field_is_inert` |\n 31|| 12 | **Target brain killed mid-remote-attach** (operator typing into the dead window) | Pumping pauses until handoff | Broker-held QUIC stream, session + child, output log, effect journal; the operator's viewport | Successor re-serves the same stream from seq 0 (worst case): replayed input dedups at the PTY-write journal; re-transmitted output dedups at the operator's render cursor — every byte exactly once | `tests/attach.rs::attach_survives_target_brain_restart_exactly_once` |\n...\n 33|| 14 | **Stream lost mid-context-bundle-sync** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's context store (an incomplete bundle is never fetched — length-gated, scratch file deleted); the responder's store untouched | No resume protocol by design: re-pull fresh — the apply is ancestry-idempotent (an already-joined tip short-circuits; fetched tips quarantine under `refs/spt-sync/` so a partial fetch never clobbers `refs/heads/`) | `tests/sync.rs...\n*34|| 15 | **Partition: concurrent context writes on two nodes** | The file's auto-propagation pauses (conflict surfaced, not propagated) | BOTH versions — local file untouched on each node + the other's version as a durable tracked artifact (hazard 6.6); every other file keeps syncing | Elected reconciler (Active instance's node; lowest-id fallback) runs one bounded Psyche turn → merged write `join(vA,vB)+bump` dominates both parents → propagates as a plain accept, clears artifacts subnet-wide | `test...\n*35|| 16 | **Reconcile turn fails** (harness absent / timeout / garbage or empty output / 6.5-suppressed write) | That file stays in conflict-holding state | Both versions (artifacts + local file) — exactly where they were; the store, the sync loop | Retry at the next sync/activation; a fresher direct write re-merges against the new local version | `spt-live reconcile.rs::failed_turn_preserves_everything` / `suppressed_write_preserves_artifacts`; `turn.rs::empty_stdout_is_an_error` |\n*36|| 17 | **Registry ambiguity during reconciler election** (partition: two nodes both see Active / no Active anywhere) | Two textually-different merges of the same pair may both mint | Neither version is ever lost — merged writes carry joined vectors, so the two merges classify **concurrent** and re-surface as a new conflict pair (detected, not silently last-wins) | Deterministic tiebreak bounds the storm (lowest node id among Actives / non-Offline holders); the re-surfaced pair reconciles on the next pa...\n 37|| 18 | **Compromised relay node serves a tampered update artifact** (bytes corrupted at rest after the relay's own verification) | That one pull ends `Rejected(ArtifactMismatch)` at the puller | The puller's staged release + running binary (nothing staged, nothing applied — no code path from wire bytes to the cache except through `plan_verified`); the rest of the subnet pulls from honest peers | Pull from another peer; the per-node gate (REQ-UPD-2) runs at **every** hop, so one poisoned node never re-p...\n 38|| 19 | **Non-conforming peer offers a rollback/expired/off-channel release** | None — rejected pre-fetch, zero artifact bytes move | The puller's version (monotonic floor holds); the wire (no transfer ever starts) | None needed — the offer-then-fetch shape gates metadata before bytes (REQ-HAZARD-UPDATE-ROLLBACK) | `tests/propagate.rs::rollback_offer_is_rejected_before_any_fetch` |\n 39|| 20 | **Untrusted node queries for the staged update** | None — refused by not offering (fail-closed, the up-to-date shape: learns nothing, not even whether a release exists) | The staged release; the serve loop (one refusal, no state) | None needed; pair the node to entitle it | `tests/propagate.rs::untrusted_origin_gets_no_offer` |\n...\n 43|| 23 | **Psyche replies with no inbound sender to answer** (alarm-fired turn emits a reply) | That one intent drops with a typed outcome (fail-closed — never broadcast, never guessed) | Every perch (zero deliveries); the notify leg (independent) | The Psyche's next answered turn replies normally | `psyrelay.rs::reply_without_target_is_dropped` |\n*44|| 24 | **Live-Psyche driver loses stdout** (the 7.3 null-stdout shape: detached/`Stdio::null()` regression or a turn that produced nothing) | That turn fails loudly (`TurnError::EmptyOutput`); nothing relays | The relay boundary (no silent success — indistinguishable-from-bug shapes are errors); the spool | Fix the driver / retry the turn | `psyrelay.rs::null_stdout_driver_fails_the_guard`; `spt-live turn.rs::empty_stdout_is_an_error` |\n 45|| 25 | **Runaway Psyche dumps an oversize body** (> 64 KiB in one intent) | That intent drops with a typed outcome | Delivery substrate (never asked to carry it); other intents in the same turn relay normally | None needed (legitimate long bodies chunk at delivery — T3 `EVENT-PART`) | `psyrelay.rs::empty_and_oversize_bodies_drop_typed` |\n 46|| 26 | **Notif feed replayed whole** (re-presented rows after loss/restart — the push-feed's normal recovery) | None — the semilattice join no-ops (`Unchanged` per row, nothing written) | The spool (no duplicates, no state regression) | None needed: replay IS the recovery protocol (full-row re-presentation, no delta bookkeeping) | `tests/notifsync.rs::notif_spools_converge_over_the_wire_and_dismiss_replicates` (replay leg); `notifsync.rs::feed_converges_two_stores_and_dismiss_replicates` |\n 47|| 27 | **Concurrent dismiss + surface on two nodes** (the same notif dismissed on A while B marks it seen/surfaced) | None — the writes commute through the join (OR/union/max); both survive the bidirectional exchange | Both stores converge to identical rows; the dismiss latch (a stale undismissed copy can never un-dismiss) | None needed (no conflict exists to surface — every field is monotone) | `notifsync.rs::concurrent_dismiss_and_surface_commute_across_the_feed`; `spt-store notif.rs::merge_is_idem...\n## FLAKE-LEDGER.md\n 16|| 7 | `spt-daemon::attach attach_survives_target_brain_restart_exactly_once` | 1× kitsubito (2026-06-16 run 27595180782 attempt 1, M11-W3) | **TIMEOUT, not an assertion fail** — SLOW>60>120>180>TERMINATING, nextest TIMEOUT at the 240s cap (`attach.rs:600`). A heavy multi-process brain-restart-exactly-once test on the slow shared kitsubito box under concurrent load (n1-gate on the same runner + a parallel `ci.yml` docs-publish run firing simultaneously). W3-INDEPENDENT: Windows green end-to-end; ALL tu...\n*17|| 8 | 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). H...\n 18|| 9 | `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 b...\n*19|| 10 | `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: gre...\n 20|| 11 | `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...\n 21|| 12 | `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-...\n 22|| 13 | `spt::bin cli::tests::probe_all_cap_batches_into_windows` | 1× hfenduleam local gate (2026-07-06, stack matrix @0f08fa9 leg-3 rerun) | fast unit (0.52s natural) FAILED at 1.7s under the 1614-test full-parallel pool — probe-batching window assert with a timing component starved under load; subsystem disjoint from the triggering stack (F-2 reporting / E-2 grid title / picker rider). Isolated ×5: green, 0.52–0.55s. Same night as #12 — the full-parallel local matrix under a busy box is the com...\n## GATEWAY-LIVENESS-DISPATCH.md\n 6|At the SAME instant, for the SAME pid-alive locally-hosted gateway (mobile-gw, status field ABSENT in info.json, pid alive):\n*7|- **human `spt endpoint list`** → **ONLINE** (renders local perches via `roster::enumerate`, roster.rs:72 = `is_perch_alive` WITH the pid-fallback: status-absent → `is_process_alive(pid)` → alive).\n 8|- **`spt endpoint list --json`** → **Suspended** (renders the endpoint via `resource_projection` over the registry snapshot, cli.rs:2855 → `EndpointRowJson.status` = the registry row status).\n 9|- flynn confirmed mobile-gw IS advertised in all 3 subnet registry files (NOT a re-advertise gap — his earlier \"empty by_id\" was a walker error, retracted).\n 10|\n...\n 13|## Key source facts (already traced)\n*14|- `advertised_status` (registryhost.rs:822-833) DOES feed `effective_rest_state` the pid-fallback `is_perch_alive` (line 825). So the DAEMON's advertised value should be Active for a pid-alive gateway — the divergence is NOT the advertise computation.\n*15|- So the `--json` Suspended comes from the projection/rendering path dropping the pid-fallback the human local-roster applies. **Candidate sites to pin (STEP 1):**\n 16|  1. `resource_projection` (spt-net registry.rs:566) — does it re-derive/filter liveness (e.g. `is_registry_entry_alive`, status-based: liveness.rs:152, which for a status-ABSENT row = not ONLINE||UNBOUND → stale/offline) and DOWNGRADE the row to Suspended/offline, ignoring the advertised Active? (its test `resource_projection_filters_hidden_and_offline` @registry.rs:1821 says it DOES filter on liveness.)\n 17|  2. OR the registry ROW's advertised status is genuinely Active but the `--json` render maps it wrong.\n 18|  3. OR the human path applies a `reconcile_self_owned` local-truth override for locally-owned endpoints that the `--json` path skips (roster.rs:52 mentions reconcile_self_owned overriding stale subnet gossip).\n 19|- **DEFECT A (separate, CONFIRMED, stands):** `establish_perch`'s build (spt/src/api/startup.rs:214-302) carries `cwd/controllable/adapter/read_env` forward from the prior record but NOT `rest_state` → a re-bind writes a fresh InfoJson that WIPES the wake intent (flynn's tick11 rest_state:active vanish).\n*20|- **DEFECT B (RECLASSIFIED to LATENT — do NOT make it the primary fix):** the `status=online` write in `cmd_listen` (startup.rs:486-491) is gated on `live_capable` (manifest `[session.psyche_init]`); a gateway has no psyche_init so it never writes status=online. BUT the pid-fallback is DESIGNED to cover an absent status and DOES for the roster + advertised_status. The operator's ready_agent counterexample proved this: ready_agents work because claude-spt declares psyche_init (live_capable-by-manifest),...\n 21|\n 22|## The fix (after STEP-1 pin)\n*23|**Reader parity:** `endpoint list --json` must report the SAME status for a locally-hosted endpoint as the human `endpoint list` does — i.e., honor the pid-fallback / local-owned truth (`roster::enumerate` / `reconcile_self_owned`) so a machine consumer never sees a different liveness than a human. A pid-alive locally-hosted gateway must read the same (ONLINE/Active) on both surfaces.\n*24|- Fix at the pinned site (most likely: apply the local-roster / self-owned reconcile to the `--json` local-endpoint rows, OR make `resource_projection`'s liveness for a locally-owned row honor the pid-fallback instead of a status-only check).\n*25|- Do NOT reach for DEFECT B (forcing status=online for gateways) as the fix — it risks the seed-#5 orphan-listener falsely-ONLINE amplification; the reader-parity is the correct, minimal fix. (If STEP-1 shows the cleanest fix genuinely is establishing status for a relay-holding listener, flag it back to me before taking it — that's a design call, not a silent choice.)\n 26|- **DEFECT A:** preserve `prior.rest_state` on re-bind in `establish_perch`'s build (same carry-forward discipline as cwd/controllable).\n 27|\n 28|## REQs (mint at build-start, activate-don't-pre-fail)\n## GATEWAY-RCA-STEP1.md\n 20|row whose id matches a LOCAL (self-hosted) perch row, it **overrides the subnet\n*21|row's status with the local roster ground truth** (the `is_perch_alive` pid-fallback,\n 22|`roster::enumerate` → `liveness.rs:136` → status-absent + pid-alive ⇒ ONLINE).\n 23|\n 24|**So for a locally-hosted, pid-alive, status-absent gateway whose snapshot row is a\n...\n 42|Note: `advertised_status` (registryhost.rs:822) DOES compute Active via the\n*43|pid-fallback, so the daemon's *live* advertise is Active — consistent with your\n 44|trace. The divergence is that the `--json` reader shows the **persisted snapshot**\n 45|row (which can carry a stale/crash-time Suspended) without the local-owned reconcile\n 46|that the human/picker reader applies. Reader parity is the fix, not the advertise.\n...\n 52|(`roster::enumerate` / is_local), override its rendered status with the local\n*53|`is_perch_alive` pid-fallback truth (mirroring `reconcile_self_owned`). Result: a\n 54|locally-hosted endpoint reports the SAME status on human `endpoint list` and\n 55|`--json`. Minimal + does NOT touch the advertise or force status=online (avoids the\n*56|seed-#5 orphan-listener false-ONLINE amplification you flagged).\n 57|\n 58|`REQ-HAZARD-BIND-REST-STATE-CARRY` (DEFECT A, confirmed, independent): preserve\n 59|`prior.rest_state` on re-bind in `establish_perch`'s build (spt/src/api/startup.rs\n## KNOWN-HAZARDS.md\n 18|- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\n*19|- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).\n 20|- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\n 21|- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\n 22|\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...\n 116|- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n*117|- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n*118|- **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 119|- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n 120|\n 121|### 4.4 Deferred rows survive poll drain\n...\n 183|- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\n*184|- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\n 185|- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\n 186|- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\n 187|\n...\n 252|- **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*253|- **Invariant:** spt-term resolves the program ITSELF before handing it to `CommandBuilder`, bypassing portable-pty's `which`. A bare name is searched over `PATH` × `PATHEXT` (whose default order already prefers `.EXE`/`.COM` over `.BAT`/`.CMD`), then an extensionless fallback. A non-PE target is wrapped in its interpreter: `.cmd`/`.bat` → `cmd.exe /d /c <path>`, `.ps1` → `powershell -NoProfile -File <path>` (the wrap args precede the caller's args); a real executable spawns directly; an unresolvabl...\n 254|- **spt-core mapping:** `spt_term::winprog::{resolve_for_pty, resolve_in}` (the pure PATHEXT-precedence kernel + the Windows env wiring), wired into `spt_term::pty::PtySession::spawn_program_in`. Unit: `resolve_in` precedence (`.cmd`-over-shim, `.exe`-direct, explicit-extension, path-order, passthrough) [`winprog.rs`].\n 255|- **Source:** field diagnosis 2026-06-16 (operator dogfood, `claude-spt:ccs` bringup) — doyle.\n 256|\n...\n 259|- **Failure:** `atomic_write_bytes` was `fs::write(tmp)` + `rename(tmp, path)` with no `fsync`. The rename's directory **metadata** is journaled durable, but the tmp file's **data blocks** are still in the page cache. A hard reset (power loss, forced reboot) between the two flushes lands the rename but loses the data → the file reappears at its **full length filled with NUL**. Field incident: after a machine restart `owlery/hall-a/info.json` was 360 bytes of all-NUL (the nested psyche record 125 bytes ...\n*260|- **Invariant:** for records whose loss is **unrecoverable**, flush data to stable storage **before** the rename publishes the name — `File::create(tmp)` → `write_all` → `sync_all()` → `rename_with_retry`. Then a crash yields the complete old file OR the complete new file, never a NUL husk. (A file of correct length that is ALL-NUL is the diagnostic tell of this class — a non-fsync'd write caught by a hard reset.) **Durability is SCOPED, not blanket.** `fsync`ing *every* atomic write serializes...\n 261|- **spt-core mapping:** `spt_store::atomic::atomic_write_bytes_durable` / `atomic_write_string_durable` are the opt-in durable siblings; the default `atomic_write_bytes` / `atomic_write_string` stay non-durable. Durable callers are exactly `spt_store::info::write_info` (the perch record — the hall-a surface), `spt_store::nodeid` (the node seed — corrupt is node-bricking, never regenerated), and `spt_daemon::machineid` (minted-once machine id — a NUL husk would silently re-mint a different id). Read...\n 262|- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; blanket-fsync perf regression pinned + scoped by the gate rig (doyle), same date.\n 263|\n...\n 266|- **Failure:** three readers each collapsed a **corrupt** (present-but-unparseable) `info.json` into their fail-open ABSENT default, so a NUL-wiped perch (5.13) read as permanently live: `is_perch_alive` returned `true` (unreadable ⇒ interim-alive), `advertised_status` then saw alive + no resting record ⇒ `Active`, and the daemon self-gossiped that Active row every round (epoch 173k+). Result: `hall-a`, dead since a machine restart, showed ONLINE in `spt whoami` and on every remote picker.\n*267|- **Invariant:** CORRUPT ≠ ABSENT. A record that EXISTS but stays unparseable across the retry budget is a **destroyed** record — never a live endpoint. `is_perch_alive` reads corrupt ⇒ **not alive** (ABSENT keeps interim-alive parity — the ONE case that stays true); `advertised_status` then lands corrupt in the cold arm ⇒ **Suspended**, never Active/Dormant. Readers that decide liveness/status must branch on the tri-state (present / absent / corrupt), not an `Option` that fuses the last two.\n 268|- **spt-core mapping:** `spt_store::liveness::{read_raw_state, is_perch_alive}` (the tri-state kernel) and `spt_daemon::registryhost::advertised_status` (cascades to Suspended off the alive fix). `list_self_perch_ids` deliberately still lists a corrupt dir by existence — that is the *visibility* the local-roster fix (counter-39 #3) relies on, not a liveness claim. Candidate sibling seam `is_registry_entry_alive` (corrupt hosted row falls to a daemon-pid probe) is parked — it does not produce the ONLI...\n 269|- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; sequel to the v0.17.0 W4 presence-truth fix (cold ⇒ Suspended) which a corrupt perch bypassed via false-alive.\n 270|\n...\n 272|### 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file)  `[REQ-HAZARD-ATOMIC-TMP-COLLISION]`\n*273|- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `Bind...\n*274|- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind t...\n 275|- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\n 276|- **Source:** RCA 2026-07-01 (doyle gate rig) — write-trace side-channel + `cmd_bind` step probes pinned `PROBE cmd_bind establish ERR: Io(os error 2)`; explains the counter-39 gate-1 blanket failure, the dummy_harness ONLINE-race DIAGs, and the nondeterminism. Lesson: a widened timing window doesn't create a race, it reveals one.\n 277|\n...\n 403|- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop...\n*404|- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a heal...\n 405|- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the ...\n 406|- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.\n 407|\n...\n 431|- **Failure (Windows):** `spt endpoint run` autostarts the daemon (`ensure_running` → `detached_no_inherit`), which INHERITS the launching terminal's Windows Job Object. Windows Terminal / VS Code place the shell AND every descendant in a Job with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; closing the tab drops the job's last handle → the OS reaps the daemon + its broker-spawned ConPTY harness subtree. The `rc` pump detaching should end ONLY the viewport; the harness must keep running + stay re-attachable...\n*432|- **Invariant:** both daemon spawn paths request `CREATE_BREAKAWAY_FROM_JOB` (0x0100_0000), best-effort with an in-job FALLBACK on `ERROR_ACCESS_DENIED`(5) / `ERROR_INVALID_PARAMETER`(87) so a breakaway-denying job NEVER regresses the spawn. **A real job CAN deny breakaway** (proven: the cargo/CI runner's own ancestor job ACCESS_DENIES it), so efficacy against the real terminal is UNKNOWN until measured. **int stage = OPERATOR MANUAL ACCEPTANCE, not CI** (doyle ruling 2026-06-18): the runner sits inside ...\n*433|- **spt-core mapping:** `daemon.rs::detached_no_inherit` + `deelevate.rs::create_with_token` (the breakaway flag + fallback). Units: `detached_no_inherit_falls_back_under_a_breakaway_denying_job` (no-regression — the shared runner exercises the fallback directly), `breakaway_spawn_escapes_a_kill_on_close_job` (the OS escape mechanism — self-SKIPS where the ancestor job forbids breakaway). Unix: the daemon's `setsid` session-detach already keeps a closing terminal's SIGHUP off its children (guard test...\n 434|- **Source:** v0.12.0 real-harness defect (operator) → v0.12.1 L1 @5ae68f8; doyle design+int ruling 2026-06-18.\n 435|\n 436|### 7.11 A dead PTY child + a dropped operator pump must NOT wedge the broker for other clients  `[REQ-HAZARD-ATTACH-WEDGE]`\n...\n 505|- **Failure (F-019 post-mortem):** the v0.11.0 path raw-injected `payload+\\r` into the spt-hosted PTY whenever no working translation binary handled an inbound message — none declared, spawn-failed, FAULTED (commit-deadline miss / binary death), or its inject-worker channel dropped — AND acked `delivered=true`. But a bare `payload+\\r` does NOT submit on a modern TUI (Claude Code): the message was TYPED into the harness but never sent, a silent pseudo-delivery reported as success. That silent degrade-...\n*506|- **Invariant:** spt-hosted idle delivery is translation-binary-ONLY (ADR-0022 amendment). `dispatch_endpoint_input` with no working binary replies `endpoint_injected_envelope(ep, delivered=false)` and writes NOTHING to the PTY; the caller (`try_broker_inject` → `cmd_send`) reads `delivered=false` and falls through to `deliver::send` = SPOOL (poll-fed, never lost), reporting an honest `QUEUED`, never a confident-but-false `SENT`. The failure is LOUD (`ENDPOINT_INJECT:<ep>: no working translation binary...\n*507|- **Boundary — what this does NOT cover (the STATE-vs-transient precision):** the guarantee is the steady FAILED STATE — once a binary IS faulted/absent/worker-gone, subsequent deliveries spool (`faulted` is MONOTONIC: set once, never respawns, so the state is reached deterministically). It does NOT cover the FAULT-TRANSIENT: a delivery that lands in the worker's commit window — BEFORE `event_rx` is dropped / `faulted` is set — can be optimistically enqueue-acked (`delivered=true` the instant `ev...\n*508|- **spt-core mapping:** impl `broker.rs` `dispatch_endpoint_input` (no-working-binary → `delivered=false` + loud log, no `input.enqueue`; the `input` writer is no longer resolved from the sessions table) + `build_translation`/`fault_translation` (`None`/fault now MEAN spool); unit `msg.rs` `endpoint_injected_envelope_carries_delivered_both_ways` (the `delivered=false` spool signal round-trips); int (real broker+PTY) `tests/broker.rs` `endpoint_keyed_inject_without_binary_spools_not_pty` + `tests/inject...\n 509|- **Source:** v0.14.3 (ADR-0022 amendment @1eaeef1, operator-ruled; doyle-scoped, the F-019 follow-up). CHANGE-5 (`cli.rs` honest report) verified a NO-OP — the spool fall-through already reported `QUEUED`, never `SENT`.\n 510|<!-- [doc->REQ-HAZARD-IDLE-SILENT-NONDELIVERY] -->\n 511|\n...\n 515|- **Failure (F-023, RCA @ `f023-f024-wan-idle-starvation`):** a message that has already landed in a node's spool — WAN-arrived, or locally spooled-while-active — stranded INDEFINITELY on an spt-hosted, relay-LESS endpoint whenever the adapter's hooks went quiet (an idle session with no user turns). An spt-hosted perch has no `api listen` relay to wake it, and NOTHING in the daemon re-offered a spooled message once the arrival moment passed — delivery silently depended on the adapter happening to c...\n*516|- **Invariant:** the daemon DRIVES delivery of an already-spooled message on the events IT owns — never on hook cadence — for a relay-less spt-hosted endpoint. TWO daemon triggers feed ONE shared spt-hosted inject leg (`spt_daemon::inject::try_spt_hosted_inject`, translation-binary-ONLY): (1) **WAN ingress** — `receive_wan` injects via the live binary BEFORE the spool fallback (`[REQ-WAN-SPT-HOSTED-DELIVERY]`, leg 1); (2) **the ACTIVE→IDLE edge** — `drain_idle_window` (fired by `api state idle`...\n 517|- **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 518|- **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).\n 519|<!-- [doc->REQ-HAZARD-DELIVERY-STARVATION] -->\n...\n 523|### 7.24 A delegated live adapter apply must NEVER report success without swapping, and the live-update seam must use ONE parent-aware adapter matcher  `[REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP]`\n*524|- **Failure (F015B / BUILD-F015B-APPLYMATCH):** a live adapter update to a PROFILE-COMPOSITE endpoint (`--adapter cc:ccs`) silently no-oped — `LIVE` then `DONE` printed, but the version-of-truth stayed OLD. TWO defects on one seam. **(D1, matcher skew):** the broker's `dispatch_adapter_apply` filtered sessions by EXACT `s.adapter == req.adapter`, but a `--adapter <adapter>:<profile>` endpoint stores the COMPOSITE `cc:ccs` (set at `cmd_endpoint_run` → `launch_harness_brokered_in` → `SpawnReq.adapter...\n 525|- **Invariant:** (1) ONE shared parent-aware matcher `spt_runtime::profile::adapter_parent_matches(session_adapter, parent)` (`split_option(session_adapter).0 == parent`) is used at EVERY live-update comparator — the CLI live-gate, the broker apply-filter, AND `select_endpoints_running_adapter`; NO exact `==` against a record name at any live-update seam (a profile-composite endpoint always resolves to its parent). (2) Once the apply is delegated the daemon owns the WHOLE apply: the CRC swap runs UNCON...\n 526|- **spt-core mapping:** impl `spt-runtime/src/profile.rs` `adapter_parent_matches` wired at `cli.rs` `adapter_has_live_endpoint` + `broker.rs` `dispatch_adapter_apply` session filter + `broker.rs` `select_endpoints_running_adapter`; `broker.rs` `dispatch_adapter_apply` swaps unconditionally (empty-affected early-return removed). unit `profile.rs` `adapter_parent_matches_on_parent_not_composite` (matcher truth table) + `broker.rs` `select_endpoints_running_adapter_filters_dedups_sorts` (extended with a `:...\n 527|- **Source:** F015B (operator escalation, doyle RCA code-verified end-to-end 2026-07-02; todlando build). ADR-0025 amendment records both invariants.\n...\n 534|- **Invariant:** every adapter-manifest program path resolves against the adapter install dir BEFORE PATH (REQ-INSTALL-11, `resolve_program_in_dir`) — at the harness session spawn (`harnesshost::launch_harness_brokered_in`: the idle-translation binary AND the session program), the W3d live-update RESPAWN (`broker::read_translation_path`), and the notif command (`with_install_dir`, both the daemon `notif.rs` and the api `reporting.rs` render paths). **Diagnosis rule:** before any deep behavioral analysi...\n*535|- **Source:** F-019 (v0.14.2); root confirmed on real claude-spt (perri + doyle). The raw-inject FALLBACK itself (degenerate no-binary floor) is unchanged by F-019 — its removal (message stays SPOOLED + poll-fed, never raw PTY inject) rides a separate ADR-0022 amendment.\n 536|<!-- [doc->REQ-INSTALL-11] -->\n 537|\n 538|---\n...\n 541|- **Failure (F-024C/F-024D, ENLYZEAM field + clean-room repro 2026-07-02):** `authenticate()` (auth.rs) gates `api poll`/`state`/`boundary` on `proof.session_id == info.json.session_id`. If ONE boundary rotation is LOST — the departing session dies (crash / tab-close) or its `/clear`-era `boundary` call never lands — the perch stays PINNED to the dead sid, and every id-scoped hook call thereafter AUTH_REFUSES *including `boundary` itself* (the very call that would re-pin presents the NEW sid). A perm...\n*542|- **Invariant:** `authenticate()` gains a DEAD-OWNER fallback — when the caller's sid MISMATCHES the pin **AND** the perch's recorded owner pid is DEAD (`proc::is_process_alive == false`), ACCEPT the caller's sid and RE-PIN (rotate `info.json.session_id` via the locked `mutate_info` RMW + a LOUD `SESSION_REPIN` stderr line). Same trust model as `establish_perch`'s conflict gate, which already allows a rebind exactly when `owner_alive == false` (an orphaned perch accepts a new LOCAL owner). This is **AD...\n 543|- **spt-core mapping:** impl `spt/src/api/auth.rs` `authenticate` dead-owner-mismatch branch (`read_pid` → `proc::is_process_alive` → `mutate_info` rotate + `SESSION_REPIN` log); unit `auth.rs` `pinned_to_dead_sid_mismatched_poll_repins` (AUTH_REFUSED → drains+re-pins, RED pre-fix) + `live_owner_mismatch_still_refuses` (the `/clear` live-rotation squat guard, unwidened) + `token_auth_path_unchanged` (correct token wins first, no rotation).\n 544|- **Source:** F-024C/F-024D (perri field RCA on ENLYZEAM + doyle code-confirm 2026-07-02; the wedge latches on `/clear` even in a CLEAN env — the env-corruption domino was sufficient but not necessary). COVERAGE: dead-owner re-pin rescues CRASHED/DEAD owners ONLY; live-pid rotation is correctly refused without departed-session proof. Cross-refs ADR-0032 (the 3-layer rotation-credential model) + `[REQ-BOUNDARY-ROTATION-CREDENTIAL]` (layer 1, the adapter/harness-contract half).\n 545|<!-- [doc->REQ-HAZARD-SESSION-PIN-WEDGE] -->\n## MANIFEST.md\n 15|- **Config knobs** (pulse period, echo-commune window, route-guard window, daily-refresh) — spt-core **global settings**, with optional **per-endpoint override** for endpoints with unique needs. Not per-adapter.\n*16|- **Cross-adapter fallback** (psyche/echo invocation falls back to another adapter on rate-limit) — a **node-wide setting**, not a manifest field, because it references *multiple* adapters (under spt-core, `ccs` is its own adapter, not a binary-swap within one). Adapter-agnostic IDs make this safe — an endpoint's Psyche can run under a fallback adapter temporarily.\n 17|- **Plugin-cache / version-trampoline layout** — none of SPT's concern. A CC-plugin adapter updates via `claude plugin update` (delegated command).\n 18|\n 19|<!-- [doc->REQ-MANIFEST-1] The sections below are the authoritative schema; the\n...\n 93|reads = []\n*94|can_inject = false                 # no hookSpecificOutput schema → sentinel/relay fallback\n 95|```\n 96|\n 97|### `[session.<role>]` — outbound session role templates\n...\n 115|[session.psyche_resume]\n*116|command = \"claude -p --resume {session_id} --agent owl-psyche --model sonnet --fallback-model opus --output-format json\"\n 117|env_remove = [\"OWL_SESSION_ID\"]\n 118|recursion_guard_env = \"SPT_ECHO_COMMUNE\"   # set on summarizer children so their own hooks bail\n 119|keys = [\"session_id\", \"parent_session_id\", \"psyche_context_file\", \"subnet\"]   # the psyche keys spt-core fills\n...\n 178||---|---|---|\n*179|| `{node}` | this node's **advertised label** — the label store value (OS-hostname default, re-checked at daemon startup), OS-hostname fallback when no store label is known | **SINGLE-TOKEN fill only** — tokenize-then-fill cannot produce a space-carrying argv element, so a composite display name like `<id> @ <node>` stays adapter-shim territory (`{node}` complements the shim for tokenizable args, e.g. `--remote-control {id}--{node}`, it does not replace it). **Caveat:** the daemon-side lifecycle reso...\n 180|\n 181|<!-- [doc->REQ-MANIFEST-NODE-KEY] -->\n 182|```toml\n...\n 220|direction = \"read\"\n*221|value = \"~/.claude\"                # FALLBACK when the var is unset (the base harness)\n 222|```\n 223|\n 224|- **Allowlist, not the whole env** — only the vars you declare `direction = \"read\"`\n...\n 226|- **Resolution order at digest time**: the value **captured** from the ambient env\n*227|  at bind (the relocating profile) → the directive's **`value`** fallback (the base\n 228|  harness) → **omitted**, so the `{VAR}` fails to fill and the digest diagnoses\n 229|  \"not set\" rather than resolving a wrong path (the same skip-diagnose posture as a\n 230|  missing `[digest]` source).\n 231|- **`~` expansion** — a leading `~/` (or bare `~`) in the captured value or the\n*232|  `value` fallback expands to the OS home (`USERPROFILE` on Windows, `HOME` on\n 233|  Unix). Any other `~` passes through verbatim.\n 234|- spt-core names **no** harness variable and bakes **no** default root — the\n 235|  adapter declares which var to capture and how its transcript path consumes it\n...\n 298|  adapter-declared **defaults** any consumer may override; spt-core ships\n*299|  fallbacks (`3` / `25` / collapse-on).\n 300|\n 301|Prove your extractor before shipping: `spt adapter digest-proof <adapter>\n 302|--sample <real-log>` runs it and prints the parsed records, the rendered digest,\n...\n 364|- **`{\"commit\":true}` is the mandatory sequence TERMINATOR, required for EVERY `{\"type\":\"event\"}`.** While a sequence is in flight spt-core buffers the live `spt rc` controller's keystrokes (the *inject floor*) and applies the emitted commands atomically; the trailing `{\"commit\":true}` releases the floor and flushes the buffered controller input *after* the sequence. The submit keystroke is NOT the terminator (a choreography may keep typing after `{\"key\":\"enter\"}`). **An empty response is a protocol viol...\n*365|- **Commit deadline + miss tolerance (F029 C-1, ADR-0022 amendment).** If no `{\"commit\":true}` arrives within the **commit deadline (5 s, `INJECT_COMMIT_DEADLINE`)**, spt-core flushes the buffered operator input (never stranded) and re-spools that message once — but a SINGLE miss does NOT fault a healthy binary (it is preserved; the next event delivers through it). Only **3 consecutive** missed commits, or a real binary death, faults the binary — and a real fault gets a **bounded eager respawn** (a h...\n 366|- **Known keys today:** `command` (preferred) and the deprecated `path`. spt-core does **not** `deny_unknown_fields` here — a newer adapter declaring a future key against an older spt-core parses fine (the unknown key is ignored), so the lifecycle-binary contract degrades gracefully.\n*367|- A minimal binary just emits `{\"text\":payload}{\"key\":\"enter\"}{\"commit\":true}` with no choreography (spt-hosted idle delivery is translation-binary-only since v0.14.3 — no raw-inject fallback).\n 368|\n 369|### `[session]` watched dirs (file-drop)\n 370|```toml\n...\n 499|repo = \"user/repo\"\n*500|message = \"Run `/reload-plugins` in any ongoing sessions.\"   # the static fallback notice\n 501|\n 502|[update.post]\n 503|command = \"{adapter_dir}/claude-spt post-update\"   # REQUIRED, non-empty; install-dir + adapter-static subst\n...\n 580||---|---|---|\n*581|| 7 CC hooks | hooks.json → `owl.exe <subcmd>` | `[hooks.*]` table; `Stop.can_inject=false` drives the echo-gate fallback |\n 582|| `cc`/`cc <id>` launcher | hand-written shell wrappers | `[adapter] shortcut_basename = \"cc\"` → the picker bakes `cc-<id>` (spt-core default is `spt-<id>`) |\n 583|| Psyche go-live gate | *(declaration only — never spawned)* | `[session.psyche_init]` present ⇒ LiveAgent |\n 584|| Psyche turn (the driven role) | `claude -p --resume <psyche-own-uuid> --model sonnet …` | `[session.psyche_resume]` + `{session_id}`/`{parent_session_id}`/`{psyche_context_file}`/`{subnet}` |\n...\n 586|| Echo-commune | `claude -p --model haiku --effort low …`, `OWL_ECHO_COMMUNE=1` guard | `[session.echo_commune]` + `recursion_guard_env` |\n*587|| `ccs` fallback | tier-2 binary swap | **node-wide cross-adapter fallback** (not manifest; `ccs` is its own adapter) |\n 588|| Transcript read | `.claude/projects/<enc-cwd>/<uuid>.jsonl` | `[history]` (fetcher or locate_normalize) |\n 589|| Session digest | extract `.claude/…/<uuid>.jsonl` → `{role,text,tool,ts}` | `[digest].extractor` (own seam, ADR-0019; not `[history]`) |\n 590|| Commune/signoff | `.claude/<id>-commune.md` watched | `[session].commune_dir/signoff_dir`; filenames fixed |\n## MESH-RECOVERY-TRIAGE.md\n 79|   discriminator required.\n*80|5. **Migration preserves fallback.** The W2 rebuild touches invalid rows","truncated":true,"truncatedBy":"bytes","totalLines":430,"totalBytes":64888,"outputLines":295,"outputBytes":51131,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":430,"totalBytes":64888,"outputLines":295,"outputBytes":51131,"shownRange":{"start":1,"end":295},"nextOffset":296}}},"isError":false,"timestamp":1784247658332}}
{"type":"message","id":"20ac8e1b","parentId":"596f4c28","timestamp":"2026-07-17T00:21:02.871Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing Redispatch milestone terms**"},{"type":"toolCall","id":"call_UBqWKATwe3YBphpdKOz6GePa|fc_0996e78cf50e3160016a59756d25488199b606ee084a85e317","name":"glob","arguments":{"i":"Mapping current RCA reports","path":".claude/reports/2026-07-16*/**/*","hidden":true,"gitignore":false,"limit":200}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":19585,"output":86,"cacheRead":143872,"cacheWrite":0,"totalTokens":163543,"reasoningTokens":33,"cost":{"input":0.09792500000000001,"output":0.0025800000000000003,"cacheRead":0.071936,"cacheWrite":0,"total":0.172441}},"stopReason":"toolUse","timestamp":1784247658402,"responseId":"resp_0996e78cf50e3160016a59756ac7808199b1bc99a3786341fe","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a59756c1a208199bb679885c55bc78a","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXVtTFfSsGdkK3qFgA4W1fq4nsHY99oCQeotCofVtCQuHtN1uC5AqzACc_JEgKGMS9SpPAP-6kwPMhr6TwJ2LmF6oz8YOM2pnnhnHYmcs3xrpbBm0jQqEZpcJTiRnm2zSY9qTqDq9g9zXrCFdqNRPlfYNyXeYT319HC0rRmZaOW0bB2xZg-yjhUskKU1A41NiolQsONd6vPIGiEYIr5e4kKiCYIvwWZ8zU8MFeMpiea9E5NN6JNVlVuoiS2ccjuhU_CrqMcwv37G2QFLjH1Z_-XX00kg-xY398s2EJrOAY9zk2077UdixJG0NESio1gLWWWs0s32B0QGPVI137Cba4N6wFw7uwEp1lagmfm2QpjhWdkvwtElQcLJuo7zWvJNcIBn9qvxw-o5mtSBg1VBqg22ALb8K9GI4yvaFONMwVl2E1qGZKz8vkVz01VCLZElSMKm0C6KDaeU7ONaYiugu831jltQujMOWC53LcvDYUPrnIyNnyEHijdVv20acLbnTa0okEDoz3Bl6GBySeaea6SzIcIOX72_rfTTzeZtUN3RtZJv6WLy-xMQly-UllWFNZlXrZeaA_YUjE0AJLeIL3QiQlbQH_9aCscJJHT09n44XDuS5ky6i0BwfXhIrLoG1Z8x0vbhCeVUfhjtOXD8olXcCpAIsSNj_Eko5f6ezcrdaEJcUnx5z2x6h75SqoJAgLH8616x7113VhAv-x6bUwA71ChcznXobD8Tn0OSg2gcAbjnXX85mLGWdPeWDiP5VXLqD82-mu_MKK2Z_N6CbFZSEz9PiajBSofvgScH70v_n1kyOyw6Cn-HaxeCiVbtPBFLfLmzniWa0SGcbxGZC-V06D9RMo6z_RPFpM_gvq6wz6MaG2ePTIS-bBP0Uccd0P--iIWCvbXEEYqyNZej4oQAvbOAR4hIzeELdPQi7lXEvP-usVu5ueIsj7BsTIDPcjj8-8yYj4fjyvt4ST3LZ5yUJqpu8MrooCe3GdkUTgV3KDhlFbU9rHU82hvp1MuZn5Ygf0H6hM2H_QhfxpYGX_DAT_XgJZ7ueEjgbOguZki-aEpxF0ot-7uQ10u61014BiHTDcBcTCU1FX3Nsyc-wHnH_j04CbZQ3ZXqocgJGAPMfx0=","summary":[{"type":"summary_text","text":"**Analyzing Redispatch milestone terms**"}]},{"id":"fc_0996e78cf50e3160016a59756d25488199b606ee084a85e317","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping current RCA reports\",\"path\":\".claude/reports/2026-07-16*/**/*\",\"hidden\":true,\"gitignore\":false,\"limit\":200}","call_id":"call_UBqWKATwe3YBphpdKOz6GePa","name":"glob"}]},"duration":4306.451799996197,"ttft":1689.315499998629,"contextSnapshot":{"promptTokens":163457,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_UBqWKATwe3YBphpdKOz6GePa|fc_0996e78cf50e3160016a59756d25488199b606ee084a85e317","toolName":"glob","startedAt":"2026-07-17T00:21:02.872Z","args":{"path":".claude/reports/2026-07-16*/**/*"},"intent":"Mapping current RCA reports"},"id":"121e09dc","parentId":"20ac8e1b","timestamp":"2026-07-17T00:21:02.872Z"}
{"type":"message","id":"1a55fe46","parentId":"121e09dc","timestamp":"2026-07-17T00:21:03.006Z","message":{"role":"toolResult","toolCallId":"call_UBqWKATwe3YBphpdKOz6GePa|fc_0996e78cf50e3160016a59756d25488199b606ee084a85e317","toolName":"glob","content":[{"type":"text","text":"# .claude/reports/\n## 2026-07-16-gaki-n-elevated-rc/\nhertz-corrected-report.md\n## 2026-07-16-redispatch-truth/\nhertz-refresh-rca.txt"}],"details":{"scopePath":".claude/reports","fileCount":2,"files":[".claude/reports/2026-07-16-gaki-n-elevated-rc/hertz-corrected-report.md",".claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1784247663005}}
{"type":"message","id":"a4134941","parentId":"1a55fe46","timestamp":"2026-07-17T00:21:07.483Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting `first_line` implementation**"},{"type":"toolCall","id":"call_IisYD535CY3Iq7MqeLbexpWe|fc_0996e78cf50e3160016a597572191481999b781fd7049850f0","name":"read","arguments":{"i":"Reading opener fallback exact","path":"crates/spt-daemon/src/dispatch.rs","selector":"385-450"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":5398,"output":93,"cacheRead":158208,"cacheWrite":0,"totalTokens":163699,"reasoningTokens":52,"cost":{"input":0.026990000000000004,"output":0.00279,"cacheRead":0.079104,"cacheWrite":0,"total":0.108884}},"stopReason":"toolUse","timestamp":1784247663204,"responseId":"resp_0996e78cf50e3160016a59756f5b4881999a16bb1be15a88ac","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a59757093e8819991a05b0e84c58da6","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXVy53dkPEtEOrH9p3otwY7kb7oDilNr3erTw5N8y9MWigy_xPj7QLsaea_aS_eKscNZQZrJ_zdc89n2PIv3V9vMppr0Pf5vPrywRdHr2VOa0mQpEAJLWkknqBO6GB_Uj9YxHMb4qLj_n7fHazUddVLolI2DUYJ7KJPL1jhc4ze-ikHDI_lSCvhGrKL7p2biwUodLOHDz38fsKSl8xJTpN4X_UulVWgFhcAQKIU9otWY5ePQzlsCjoJ4LH35h4_m_gULcAShK-FgCnKAGUZ33duh9a8NE8EqBfUbhJXn8TNWutyjN_w1GqP690In-19NwQTVMQqcaO2Eso9QgW-kSj0U1wOVlhcaLGZkSKq4vZhdz-lfFxnpb0foqb6M6v5QS9TYxw39BmtqZbfZHmjbzFOwuHND8GiyTRgvuhFarlUbII01rJMaZqVrouEl2OJl9qNwnSr_IMJk9nbSDnupZmiclxKm_T0R8EvM5m1bowkzchKLHRN1S-zZ7JTQeKrBHxJN0ZhuEZ1wKAeuZHFtCZsgOV6-YrgWb7HmFORr6hhKYJegPBsdQT7pXchcXQw56qzwbtdoUTa7LmpzGUs7BezkNQSIHQBd5P_e046lCStY5tS0Kp80Qmxno27J1TJ19QSgvyPnio_fP6gjyD8k1s60v3jeyffIFvIVyGoD-H4bencghHArxwju6xGH_jL57nxac5oQEu2y1ZNus2sUUpaSMJzcZrDw7JGmH-0g24goX7a3RLK3-P8MLFc2AQfsrV_p05gxHutLEdJML_8zeCmALHINWM9nWo7KkljLXLEs4_Sx1ZY4v9-l51um8xHw4ShhRS3iWl-BD1hfFv167JERkr0EbPZVXNl-zXlvbfPI7O2McRUy1JHsQpzrlf3QV5UQzxQ-E3SKjedk3cH9ZwMVRXoNxhBV5EExvYIlxISZjJv2cr2-gUTRZAm2UjXYK5FhC2RCgoTduZFUQjzOlytnMJTRmlIKr7idpI3nbuUaAkAewzvjXKCA-qdsTOKS5YceZK98x8WldbvWiyFm9oZvJEbOpXWdQstEPQ5yvi9jhzq5Ok4rB4JDcLOcq3gRM8w_-7MchrfA3sTSwJU93lmQR1wQDitYLDSu4TXz5nzk675vdpjhruVimiWzAekntdj-RL0nNUTT_UFsX6VjgCJFYgBglqy_wizFdleXOzLsNGRZ-w9ixztYY2AP8EVGb_q77xQIzVZc9Umou94tu_VemA==","summary":[{"type":"summary_text","text":"**Inspecting `first_line` implementation**"}]},{"id":"fc_0996e78cf50e3160016a597572191481999b781fd7049850f0","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading opener fallback exact\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"385-450\"}","call_id":"call_IisYD535CY3Iq7MqeLbexpWe","name":"read"}]},"duration":4275.489100001752,"ttft":1568.2422000020742,"contextSnapshot":{"promptTokens":163606,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_IisYD535CY3Iq7MqeLbexpWe|fc_0996e78cf50e3160016a597572191481999b781fd7049850f0","toolName":"read","startedAt":"2026-07-17T00:21:07.483Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading opener fallback exact"},"id":"f032cb84","parentId":"a4134941","timestamp":"2026-07-17T00:21:07.483Z"}
{"type":"message","id":"87ab6f2e","parentId":"f032cb84","timestamp":"2026-07-17T00:21:07.559Z","message":{"role":"toolResult","toolCallId":"call_IisYD535CY3Iq7MqeLbexpWe|fc_0996e78cf50e3160016a597572191481999b781fd7049850f0","toolName":"read","content":[{"type":"text","text":"fn now_ms() -> u64 {\n    std::time::SystemTime::now()\n…\n        .map(|d| d.as_millis() as u64)\n        .unwrap_or(0)\n}\n\n/// The stream's first NDJSON line, restart-durable (ADR-0038 B,\n/// REQ-STREAM-OPENER-DURABLE): read the broker-pinned opener fact first —\n/// it survives any ring roll — and fall back to the ring peek only when the\n/// broker predates the verb (KH-2.3 N-1 window). `Ok(None)` = unclassifiable\n/// (terminal); `Err` = transient (the claim requeues bounded).\nfn first_line(\n    broker_name: &str,\n    stream_id: u64,\n    floor_seq: u64,\n) -> io::Result<Option<serde_json::Value>> {\n    match connect(broker_name).and_then(|mut b| b.net_stream_opener(stream_id)) {\n        // Pinned: classify from the durable fact. A pinned line that does not\n        // parse is terminally unclassifiable (same contract as the peek path).\n        Ok(Some(bytes)) => return Ok(serde_json::from_slice(&bytes).ok()),\n        // The broker holds the stream but no opener is pinned yet (first\n        // newline still in flight, or the capture gave up bounded). The\n        // next_seq guard proved bytes exist, so treat as transient — the\n        // bounded claim retry re-reads it shortly, and a stream that never\n        // pins goes terminal when the budget is spent (no hot loop).\n        Ok(None) => {\n            return Err(io::Error::other(format!(\n                \"stream {stream_id}: opener not pinned yet\"\n            )))\n        }\n        // Older broker (unknown verb answers error) or the row vanished —\n        // fall through to the ring peek, the pre-ADR-0038 path.\n        Err(_) => {}\n    }\n    peek_first_line(broker_name, stream_id, floor_seq)\n}\n\n/// Peek the stream's first retained NDJSON line on a throwaway brain\n/// connection (the pre-ADR-0038 fallback — subject to ring eviction, kept\n/// only for the older-broker window). Subscribes from `floor_seq` (the oldest\n/// retained chunk) so a rolled ring replays instead of tripping the brain's\n/// gap check.\nfn peek_first_line(\n    broker_name: &str,\n    stream_id: u64,\n    floor_seq: u64,\n) -> io::Result<Option<serde_json::Value>> {\n    let mut brain = connect(broker_name)?;\n    brain.net_stream_subscribe(stream_id, floor_seq)?;\n    let mut buf: Vec<u8> = Vec::new();\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                buf.extend_from_slice(&bytes);\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                // Finished without a complete line: classify what we have\n                // (a line without a trailing newline before EOF still counts).\n                return Ok(serde_json::from_slice(&buf).ok());\n            }\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n            _ => {}\n        }\n    }\n}\n\n[Showing lines 384-453 of 1034. Use :454 to continue]"}],"details":{"truncation":{"content":"        .map(|d| d.as_millis() as u64)\r\n        .unwrap_or(0)\r\n}\r\n\r\n/// The stream's first NDJSON line, restart-durable (ADR-0038 B,\r\n/// REQ-STREAM-OPENER-DURABLE): read the broker-pinned opener fact first —\r\n/// it survives any ring roll — and fall back to the ring peek only when the\r\n/// broker predates the verb (KH-2.3 N-1 window). `Ok(None)` = unclassifiable\r\n/// (terminal); `Err` = transient (the claim requeues bounded).\r\nfn first_line(\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    floor_seq: u64,\r\n) -> io::Result<Option<serde_json::Value>> {\r\n    match connect(broker_name).and_then(|mut b| b.net_stream_opener(stream_id)) {\r\n        // Pinned: classify from the durable fact. A pinned line that does not\r\n        // parse is terminally unclassifiable (same contract as the peek path).\r\n        Ok(Some(bytes)) => return Ok(serde_json::from_slice(&bytes).ok()),\r\n        // The broker holds the stream but no opener is pinned yet (first\r\n        // newline still in flight, or the capture gave up bounded). The\r\n        // next_seq guard proved bytes exist, so treat as transient — the\r\n        // bounded claim retry re-reads it shortly, and a stream that never\r\n        // pins goes terminal when the budget is spent (no hot loop).\r\n        Ok(None) => {\r\n            return Err(io::Error::other(format!(\r\n                \"stream {stream_id}: opener not pinned yet\"\r\n            )))\r\n        }\r\n        // Older broker (unknown verb answers error) or the row vanished —\r\n        // fall through to the ring peek, the pre-ADR-0038 path.\r\n        Err(_) => {}\r\n    }\r\n    peek_first_line(broker_name, stream_id, floor_seq)\r\n}\r\n\r\n/// Peek the stream's first retained NDJSON line on a throwaway brain\r\n/// connection (the pre-ADR-0038 fallback — subject to ring eviction, kept\r\n/// only for the older-broker window). Subscribes from `floor_seq` (the oldest\r\n/// retained chunk) so a rolled ring replays instead of tripping the brain's\r\n/// gap check.\r\nfn peek_first_line(\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    floor_seq: u64,\r\n) -> io::Result<Option<serde_json::Value>> {\r\n    let mut brain = connect(broker_name)?;\r\n    brain.net_stream_subscribe(stream_id, floor_seq)?;\r\n    let mut buf: Vec<u8> = Vec::new();\r\n    loop {\r\n        match brain.read_event()? {\r\n            BrokerEvent::NetStreamData {\r\n                stream_id: sid,\r\n                bytes,\r\n                ..\r\n            } if sid == stream_id => {\r\n                buf.extend_from_slice(&bytes);\r\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\r\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\r\n                }\r\n            }\r\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\r\n                // Finished without a complete line: classify what we have\r\n                // (a line without a trailing newline before EOF still counts).\r\n                return Ok(serde_json::from_slice(&buf).ok());\r\n            }\r\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\r\n            _ => {}\r\n        }\r\n    }\r\n}\r","truncated":true,"truncatedBy":"lines","totalLines":651,"totalBytes":3136,"outputLines":70,"outputBytes":3136,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn now_ms() -> u64 {\n    std::time::SystemTime::now()\n…\n        .map(|d| d.as_millis() as u64)\r\n        .unwrap_or(0)\r\n}\r\n\r\n/// The stream's first NDJSON line, restart-durable (ADR-0038 B,\r\n/// REQ-STREAM-OPENER-DURABLE): read the broker-pinned opener fact first —\r\n/// it survives any ring roll — and fall back to the ring peek only when the\r\n/// broker predates the verb (KH-2.3 N-1 window). `Ok(None)` = unclassifiable\r\n/// (terminal); `Err` = transient (the claim requeues bounded).\r\nfn first_line(\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    floor_seq: u64,\r\n) -> io::Result<Option<serde_json::Value>> {\r\n    match connect(broker_name).and_then(|mut b| b.net_stream_opener(stream_id)) {\r\n        // Pinned: classify from the durable fact. A pinned line that does not\r\n        // parse is terminally unclassifiable (same contract as the peek path).\r\n        Ok(Some(bytes)) => return Ok(serde_json::from_slice(&bytes).ok()),\r\n        // The broker holds the stream but no opener is pinned yet (first\r\n        // newline still in flight, or the capture gave up bounded). The\r\n        // next_seq guard proved bytes exist, so treat as transient — the\r\n        // bounded claim retry re-reads it shortly, and a stream that never\r\n        // pins goes terminal when the budget is spent (no hot loop).\r\n        Ok(None) => {\r\n            return Err(io::Error::other(format!(\r\n                \"stream {stream_id}: opener not pinned yet\"\r\n            )))\r\n        }\r\n        // Older broker (unknown verb answers error) or the row vanished —\r\n        // fall through to the ring peek, the pre-ADR-0038 path.\r\n        Err(_) => {}\r\n    }\r\n    peek_first_line(broker_name, stream_id, floor_seq)\r\n}\r\n\r\n/// Peek the stream's first retained NDJSON line on a throwaway brain\r\n/// connection (the pre-ADR-0038 fallback — subject to ring eviction, kept\r\n/// only for the older-broker window). Subscribes from `floor_seq` (the oldest\r\n/// retained chunk) so a rolled ring replays instead of tripping the brain's\r\n/// gap check.\r\nfn peek_first_line(\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    floor_seq: u64,\r\n) -> io::Result<Option<serde_json::Value>> {\r\n    let mut brain = connect(broker_name)?;\r\n    brain.net_stream_subscribe(stream_id, floor_seq)?;\r\n    let mut buf: Vec<u8> = Vec::new();\r\n    loop {\r\n        match brain.read_event()? {\r\n            BrokerEvent::NetStreamData {\r\n                stream_id: sid,\r\n                bytes,\r\n                ..\r\n            } if sid == stream_id => {\r\n                buf.extend_from_slice(&bytes);\r\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\r\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\r\n                }\r\n            }\r\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\r\n                // Finished without a complete line: classify what we have\r\n                // (a line without a trailing newline before EOF still counts).\r\n                return Ok(serde_json::from_slice(&buf).ok());\r\n            }\r\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\r\n            _ => {}\r\n        }\r\n    }\r\n}\r","startLine":381,"lineNumbers":[381,382,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1034,"totalBytes":3136,"outputLines":70,"outputBytes":3136,"shownRange":{"start":384,"end":453},"nextOffset":454}}},"isError":false,"timestamp":1784247667559}}
{"type":"message","id":"6a867a66","parentId":"87ab6f2e","timestamp":"2026-07-17T00:21:30.843Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Clarifying error fallback and latency timeline**\n\n**Modeling stale worker recovery and freeze timing**\n\n**Analyzing worker launch concurrency impact**"},{"type":"thinking","thinking":"**Inspecting stream worker timing and fallback errors**\n\n**Analyzing fallback retry delays in mixed-version setup**"},{"type":"toolCall","id":"call_MGVmuDfffzz6n4y1NBHel1s3|fc_0996e78cf50e3160016a597589b0608199be5191d3f3c6e817","name":"read","arguments":{"i":"Reading opener request timing semantics","path":"crates/spt-daemon/src/brain.rs","selector":"1435-1495,1525-1585"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":6276,"output":1019,"cacheRead":158208,"cacheWrite":0,"totalTokens":165503,"reasoningTokens":969,"cost":{"input":0.031380000000000005,"output":0.03057,"cacheRead":0.079104,"cacheWrite":0,"total":0.141054}},"stopReason":"toolUse","timestamp":1784247667576,"responseId":"resp_0996e78cf50e3160016a597573ed34819993fb3c89c3f4b9d7","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a5975752f0c81998d47c76a6809900b","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXWAL7eKEu52KBH-AtrfCnQVwVkPxd7U9ChobjLwynrhoaKwdTIp9_8GT1wdW_Reg_uYXm4gV00XwEs6nc42K3uaEt2vqGG_MKghtDlkVrD17O2Mv6m_DunMqUpJN_gW-IG9vOhPyoN5Uh7cAO8NgKyDZtbi2KwQyT1ZROVDENC27JzjzbhaqEs6viXIBxvCxs6-y09fgel8h5_tVWxi1Q8LyldRaNAr8Dca0PU3kYYBmba4i2XXgdXtOpnfqTlAAAXCVkt8D54ifPWvEFc6oeADnEEsOSMnvLuzgbFYdUkGN4QzLvepPJ6uvN-BcYNvkgyqVAFRvGhnhr8jRefSP70Vc1zVNrQDdqb0DgwsXBuZlSmaAYNghL20hTEx1ZgbHmO1U6i1Wi7TIKYwXnacOxbJhYc0SjD03N7-ra1jMEpGsqqZGa1ViD3c77RAy8aOuyh3WSq7vrS19mtybWvvBDAXnzk2wcYhTNpItlAeWnnZ36g-0KKHVUhNyuhkDluhE_fOl9PdyMaY6nT-Iesr7ejseAC55lwxks86p7n7LuX9gOtlldB5NyujivYQV0B5JzyngIYepPbs-CBAejoH4Pk-nigIqc9YvfrLaeB4dYjfJCDNVwekqlPWSZu628l2D5AgL9YRmMA-AViyh2okW0sVTgoj4pjRI0aJTBFfs1gsBKGnMQ99_4rT6K9qy-JTUeZeTc2EVmBET_pEJWOPruqffAztunHmkDBkBHqAbBY8CvIujsmzEnW9tedEzxvfxjAQevOhxufRvPuOMku8y-hKuPofdd8Qwntk8eKY8gvdG2rbMAzT7c8MOoBMHilEYMsXvkSeFGnRB33bTAzRn5zgTcUhdzYZpmGpWxMWIVBqzUNDVuo-yTcvdQPzydAN-IRXaY9JGIYFFrMRSbQbLcsn6d_qDQAemQQU2sptC7kjPCl04BhB8gADfUkU51oVqS_uVJE9a1rSmFNqhABb3goYAtB9a667Ne-1LGsQxmYEBehb5EK2Wl83suTissOtO0iKdEOlc6LwpyQ72OQhShXExi2uLrVzY2cihBx5c5N63HH6jBLT92dtZFEtDC4NPqr7XqFbt3gh-RYOaODE1Wi0wXSXje1IpgQeq3uZzuxSIdJBBrGenWhjgnIL4_0Aa1cJQjM-H03cVE85Uj4RP6DrSEdwzCg8DobFJweC5OYmgtzpUmALJz_Ke6dYaLPH4WZLQeTcTFZPwgs0Ufn2XiwVTsWp1gUtpHL7W83i1USPtGY80Nxem2mxeSjqkXM3qQrFIhvKfhK6Qpa4K3THAThnsJht11ZlZiqSNB9R9xbMa275IUwV9uKrZ2BIP6OF2EdgRBvK1KJXRfBttD5_813nNzIkLFWE53Z7Fj9UMHHSIC7AEbmQpusyDe2ZInRfqR_9tunA7qbbfxiMGDzK3oQeER2-yq2uO4OYsN0SbpjzGzbh4CtGF2NeBhcQ5PU0eTPBob2KlUcUIbIVgdaOBnv4WbWH02QB-lparw5-GmDSE-_EkCoRKJ--fw8al5Hqa4EsjYiVdk6aX05U7Nd4t2380VLcSRAMa4Aa8KvXspFKq8RN06b3z1xoKl5eE0kXaFuc8Xp6XZDlDCK3EMZVJmJ7rn7Pf9nKExUYINgg74eMdi-CN6nzNljmtBg1Sqc08kQVHOifBcksUp5szHlj6OG69_p1DsY1tBu09aHxw4aK2M_8d0Zz7cuz6rS99jMukPJHL-XY_vy4ZPBa2rX5pC6oMigVgTDZo6oG15iYXfqHNURa1q-iAyfU57FGk-WCGSXsYZozuMXDYv-zUrh9QP_hxjMAIJbJJNRM5Dqn3Zn_ml9acgdkSAjOuwDeKPqJZZ1IViPIjCWggx5j73pnM5tVI7xCa5y2ncKYEkRawXs4wY3zmN7H3cHUvmNQDiqUNVorgz_tw357TcN-4CMQ46uL8PUR1zTmUrDyB0ec7u0J8HFrT4aTi0NE75E8wpq3lFaaG4lQ1BDbhDcJkShGXwnkRCqK-FvCMbvhBZYruqSklOaJYBay_MCs6vmCJP83NKOuNr9KcQ3kWHSMtiFMrbCCn6Q8FDAOalu-IBQOUIi9VD4GKzhp31nkT1HUrEC146GJ4oFsbssSyShbKvTHxUjbGvwgaK9aZBRr7s2jRkOfgCEssIvLceGXi6t5vEpI0vay9O04_RHzZT9efigHH0t9N1ihQ11UzX4njZ1BHyBrIQZODODDJGhFIgvCp0oIGpxlWGeZHtPksEspFo4PW5rDM6Keua-47n0dvtR49-ObXmDGrOQLSUp95kIry99eWv3rp-S9_4qphUdBfxpXRo2EDT-vj-yQSl1B2dIqVa0md4XZA4TauYjf4mr0uCFl8QqwWDUgtkpM3gMwISzIMBTPNMwDDJlyEx62VDn-LxIpRWB5FbGg_RhlzG-rLdYm0rh2RK0zWor6chzaJCehqIND2S9Hm36nxsMDepy_zH25iHBX1DixJGJvgQIKUYjjKBDXoyyX-3ji0F6iu7himXE1qg9ekeVnw5U_RuqccFxdps6BxbuFplKwlBrd8ewRlDym9nOO0iaNM1Fitm05taLzSIVEvSZwlpPQx4P4q4xL8gOyjjLZ8pByyvr7m2rVHdeyAYZ605-DOJ2xrxDjrKEIiRQXJetnqRWZt0aEELePkFvtdIaktY5ITS7_7SWResA1OQbQMa42uj3qypZnt2FZ7VapNTAOJAKT73boMYDuVkkYVl46l934XG4lRrqdJ_XoDLh6JFnExYvnXz8HEIBjHg3iREOJBkxITVswgZk0zABsanvDN3d1R6Qa7P4yMAjHnHD92zyvPEX3d2ocPTOhQo-ylIjVy1rLC1Smt7roxT2wphZZxZL_IEbi4crnuy8wbxN-tcMpJwKUmJjuzw22JKOEx5dnanlj1_q7bbaRoLqA1qbTI3zQgsJ9w6eKZ6duAuqsJV7zk0c98r7y3Q_PyXG5DFeLwf9mwkMB2TOqMkYQd8LQ0gA9cw0sSxhBXxfyCKvQwYh0s_w8B91msTXVBKS8yEIWCAaDfZPtz-akS7Gaz9TX0Oab-2tEZII-mzvrv-eCcjoCvB6giijb5zpZ-2A9HOuAehHEW06abQjpv6CbvJPWSYaYmsWYZsq8QygaQ03BZcY4lkALvOxZtoyHY36PTlalYq73US0v7Ou0sLELBjFao_vrdF4ViJ8HG_u6-g_gJYyBAveP32uzlVK2FY3Ab6wUsyJrhuj0PbGU6aatMUbVwVAqLIKyeWrIcILElm7qgjf_z_SrNVyM2sXqW36hbUQn3GaID-2DjudHwaaKZUhX_ly8bkG1EPF3b4uRGU4RoQTYNzvum5QIvaLuziHJyCD9BZCgQqCbpP4T2nCrO__ZfafdfjP3kluoFdOJJ4hSKlq8c8PjmzIBvDmbyBAT7W9GD_ua0Lp8bvfY9jbgseRQl0_ScI4L4om28yxfRjH_NqmEKddBAMzUDuBZbBeaSh9ILwRHg7y9tFCgEZh9lrROfMie9irpMe-iZMgo-ZwLk9S8_2WpQilmGRA046Bh0cwOK_MbJx925FqfA_ylhw0BpFJWVXM7nnmJ0WAJPaposD5LgVaw-AOeWfK2M2XWWcyYSl1uVT-KTM6ZqzyidGUuFvmagGmZI8PYW-fDA18tjud_pbHk0nVqaalgtAi8adCoPCViZlu29GgJjZfU7S9f0hLDuJIi3tjDLRSd30KdmfNbmQ8OUd2l_RPoW9iLfYGEl4PDc2KguICrr9j2Fm_8TP0gZiYgMgZcHGAMRBFONKHUMdfZ3z7BRlz4TPwBQqqi8tVcEFPKL_xBgHJ2JD6a1t28JGsUPrEP470lt__6xV-ecKA8RaA7WH4OVHLW-QErZZvwpOWnadp7HS8mjkWeKCeTnVJoaLzQE1AjJbnRTpqQ8vGj4ovFubbuU-9j7Xq79xhpldkboxa2V_3jtbIQGxmBDJs5hXCed8EbavURwb--qyGzTlavg0-27iKHGrMDzAryQB1mvI9oAM3i3RODESL5knm9PEjIQqw1ybBI8gYsClXS70nUStPdIqu_ntNfaYjN49mq_2TFhkyOUNz16Xfyn3wxyn-zu9BKZEifu30Qi21b1CtfFqjushLHqsY7PWAe1GYQTJwcPDrObfYHYidspbJf1aRUHt8a28-ShTQYPy1hAExuGEk_jPGuU5gjyiRGua5mH4IGPi0=","summary":[{"type":"summary_text","text":"**Clarifying error fallback and latency timeline**"},{"type":"summary_text","text":"**Modeling stale worker recovery and freeze timing**"},{"type":"summary_text","text":"**Analyzing worker launch concurrency impact**"}]},{"id":"rs_0996e78cf50e3160016a5975800c648199be5ba54a1e3e520b","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXWJVR_yLy6phQkYf3-iBmuxfg6PC1HhHaWATRYFb9jXS_Z-FweI9t0Eec135b9VnU04puEO-3BsquHSpBgOWTDaFWAz4sKGQBNcoSJDFhwwf9b9VKZc-_EC3qVTp1yoVx3J4ToeN4V-duRb5N8Wa9GQlpStMMuNQVvnsSzgW6QzeM8AjNhOY8icvwoH91TIo36t-ZO44yMAai0F0EFq1oKFRGjm5-mQV4_6Y4Tge6lPWouDQS2rbeJGmxKga85zQs_3V7gHDFRagtIe_TYrRxuXWcoUyM4RRzUku--9SgeukRTxGjMG8VP-y0bP4w3RjnRlK2AdgPEtaTgUj5ULBFd4fdro6I2ynOWmN0bfdC72U_oa5vvl1ilCDcbo7D2eomqm3j1Qtq7S8UU2Uo64GT4gaUhAzuXFQQYqGkwJ-1A6BYKXUpsvOOLUUS-zcAU0iorXEa3y1BiSQ6hZTl_hXgeVr02yReTB6huZ1faP8eNMofsIW3THy0_otqp3YT6wm-VdORRf4e7WdNARAewwpCXDN_jg-3rmSnSj24TELBWoainEfIye1jMmhWWZVNzb02RVNBnwN_syBYZbc1myJR_wy5tFy3eiae_2FQiZd8VgDQPEzVYTSpxVB-s7V_fNZEidQrDT_PZfBh3ELEI7ps_HtLXX09aKVtmRbC9GFnL_Jp9QqheUPADoQ9aTmqeAPr8-GeptUYNAYEhftGvMdurRG_eAx4KiybMa2iknA4p5FhJUjeLGARL7vU9TeWNAmDDVWNlaFUpgugSJattGenfdYbec72L9cTwcH4iV0gvuLcjFygl6CRSQaGorGrllP28Zl0_fEO6IotXqIBI_nbfqSRTB1Tnz4jMqlAJ1-e3Y0YKty3Fcax88Cqr81jhSEKlpdJsKAdKTpSsse5J6VwWavJQo_47tVjwAGP-AjkHvCssrP4_eoUWJC9WFIzEEbfblKYrT955bdYZ8ewzwzbYv5G5gwphrFzB7rM_2UsL-PgSrhEjrCgoyFAlflmq7OO-2i1XfqlfgVFUc78j4rS7Lxe8r4Uq53oS8ClO2CXiHed_aHBMNWGLa8_a_pRtAYdzPw3AFk5Y6-f3oD8Xjs0-d9mP07lnnF6ezrS4nPIFeNsItb_MaxZi5zpOZ4N1DvTUW1bOZm9_sASZ_ln-lO0jF3YA5LCy45yF9xFTakVz4BChT5_XAnhOh5sg4s7gqhuoXyNiFWauax5hYOYZrKhyL_nPY93BcUnk_5lySwVAA1qTQDQ3HyePTyrgr18e9FCm5xsVPi3kyqh8lz2amjcl1ubgfQyqDzVbZfHY4vCSRfUfCCobzgmg_FFGyD3VI7iK_e6Heya3s-3sb4wd7DH5IfHyRJYN4ZXwUyDpQE2705WcT0hMb1nifIQTicYEIJL8Tdk80_E0k2-rBA1Difxml5MbGGeRJOO8rvtUDxbbz9v1A4NhPghBx_usoSurFb4nywN9feOzRpx6-f80ePqyQc7UUmn1qV2aD1NgDcrR4AlegXwj0madOcngPEtW-N162QbNTRMO3g9BxjaeFT-ZQxyZeUs2EyAQnDhlEcxx8zSo0BX-G-n3y4be53RSqB6dcYfYUHtIOU3T3kqAj2cO4HDeeFq3QxHQAgsFlGn1M5XXsMAI0EEoiQt-m3ZO4vnXFZBNl14uuIp6rIS79GU3Csr6LQSBo1AfPy1tty84X-TgDMqjVEPuE8dit8V8_JcAvRMtMV5UwgjLX8_crluyKzJ3h_F3LehKXn5woYuFgWrtYaMW3naGjmNeUyjrd9JNEf2YLKRl0BdMbP5RloWPxUtiEjICXM9rcZOtdXokixsG9VzNrk-fSsl--TRnz61ARizBqOD8pPBlXx3xty1jfN3I07OTLnQtWslwnoxoXJJTPVZbzKKtccObj8r60HskUnHfeKr5hCV8lSwh0Bh_T8tbM2i8WOcEmc504WM4m_q870BJOvUk4G2zVJyqUGynbe2xFQcQOVIhLYIcSKiR2YkckS-sOW9uuC4PhxULpuxtMu-NfDqdRIt7GXDzkXevDfnPjN9SYg-UTFnjg0eiq1yxO1OjlNwwHe8tMKcP1zlcEr6DpzmHlKjFIXSW4bv84RXETLd7tqMW3nJZXXJfDsqea1XThcI3ICQQxK_Se08vAH5wlAHQvYLMuhnMIzBqSeZZSDR3SFQb-LunKc1ICtxOB7FEz-tD6hjQufhMesPu7ryvrbzigLgzarVJom0YvwvNbJAUSXd766Xq9A_EJxqKFM7BcFvEw-keSIiEbekuroi7OWG2pS_zss1Rlc3JKQYE8bg-h5RVT_elhJsLXgf0EyMn4zpYaAX_KJskipjDwfdGhEkb-Jpun5KAjjrCerzbgBZ2SAqyq0alQTShNnhKz03UHsQBInL-huMBgpU2pUMUQLGWFlWTSu8LS1iZ2snSJLz9p-UDiH_N9xS8RTib7RBSxBZ6gzEfmvRxmsQ3wKYtx1xJ_ega7B4OAULositxxz0-bXKu7bsvfXRHBkZ3S9KmufSP6-VfjQhLbHyLlMxgz-aeVeOf9Dw8EJJGg80izY4O1fLQfc1p_mIz8GIkJ3gzDFWDGr7JjbhtwpLXpaVj1kBT-iRjDMChAfzgN9RIQU2fxqQYvYrQIm8FsHJzoPbHv8tLcpk7lPikfH2j_AriwCkplsrMz59QrxD8DVoDiG2_6qwUu8jDGqGLlpmhs4q9JbfAdQmSGjBSg9ZSgTCnqVI6IPsE05kXzoTdWlcRFU_Z29Q5msYsfUo54SaeQIZyXqwCs_-yRKbGZ14Yz9qrwpk56xcRZwGAoPoaBs2SM_UbEWNEfcq8aKFWVO3PGAOzYbMu8mQOS4b8S06OxK7uyZ_8o_yGAbvetpSDxxJnSroqQNwVOJWG5dmqShP4SRz6FLl2lzAdCWbogQtHBFkC9f6e6uo37aDeSyFO82zPS6QmvAqUsu-IXWGt_LMnXGQyPi_IjYkDX1lWkwWXF2yC6P7LHciXkFENga6_j_hbYmnDr3PdqDvW57UEIN5cDTJAO5zTsyi4iXe4IlYlvw4C-WVfkruEv3z0U1gFdJdifN0wLoqpc9DsE8CPm9o5hxySb-0eaP2wCHiCC-kwSpJXEbOoNaq-myTShoPm2NdHet1vOB78JP2Q4Bx_0piLPF31iqz6QmaA2cSFu07eY8AxJG68ISAvkyrOq8WfLYAAn9rdY1krGvMhx2ewUDqlGVKf8nINSD3mWy2QuJ-8hJP4YDlo8_51TFaHl4ybLSaLTZCYbL4OVj1Gis-5Evr2ne4mf1ma46ycp8k9CNdQuLkgkiEMSKnw3cY-N3km61A9aqfuYLz8cbREE8aoDv-Ot8lxOV7ZV2e7-shk3GFLYjKTlbg8ANk6O8MgN25eq4d2NHZVVHR6D5lTNTDkdXrtU2a6ZDgYdCjvMF8c5wQuMj0jbVmHiDZyJem4FPk-XJdYuHDgtPsAfiqs90hGPT2QEieSJDhV9yhVruq0rULcGmwptCv8iaBwDcui4Kzjp2NqF576oGsn-bg3rB1YK-fiZvULDo-RilgszzwF7srSzLhIBiyKGwHJUJmt-fUuB7_PIgSNiWTA0LkPjLSrhq3xVrJlbEVWj5XITKeku_WauXWwbH7MfFH1c7KVWUIiXIeUJkzwA0H_Dcmkj0oGcJie26IZxWvZ2e1h4Ir3N-hY=","summary":[{"type":"summary_text","text":"**Inspecting stream worker timing and fallback errors**"},{"type":"summary_text","text":"**Analyzing fallback retry delays in mixed-version setup**"}]},{"id":"fc_0996e78cf50e3160016a597589b0608199be5191d3f3c6e817","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading opener request timing semantics\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":\"1435-1495,1525-1585\"}","call_id":"call_MGVmuDfffzz6n4y1NBHel1s3","name":"read"}]},"duration":23263.328100003302,"ttft":1608.6604000031948,"contextSnapshot":{"promptTokens":164484,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_MGVmuDfffzz6n4y1NBHel1s3|fc_0996e78cf50e3160016a597589b0608199be5191d3f3c6e817","toolName":"read","startedAt":"2026-07-17T00:21:30.843Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Reading opener request timing semantics"},"id":"0e08d540","parentId":"6a867a66","timestamp":"2026-07-17T00:21:30.843Z"}
{"type":"message","id":"6c9343cc","parentId":"0e08d540","timestamp":"2026-07-17T00:21:30.862Z","message":{"role":"toolResult","toolCallId":"call_MGVmuDfffzz6n4y1NBHel1s3|fc_0996e78cf50e3160016a597589b0608199be5191d3f3c6e817","toolName":"read","content":[{"type":"text","text":"    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {\n        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::NetStreams(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A).\n    /// The dispatcher calls this when a serve loop runs to completion, so no\n    /// future dispatcher generation rediscovers and replays the stream\n    /// (KNOWN-HAZARDS 7.41). Best-effort at the caller: an older broker\n    /// answers `error` (KH-2.3 N-1 window — retirement then waits for the\n    /// broker to cycle onto the new binary).\n    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n    pub fn net_stream_retire(&mut self, stream_id: u64) -> io::Result<bool> {\n        self.send(\n            KIND_NET_STREAM_RETIRE,\n            serde_json::to_value(NetStreamRetireReq { stream_id })\n                .expect(\"NetStreamRetireReq serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_RETIRED => {\n                    let reply: NetStreamRetired = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(reply.removed);\n                }\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// A stream's pinned opener line (ADR-0038 B) — the restart-durable\n    /// classification fact, read instead of peeking ring seq 0 (which a\n    /// bounded ring evicts under traffic). `Ok(None)` = the broker holds the\n    /// stream but no opener is pinned (no newline yet / capture gave up).\n    /// `Err` = no such stream, or an older broker without the verb (KH-2.3 —\n    /// the dispatcher falls back to the ring peek).\n    // [impl->REQ-STREAM-OPENER-DURABLE]\n    pub fn net_stream_opener(&mut self, stream_id: u64) -> io::Result<Option<Vec<u8>>> {\n        self.send(\n            KIND_NET_STREAM_OPENER,\n            serde_json::to_value(NetStreamOpenerReq { stream_id })\n                .expect(\"NetStreamOpenerReq serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_OPENER_REPLY => {\n                    let reply: NetStreamOpenerReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return match reply.opener_b64 {\n                        Some(b64) => decode_bytes(&b64)\n                            .map(Some)\n                            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)),\n                        None => Ok(None),\n                    };\n                }\n…\n            }\n        }\n    }\n…\n    pub fn request_brain_restart(&mut self) -> io::Result<bool> {\n…\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_BRAIN_RESTARTED => {\n…\n                }\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Deliver `bytes` to the PTY of the session hosting `endpoint`, by ENDPOINT\n    /// ID (REQ-SEND-SPT-HOSTED) — the endpoint-keyed inject a CLI `spt send` uses\n    /// to reach an spt-hosted endpoint (broker holds its PTY, no relay). The\n    /// broker resolves endpoint→session atomically and writes the bytes (a\n    /// pre-rendered `<EVENT>` envelope). Returns `(delivered, spool_deferred)`:\n    /// `delivered=true` = broker injected via translation binary; `spool_deferred=true`\n    /// = caller should spool deferred (active window), `false` = spool non-deferred\n    /// (idle window). When `delivered=true` the spool_deferred hint is false (no spool).\n    /// `native` (ADR-0028 W3): a `--prefer/force-native` send → the broker injects\n    /// via the translation binary regardless of the `.idle` activity gate.\n    // [impl->REQ-SEND-SPT-HOSTED]\n    // [impl->REQ-MSG-DELIVERY-AXES]\n    pub fn inject_endpoint(\n        &mut self,\n        endpoint: &str,\n        bytes: &[u8],\n        native: bool,\n    ) -> io::Result<(bool, bool)> {\n        self.send(\n            KIND_ENDPOINT_INPUT,\n            serde_json::to_value(EndpointInputReq {\n                endpoint: endpoint.to_string(),\n                data_b64: encode_bytes(bytes),\n                native,\n            })\n            .expect(\"EndpointInputReq serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_ENDPOINT_INJECTED => {\n                    let reply: EndpointInjected = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok((reply.delivered, reply.spool_deferred));\n                }\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// List the broker's hosted sessions (id + owning endpoint) — the inbound\n    /// dispatcher's session→endpoint resolution (D9-1).\n    pub fn sessions(&mut self) -> io::Result<SessionsReply> {\n        self.send(KIND_SESSIONS, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Sessions(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the"}],"details":{"displayContent":{"text":"    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {\n        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::NetStreams(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A).\n    /// The dispatcher calls this when a serve loop runs to completion, so no\n    /// future dispatcher generation rediscovers and replays the stream\n    /// (KNOWN-HAZARDS 7.41). Best-effort at the caller: an older broker\n    /// answers `error` (KH-2.3 N-1 window — retirement then waits for the\n    /// broker to cycle onto the new binary).\n    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n    pub fn net_stream_retire(&mut self, stream_id: u64) -> io::Result<bool> {\n        self.send(\n            KIND_NET_STREAM_RETIRE,\n            serde_json::to_value(NetStreamRetireReq { stream_id })\n                .expect(\"NetStreamRetireReq serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_RETIRED => {\n                    let reply: NetStreamRetired = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(reply.removed);\n                }\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// A stream's pinned opener line (ADR-0038 B) — the restart-durable\n    /// classification fact, read instead of peeking ring seq 0 (which a\n    /// bounded ring evicts under traffic). `Ok(None)` = the broker holds the\n    /// stream but no opener is pinned (no newline yet / capture gave up).\n    /// `Err` = no such stream, or an older broker without the verb (KH-2.3 —\n    /// the dispatcher falls back to the ring peek).\n    // [impl->REQ-STREAM-OPENER-DURABLE]\n    pub fn net_stream_opener(&mut self, stream_id: u64) -> io::Result<Option<Vec<u8>>> {\n        self.send(\n            KIND_NET_STREAM_OPENER,\n            serde_json::to_value(NetStreamOpenerReq { stream_id })\n                .expect(\"NetStreamOpenerReq serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_OPENER_REPLY => {\n                    let reply: NetStreamOpenerReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return match reply.opener_b64 {\n                        Some(b64) => decode_bytes(&b64)\n                            .map(Some)\n                            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)),\n                        None => Ok(None),\n                    };\n                }\n…\n            }\n        }\n    }\n…\n    pub fn request_brain_restart(&mut self) -> io::Result<bool> {\n…\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_BRAIN_RESTARTED => {\n…\n                }\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Deliver `bytes` to the PTY of the session hosting `endpoint`, by ENDPOINT\n    /// ID (REQ-SEND-SPT-HOSTED) — the endpoint-keyed inject a CLI `spt send` uses\n    /// to reach an spt-hosted endpoint (broker holds its PTY, no relay). The\n    /// broker resolves endpoint→session atomically and writes the bytes (a\n    /// pre-rendered `<EVENT>` envelope). Returns `(delivered, spool_deferred)`:\n    /// `delivered=true` = broker injected via translation binary; `spool_deferred=true`\n    /// = caller should spool deferred (active window), `false` = spool non-deferred\n    /// (idle window). When `delivered=true` the spool_deferred hint is false (no spool).\n    /// `native` (ADR-0028 W3): a `--prefer/force-native` send → the broker injects\n    /// via the translation binary regardless of the `.idle` activity gate.\n    // [impl->REQ-SEND-SPT-HOSTED]\n    // [impl->REQ-MSG-DELIVERY-AXES]\n    pub fn inject_endpoint(\n        &mut self,\n        endpoint: &str,\n        bytes: &[u8],\n        native: bool,\n    ) -> io::Result<(bool, bool)> {\n        self.send(\n            KIND_ENDPOINT_INPUT,\n            serde_json::to_value(EndpointInputReq {\n                endpoint: endpoint.to_string(),\n                data_b64: encode_bytes(bytes),\n                native,\n            })\n            .expect(\"EndpointInputReq serializes\"),\n        )?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_ENDPOINT_INJECTED => {\n                    let reply: EndpointInjected = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok((reply.delivered, reply.spool_deferred));\n                }\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// List the broker's hosted sessions (id + owning endpoint) — the inbound\n    /// dispatcher's session→endpoint resolution (D9-1).\n    pub fn sessions(&mut self) -> io::Result<SessionsReply> {\n        self.send(KIND_SESSIONS, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Sessions(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the","startLine":1435,"lineNumbers":[1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,null,1499,1500,1501,null,1517,null,1519,1520,1521,null,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs"}}},"isError":false,"timestamp":1784247690861}}
{"type":"message","id":"758dfc17","parentId":"6c9343cc","timestamp":"2026-07-17T00:21:57.747Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing cold start recovery discrepancies**\n\n**Examining retry logic and timeout behavior**\n\n**Clarifying worker retry impact on recovery timing**"},{"type":"toolCall","id":"call_GY6XM1JU0OildfEmsADplUYB|fc_0996e78cf50e3160016a597599dac48199b71112145777c565","name":"irc","arguments":{"i":"Clarifying regression scope and recovery","op":"send","to":"Main","message":"Scope correction: the stale-finished-Attach RCA is from the exact 2026-07-16 HFENDULEAM refresh/update incident with 4/5 frozen (`.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`, field bullets + causal steps 3–7; ADR `docs/adr/0038-broker-stream-lifecycle-truth.md`). But it does NOT supersede W1 retry/ANY-Err as explanation for a NEW deterministic 20–30s auto-recovery. Primary stale replay has no timer: `dispatch::run_dispatch_loop` starts all old/current workers; old `serve_attach` same-origin `attach_as` → `broker::OutputLog::resolve_subscribe`/`become_controller`, then replay EOF → `attach::serve_attach` `detach_session` → `broker::dispatch_unsubscribe`/`detach_if`. If stale worker wins last, freeze is indefinite until fresh rc; if active worker happens to finish setup last, recovery is automatic once the finite replay storm drains. Current W1 can make that ordering/timing: `dispatch.rs::first_line` catches **every** `net_stream_opener` Err (`Err(_) => {}`) then `peek_first_line`; `DispatchOutcome::Failed` transitions through 3 attempts (`MAX_DISPATCH_ATTEMPTS=3`, backoffs 500ms then 1s). Thus stale replay = ownership corruption mechanism; ANY-Err fallback/retry = plausible amplification/delayed-last-active-worker mechanism for 20–30s. Keep both unless logs map controller-replaced/attach events.\nExact log refs in RCA: generations 2/3 `BRAIN_RESUMED...7`, `BRAIN_NET_CONSUMERS_UP`; `DISPATCH:741 Failed(os233)`, `:3 Failed(fill whole buffer)`, `:4496`, `:4176`; low+current IDs prove broad historical rediscovery, not endpoint map. The delayed conn poison example is secondary: first `write-start` at 10:51:24 arms 15s watchdog, poison 10:51:40; idle gap before first write explains lateness, not a 20–30 recovery timer.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":7658,"output":1025,"cacheRead":159232,"cacheWrite":0,"totalTokens":167915,"reasoningTokens":505,"cost":{"input":0.038290000000000005,"output":0.03075,"cacheRead":0.07961599999999999,"cacheWrite":0,"total":0.148656}},"stopReason":"toolUse","timestamp":1784247690891,"responseId":"resp_0996e78cf50e3160016a59758b2d0481998ed4278ce63837b7","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a59758ce6588199864376e17c2e8141","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXWZBIY1BlfG0YilD6EVX0rwLBXS57wEtKLQsnkjXvSY9yfTFGmQIUUaURk8rK6pQIiSFtwKvwApVTeYO0QHv-pFatplU_vB9R5aXwp2ADw1mbSOGsjYDx8Zl2GeaAmdgQTbG6GH6oYYgFQvVMGGbBbVE_OhPqaqNXAveZs7V6PcJHJVa6b8iQC63yCaP2Q7UPsfHZ-s6k_y6p5m2g1unRecaw2vqu9EUnwh3DqFGsGjkRjgKViVZFYMVgpqxC7y-Rby0ZLcgywNRVYhS9H5pfmULMz1wrzznHtdKQtUZzZhE5Gy0mEd0hIFnlLlZtpfFsyvhsaL2AZz9KUW1YqMPLtbG57C4DjEOy7pjvyx4RuNmBgHuZPCKwBA2VTggG78az25bdo5w5Zij9fNJh6_dhiWS5ewrCqJ1H9bPBU5zQT5e64wioMV7oecGJD9pS6cioQp0rV2TKCc8Xo_44wX6DER5U-I1QSFvgxr95VSk5Q10JHIqoH0UvafyUljwKn-Xny8VuXcDUnWvQR3TaEW1N6xhrNZg9KwRfQOIlGatXttYXkNAZN-9gTtuKwDQnVS7wozhls7BtwNRds24Ixf8_B-XDvv9bSvxSBoOjq2doVRRjlBqfjDseKZtyRqq3F7EuRsJJZP-IaKzdVbs45LbXaVjELJ1ifQiFPd2kWKgXDUIzPsbCMMyVs38taiHDY9h0AF_F3SXScruhtojsQupPsdBZTieD-6rg1wJUU-tRiAetY2DMBwVuc3ig_H-0M-OcBE-ZKwW0o-W6Jpal7uOgb4hrCG1QLYfZQv8U5vqymEq_mJzcbfRcutMrwcEoieg8v3fwk-6lbNEF0NKg6M_H_tGoTb0ASIrpFY4U5w0e4qODsMFBcqAwSgR7WWqqkj6RiKtB45X3XlF7PISNVD-Kv4_fBoN8Y19k4Ap1ke9u747X5IBPQPwpc402wKt0agQZggkwrcblGxZJfljUuOggWOAFCOIEFVuLuirwKqPcf-1BAr_PU0IbiReeSdhoQk6yx4gHxOwhjd5_EjONwPZaDgV23x3IzcWa-i6kGScqFyMMvQQ103TaNx-8Xo4DtBInfYxLgmWGiiALuFUJiKMrGDQr2qhjfWJuRt45rdLWlbBtBBv8xraLq4A262X647lmBRlrmlNQvdIECjwl_hTyIjupjXjnhUzFfRlh3BcOhH4h9sM6IBHjlJydx79M0lVgIXAaFa5PzSD265JCN5kTvH0Bezb2AGM3KIGYnZC9jrrkiO0csDIlp1VLb4NguZ0LdWeGMuai2EpAgrC0sUBTAdG_dOKhzs3_G9zmtZHGfu-GgzgTQoikIw7hQBa12WDkeBizhXXqo_lz_pNDgxjmgy4h_qI8DdyL6HDFU3uXj2b6SwVJEA6577ofAfXHUl9YTnCA2J8qd7cWWoqkk6jS1gvCG9y1d8lNMZAH1lAsaowmbJaSjtktZKc6Q0b6F0DP2JTPm9C7B-8Wo-pe7bnMUq1Va9fx5dq4WLn8KNaI4HhPXoWCFHA8pf94C_4z9Davyo02n98StjnvgAcwMELKYT0KVNt-Om6l6eTP5pE08Q31NbgPJOOVLC_OGblZRTTXphRdusmXNcMNUHA3Z_rX-lIZTPMbdZdnMUImNz4A2WgB4f4W9IbObhhHmZXeh1bWB5fLVrz_E0WQD3pT5v6nVoecrzEVLSM23nVdQHRJBXK4TO4hvRNULZ3wFxVZCOwocx5MlKGrK_agfOSqVQb2BA3w6DnT5wg-md70Xhd2o27DWd5k9uafWJyeVzah3xuWlpjXYTXHsHVzeahufHV95CBmIWhibLP7Q8EAnzLupenBg0cShkg4bVBZrisRNfwXGTA5Ry4y90dEguCM1qxrZykoZ8tAg81H_vehRN9tVffUwKPbSYGEmOAIkygsgpH9uephQ29UBMUNiN0j8MXyYxc7MAMXf8ZYK8sp3CwlWeWqrounQuRMvEGKBgSx_7nGMLx5GGVPpImm19mIGyZ7MuWa9KXu8GMpBpm1h--RXgG86OhSJSzsWZns5Li-kIX1A7vVTS_yXD7SlQsQcRYTA0ipwPFh2rm7J6-fbhbM36LPzb9zAwwgrFqn-cHl-TxfpbDksTK3S77PGfIo5pwuiTJHy_zZhOq-3Jjf5Hm4cAso9OE6B4ESxDzSbA7OXDPTc5etQT7aDnjig-BLxq7ytpl8JVTQIYbP7Q0IW2_9eio3YsDOWck-wmSwKmWy2W9AJ-tpoHILFSfLAonH_3NgfcKx7YqdpCmNoE4kzn7Zn53bxcrAST72T4o6fnC5p4ezad7cXgXvedSgfTGlOBWiFpEFne37WHpzZyNAvRoKYyfLNkjZX6pPQPOgD54kELq5VMR8pwfv7qvsrNyxa-LwD1AsB7DtOZwMnF-MmUF2fgTpCnD-CvGJIIw90Kr_qKFyzzD9RBf8GlgFUXrjHMzb1VV0nXEgZ2wJD6wYxNmLkJiqMCFkYxmUDw-pUM8uDAj68iapHrfLgq3LKjPR80cEfmY4uiNXLeadYjXMdaYPCKSyRH7rib0DpEB9Er62r8DlXzOc0mEWMc7sQwpTropleC2psGRfJx9T3pyNlg6wOuIIj9Cn-s3E_cBhpb58mr5tRw-EuT8eN8DnB7875_4t5mqCEPsiBShkYFhJ0yM6AG8gj12Yd2ZZB4xSgW37cWCS1QOUGe6eOTDePXlnTsFazV3CD_xzb9GZ1wx0KT5noYLbJQH0JGXI_DCDwKaTKt0nIJNH9jENQ2xmSsrU0lJSDw5hBwurCmEwnzBfLbUbvlXEif0x8GgPqSYZSjA3vbjAf7-DN8-j6_MAgfui6V59kAgSCB--mVExakRM-cmMl8iQq-SqajRKDz8PFY1mFQlZnn7RYIdJfUX7U9O5ehB42qLz7A5w4Dm0Kyz3rqG50O9gnKIed8BXJBpiYpWoXyAlxMTrp2dyX14p3fBLIGkXZmzDJPS_b9blHGyv_z6_ut8YqzODLlqkFikZsiMOd0zLKW0zfhlqA1-GridK3whNgH3x-SengwzrXkoNEOe0DedxKOBfUawKTEFI_0GIKulTxGRn4TIXrt5noFGmYl2rxQIwhMs0drMgqsTEUBGh0EvE9Q3CbVhYqlksqnwS_zC7gFlbTbHhrzfKH-vk8Gi0yAxpSti0tX4CNrjCJrV-RI8vHugcT0obpaDWNlsN_01iTUReH3Tavaa4B-wOXs2_37KQUOiSwZdTY-tq7xE2hyQyaIywCDX2mbAgBn8ZmzmRLEEGgc1ZBSXAf2Faz0lq4RO_TNlrtFzoT0f1rmA4NH-zoKoXMBBppldXH8ki9EVXROLsL2pC8BhnxdMoDcGOZBPVO5Xx9K44jGhBzekrHfxT1BTjjiX73DUNBkl1-XMchVeLZ7vXuRkqUOwLQVJp5kdVKSykpkKkODhI-gKvJJIOgM3wST2BNAS69lYcQ_-ZbGXmZ4bjnDlrGOV3aM7d9FdLF42M25E-IxAZjYB4xJNG7pDQFFcxRnjYZHNsbZSZr74eoDWtPHdV_PlGMZkLy11OQz-gNCJ1DUFIRfnGhkJ6FrmV22tOO10gaQppB_eHBGe3xGac6sSsQwEg63OTxFBF_U3uaJiQp-Jc-oyZklm637djUNGGz_cTM3kleUecHdxhDe33AUykZecnm17CVVrE7prExjdBhvD9OCTw79lI6DIEa2QlApU1p0hP_iur6mft9Cxfboev8naH57CnEqn2zZAPp9jypMj6tAu61YCb5NwMZ7gzUzl2Csv8PV5B_2AIj4XHDcKpWeEhDT51z9zY_wEzJlzzp8iyYLjgiObNmqmp2UQ9DmWl-uQEt86T4-hgoM2qIEPClz31I8fb-zu8bPhcQrU-ozad-c0Ect6jyDMuC4rkLdx-hbqVyInGnkt1kaiINOT8UkIbPN8GGp9jlui6WdIvl7ltYaJp4UNoiGN_5JDbdpA3HWvhvO_JceDWyaRum0VkvukddQxAPwUjmQdlCdRlVLgTqFvPnVjin_L-Fz5q3fm3NPwgOqnWtmpONh83iHdaCxb0mFySF5vA==","summary":[{"type":"summary_text","text":"**Analyzing cold start recovery discrepancies**"},{"type":"summary_text","text":"**Examining retry logic and timeout behavior**"},{"type":"summary_text","text":"**Clarifying worker retry impact on recovery timing**"}]},{"id":"fc_0996e78cf50e3160016a597599dac48199b71112145777c565","type":"function_call","status":"completed","arguments":"{\"i\":\"Clarifying regression scope and recovery\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Scope correction: the stale-finished-Attach RCA is from the exact 2026-07-16 HFENDULEAM refresh/update incident with 4/5 frozen (`.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`, field bullets + causal steps 3–7; ADR `docs/adr/0038-broker-stream-lifecycle-truth.md`). But it does NOT supersede W1 retry/ANY-Err as explanation for a NEW deterministic 20–30s auto-recovery. Primary stale replay has no timer: `dispatch::run_dispatch_loop` starts all old/current workers; old `serve_attach` same-origin `attach_as` → `broker::OutputLog::resolve_subscribe`/`become_controller`, then replay EOF → `attach::serve_attach` `detach_session` → `broker::dispatch_unsubscribe`/`detach_if`. If stale worker wins last, freeze is indefinite until fresh rc; if active worker happens to finish setup last, recovery is automatic once the finite replay storm drains. Current W1 can make that ordering/timing: `dispatch.rs::first_line` catches **every** `net_stream_opener` Err (`Err(_) => {}`) then `peek_first_line`; `DispatchOutcome::Failed` transitions through 3 attempts (`MAX_DISPATCH_ATTEMPTS=3`, backoffs 500ms then 1s). Thus stale replay = ownership corruption mechanism; ANY-Err fallback/retry = plausible amplification/delayed-last-active-worker mechanism for 20–30s. Keep both unless logs map controller-replaced/attach events.\\nExact log refs in RCA: generations 2/3 `BRAIN_RESUMED...7`, `BRAIN_NET_CONSUMERS_UP`; `DISPATCH:741 Failed(os233)`, `:3 Failed(fill whole buffer)`, `:4496`, `:4176`; low+current IDs prove broad historical rediscovery, not endpoint map. The delayed conn poison example is secondary: first `write-start` at 10:51:24 arms 15s watchdog, poison 10:51:40; idle gap before first write explains lateness, not a 20–30 recovery timer.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_GY6XM1JU0OildfEmsADplUYB","name":"irc"}]},"duration":26855.596400000155,"ttft":2018.972600005567,"contextSnapshot":{"promptTokens":166890,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_GY6XM1JU0OildfEmsADplUYB|fc_0996e78cf50e3160016a597599dac48199b71112145777c565","toolName":"irc","startedAt":"2026-07-17T00:21:57.747Z","intent":"Clarifying regression scope and recovery"},"id":"7408b748","parentId":"758dfc17","timestamp":"2026-07-17T00:21:57.747Z"}
{"type":"message","id":"7df08e33","parentId":"7408b748","timestamp":"2026-07-17T00:21:57.748Z","message":{"role":"toolResult","toolCallId":"call_GY6XM1JU0OildfEmsADplUYB|fc_0996e78cf50e3160016a597599dac48199b71112145777c565","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"PtyControl","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784247717747}}
{"type":"message","id":"a0f02764","parentId":"7df08e33","timestamp":"2026-07-17T00:22:05.428Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Compiling structured test hypotheses and harness**"},{"type":"toolCall","id":"call_BdqhesD9YuMSOgsgqqY9ctMd|fc_0996e78cf50e3160016a5975ab82ec8199acc4acedac3de28e","name":"grep","arguments":{"i":"Listing controller viewer unit tests","pattern":"^\\s*fn\\s+[a-zA-Z0-9_]+\\s*\\(","path":"crates/spt-daemon/src/broker.rs","selector":"4700-5900","case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":6657,"output":247,"cacheRead":161280,"cacheWrite":0,"totalTokens":168184,"reasoningTokens":165,"cost":{"input":0.033285,"output":0.00741,"cacheRead":0.08063999999999999,"cacheWrite":0,"total":0.121335}},"stopReason":"toolUse","timestamp":1784247717795,"responseId":"resp_0996e78cf50e3160016a5975a61e9c8199be8d3945cbbee66d","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a5975a7f760819992c94d9c4b48bc54","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXWrZEPF76LHYsQ8zHzgR7QnY_RCVn6u9RkR2wTPzsVaKVPE5agInq8_NrbeB3URxPc6st1S_kt4WVp6H-dkYsn_lP43wiWd5k3o2tJPTveEqpnhYOIBHSzI7Wf-SGLtO5fpONKwooYiGfeuS1nxDrYCgA_yFFV9INJWgOgCLSZi5qy_hXjnrZ8rJUNelaboB0ZjF37Bb8qy0IxlEB_s5mihNXSEc0daL9e00aPWbFoGqleTB_SJn6AdmL4XnO1zl-GhGJH4VNhLfpM1UPlyF3251AAIS4t5zjraZqXB7h2dK5kQNUh9tdhbYUSugJFSawtRveqXG-9CQIlu68ZQtasL-gQbPsxcT8o6M2T597rtY7alP_6gi2sSkNQ3zpRO9sglHggcWAVbFeAPT79tFKkXWHFNg7nVEK-cBH61vn3mC5HBrKRzAyw4YW3mIGWhh5ss_6YaA33vjZnDMV5wjlQiGDoFe0b7jvOEY_OIu6ks2gYqEPnMRBskT6ZiDDzkLorESuzadSZUqMl1Ps38shd_bcFKLb0dha7PGXe5YURTd_WEbCGknHmcauVNm7hB_CybcdoDynDYp0odXRpMvoeIGqyDbxfFyDn5Kbdvbu4k9OmLH6HojVJCWSYFmWqHIzX6XQvPiFjObKp7SxMhvsVFpUno4EMce0rf0ahq4ikJesqk5UoxmG0KqFwYwELtjAJ4uCjzpuAi01hS98Hay1IfjYEmIeX1QiLmggp-JJRiyeI9UeeSEgO1ht0PYg-POm4sXnppb5GrJED9ZRor3nPg47kLSydxyOsCe_BRqSvAJHL_cMDRYteW_8DmbhGICQG1QrTAdxd_aTi0xfKrt876ru1l25OrKgwIxRkjAf1jet1nTQ4LbnVdbNaCNxI_FwyJBtqm2DDjJEaxAx-sGPEcfM03rz93Wwa8NduKYMBNEWuqeSXXc7zR1gsmSjeNduvsJcjrQcGGTfa9el4MtBbStZSJE00dndaPJHTlCRt0XAagNgd7Yp1S_6AUDXg0dj_q9KxX99agetnd1k-cK2sOw_yU8G-y3b2eHs9gpNQmCrSvl3x3J2ByWDbPp7KpInwT921QgHHC_ZXh4Skkpe635pgYV5uxR8Tyn470VVHXQsYMnvp5kI69bG7lmFCe4-YYJ4pVJGsb3mzWLA3lg8q1LWXbEla6qSgLI12fy_ahB3OLPV9Vw0HXa-VupSw_BLHC3_52J_4ees4osCAswBbsPVKmjkrcPWb_6zVBA-2mJYJWqsIM9LBaTUbn6GXTx1tubcwH2LaA_EGWKlKYqL5OoRRlhnpnqOjj4ElB94BCRme9w0xHHmAqAvkDRCNzozRYLWO9yYPAXVPImSY-H0CxBMowYe7EAEQKlqVSrifh1gWBE301jJ-a51qAsOBkqWEoz-dbOPifW6B8UD5KAD9C1sShYCOXTGnoy7KvyRK9zF9SXyX3H4z4b8HVqlJA47JUGxv6fcFoyjlgiTR9AgCHFdFCfJlrZAt_uyhSHBo6Tx1bHnzi25FbF-7MYslM9fXNRzYmkkjoW15_kwXJjhH5ZP07lT3P_c8rfFKcbjdrNJWRcTA8r3Mxpw5z3rZMTYe3tuL56rbFIIjzlZpECKnGK1Y-VThuWM7UPXOAmDyMJXGPCYMfAN73hqHVOvYsoba4ePwyXUfU4DOvX9g3-VMhVdYSQ7FL7Y4oXWEjVR5rFzEpvoMkPaemaKsiPUk1X_Ebyn8rhwm07gt4DGZ_8DUK52M2QcLRP5Wbm6HvHSuDQGy9vmiUKxRQ9HzR5MI44PhUc4HqKimmZKd-jLIca0V6znIVJzgJlPDHyPNo-ntVyWG0iUfodYs97FibqLJh0kS0d-2RoU-l_aOVUfbWwydFESdnwsdbP_ANhryevvpyH9ywG4XOkxzT--wv8BWfanfk","summary":[{"type":"summary_text","text":"**Compiling structured test hypotheses and harness**"}]},{"id":"fc_0996e78cf50e3160016a5975ab82ec8199acc4acedac3de28e","type":"function_call","status":"completed","arguments":"{\"i\":\"Listing controller viewer unit tests\",\"pattern\":\"^\\\\s*fn\\\\s+[a-zA-Z0-9_]+\\\\s*\\\\(\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"4700-5900\",\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_BdqhesD9YuMSOgsgqqY9ctMd","name":"grep"}]},"duration":7632.906500004232,"ttft":2161.299800001085,"contextSnapshot":{"promptTokens":167937,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BdqhesD9YuMSOgsgqqY9ctMd|fc_0996e78cf50e3160016a5975ab82ec8199acc4acedac3de28e","toolName":"grep","startedAt":"2026-07-17T00:22:05.428Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Listing controller viewer unit tests"},"id":"582587df","parentId":"a0f02764","timestamp":"2026-07-17T00:22:05.428Z"}
{"type":"message","id":"32de930b","parentId":"582587df","timestamp":"2026-07-17T00:22:05.433Z","message":{"role":"toolResult","toolCallId":"call_BdqhesD9YuMSOgsgqqY9ctMd|fc_0996e78cf50e3160016a5975ab82ec8199acc4acedac3de28e","toolName":"grep","content":[{"type":"text","text":" 4713|    #[test]\n*4714|    fn viewer_writer_emits_evicted_marker_only_on_an_evicted_close() {\n 4715|        // ── EVICTED close: flag true before tx drops → marker emitted. ──\n 4716|        {\n 4717|            let (send, client, _recv) = controller_socket_pair();\n...\n 4780|    #[test]\n*4781|    fn append_overflow_flags_the_evicted_viewer_for_skip_to_live() {\n 4782|        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 4783|        // A viewer whose live queue is depth-1 and which we NEVER drain (rx held,\n 4784|        // unread) — it cannot keep up with the drain. We do NOT use `add_viewer`\n...\n 4831|    #[test]\n*4832|    fn sessions_reply_resume_seq_defaults_when_absent() {\n 4833|        // Old-broker shape: no `resume_seq` key on the session object.\n 4834|        let old: SessionsReply =\n 4835|            serde_json::from_str(r#\"{\"sessions\":[{\"session_id\":7,\"endpoint\":\"e\"}]}\"#)\n...\n 4862|    #[test]\n*4863|    fn a_full_controller_does_not_starve_a_viewer_or_block_the_drain() {\n 4864|        let mut log = OutputLog::new(1, 1024, String::new(), (24, 80));\n 4865|        log.controller_epoch.store(9, Ordering::Release);\n 4866|        // A CONTROLLER whose 2-slot channel we NEVER drain (rx held, unread) — a\n...\n 4932|    #[test]\n*4933|    fn contiguous_advance_freezes_on_a_gap() {\n 4934|        let dt = AtomicU64::new(0);\n 4935|        contiguous_advance(&dt, 0);\n 4936|        assert_eq!(dt.load(Ordering::Acquire), 1, \"seq 0 (== cursor) advances to 1\");\n...\n 4959|    #[test]\n*4960|    fn advance_delivered_is_monotonic() {\n 4961|        let dt = AtomicU64::new(0);\n 4962|        advance_delivered(&dt, 0);\n 4963|        assert_eq!(dt.load(Ordering::Acquire), 1, \"seq 0 -> cursor 1\");\n...\n 4983|    /// the ORDER test; here the TEST is the consumer, draining `rx` deterministically.\n*4984|    fn test_input_writer(depth: usize) -> (InputWriter, Receiver<InputRecord>) {\n 4985|        let (tx, rx) = sync_channel::<InputRecord>(depth);\n 4986|        let w = InputWriter {\n 4987|            tx,\n...\n 5003|    #[test]\n*5004|    fn input_writer_preserves_strict_fifo_order_through_the_sole_writer() {\n 5005|        use spt_term::surface::{SessionSurface, SurfaceError, SurfaceSize};\n 5006|\n 5007|        /// A stub PTY write seam: appends every `write_input` to a shared Vec, in\n...\n 5010|        impl SessionSurface for RecordingSurface {\n*5011|            fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError> {\n 5012|                self.0.lock().unwrap().push(bytes.to_vec());\n 5013|                Ok(())\n 5014|            }\n*5015|            fn resize(&self, _: SurfaceSize) -> Result<(), SurfaceError> {\n 5016|                Ok(())\n 5017|            }\n 5018|        }\n...\n 5067|    #[test]\n*5068|    fn input_enqueue_drops_and_stamps_on_full_then_heals_on_drain() {\n 5069|        const DEPTH: usize = 3;\n 5070|        // Consumer is the test; it does NOT drain until we say so → the queue fills.\n 5071|        let (w, rx) = test_input_writer(DEPTH);\n...\n 5123|    #[test]\n*5124|    fn lock_floor_recovers_a_poisoned_inject_floor() {\n 5125|        let floor = Arc::new(Mutex::new(InjectFloor::default()));\n 5126|        // Poison it exactly as a panic-while-holding-the-floor-lock does.\n 5127|        let f = Arc::clone(&floor);\n...\n 5150|    #[test]\n*5151|    fn recover_reopens_a_poisoned_sessions_map() {\n 5152|        let sessions: Arc<Mutex<HashMap<u64, u64>>> = Arc::new(Mutex::new(HashMap::new()));\n 5153|        recover(&sessions).insert(7, 70); // a pre-poison \"session\" row\n 5154|        let s = Arc::clone(&sessions);\n...\n 5174|    #[test]\n*5175|    fn clamp_or_reset_resets_a_torn_ring_only() {\n 5176|        // Coherent ring → no-op.\n 5177|        let mut log = OutputLog::new(1, 4, String::new(), (24, 80));\n 5178|        log.append(b\"a\");\n...\n 5206|    #[test]\n*5207|    fn recover_log_clamps_a_poisoned_torn_ring() {\n 5208|        let mut seed = OutputLog::new(5, 4, String::new(), (24, 80));\n 5209|        seed.append(b\"live\");\n 5210|        seed.ring.push_back((999, b\"torn\".to_vec())); // corrupt: back >= next_seq\n...\n 5231|    #[test]\n*5232|    fn miss_respool_is_once_then_dead_letters() {\n 5233|        let mut set: HashSet<String> = HashSet::new();\n 5234|        // First miss → respool + remembered.\n 5235|        assert_eq!(note_miss_respool(&mut set, \"env-A\"), MissRespool::Respool);\n...\n 5251|    #[test]\n*5252|    fn miss_strikes_exhausted_at_budget() {\n 5253|        assert!(!miss_strikes_exhausted(0, 3));\n 5254|        assert!(!miss_strikes_exhausted(1, 3));\n 5255|        assert!(!miss_strikes_exhausted(2, 3));\n...\n 5267|    #[test]\n*5268|    fn echo_verified_catches_a_swallowed_head() {\n 5269|        let payload = b\"XLATE_OK line one\\nline two\\nline three\";\n 5270|        // Full echo present → verified.\n 5271|        let full_echo = b\"prompt> XLATE_OK line one\\nline two\\nline three\\n\";\n...\n 5296|    #[test]\n*5297|    fn should_settle_rearms_on_observable_pty() {\n 5298|        // Echoing/interactive PTY (probe observed → not unobservable): re-settle EVERY\n 5299|        // delivery's first byte — this is the class the `/clear` head-swallow bites.\n 5300|        assert!(should_settle(1, false), \"observable PTY re-settles before each delivery\");\n...\n 5313|    #[test]\n*5314|    fn contains_subslice_finds_contiguous_only() {\n 5315|        assert!(contains_subslice(b\"abcdef\", b\"cde\"));\n 5316|        assert!(contains_subslice(b\"abcdef\", b\"abc\"));\n 5317|        assert!(contains_subslice(b\"abcdef\", b\"def\"));\n...\n 5329|    #[test]\n*5330|    fn output_log_tap_reads_bytes_since_snapshot() {\n 5331|        let mut log = OutputLog::new(1, 64, String::new(), (24, 80));\n 5332|        assert_eq!(log.high_water(), 0, \"fresh log high-water is 0\");\n 5333|        log.append(b\"probe-echo\"); // pre-snapshot noise (a settle probe's echo)\n...\n 5353|    #[test]\n*5354|    fn chunk_text_splits_and_reassembles_exactly() {\n 5355|        // Small: whole, single emit.\n 5356|        let mut parts: Vec<Vec<u8>> = Vec::new();\n 5357|        chunk_text(b\"small\", 256, |p| parts.push(p.to_vec()));\n...\n 5378|    #[test]\n*5379|    fn supervised_worker_panic_faults_and_reaps_child() {\n 5380|        // A real, alive stand-in translation child (reads stdin, stays up).\n 5381|        #[cfg(windows)]\n 5382|        let prog = \"cmd\";\n...\n 5441|    #[test]\n*5442|    fn apply_once_enqueues_ptywrite_exactly_once_and_dedups_replay() {\n 5443|        use crate::effect::{EffectJournal, EffectKey, EffectKind, Minter, Outcome};\n 5444|\n 5445|        let dir = tempfile::tempdir().unwrap();\n...\n 5510|    /// `conn.split()`.\n*5511|    fn controller_socket_pair() -> (SharedSend, Stream, interprocess::local_socket::RecvHalf) {\n 5512|        use std::sync::atomic::AtomicU32;\n 5513|        // A process-unique pipe/UDS name so parallel test threads never collide.\n 5514|        static SEQ: AtomicU32 = AtomicU32::new(0);\n...\n 5534|    /// (no per-read deadline: Windows named pipes reject I/O timeouts).\n*5535|    fn drain_output_seqs(client: &mut Stream, n: usize) -> Vec<u64> {\n 5536|        let mut seqs = Vec::with_capacity(n);\n 5537|        while seqs.len() < n {\n 5538|            let env = read_frame(client).expect(\"output frame on the wire\");\n...\n 5581|    #[test]\n*5582|    fn superseded_controller_writer_is_epoch_gated_silent_one_live_writer_per_conn() {\n 5583|        let (send, mut client, _recv) = controller_socket_pair();\n 5584|        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 5585|        // Two chunks in the ring (seqs 0,1); the grid now holds their screen state.\n...\n 5651|    #[test]\n*5652|    fn attach_initial_batch_is_a_clean_alt_screen_repaint_not_the_raw_ring() {\n 5653|        let (send, mut client, _recv) = controller_socket_pair();\n 5654|        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 5655|        // The raw ring holds ALL of these chunks; a raw replay would dump the main\n...\n 5712|    #[test]\n*5713|    fn reap_dead_controller_clears_a_finished_writer() {\n 5714|        let (send, _client, _recv) = controller_socket_pair();\n 5715|        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 5716|        // A controller whose writer thread has already EXITED (models a writer that\n...\n 5755|    #[test]\n*5756|    fn reap_dead_controller_leaves_a_live_writer_untouched() {\n 5757|        let (send, _client, _recv) = controller_socket_pair();\n 5758|        let mut log = OutputLog::new(2, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 5759|        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n...\n 5784|    #[cfg(test)]\n*5785|    fn seed_controller_with_block(\n 5786|        log: &mut OutputLog,\n 5787|        by: Option<&str>,\n 5788|        blocked: Option<Instant>,\n...\n 5812|    #[cfg(test)]\n*5813|    fn wedged_since() -> Instant {\n 5814|        Instant::now()\n 5815|            .checked_sub(BRAIN_WRITE_DEADLINE + Duration::from_secs(1))\n 5816|            .expect(\"process has been up longer than the deadline\")\n...\n 5823|    #[test]\n*5824|    fn controller_write_stall_predicate_distinguishes_wedged_from_idle() {\n 5825|        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 5826|        let blocked = seed_controller_with_block(&mut log, Some(\"n\"), None);\n 5827|        let now = Instant::now();\n...\n 5843|    #[test]\n*5844|    fn local_controller_wedged_scopes_to_the_brains_own_conn() {\n 5845|        let now = Instant::now();\n 5846|        // A LOCAL (by:None) controller wedged past the deadline → wedged.\n 5847|        let mut local = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n...\n 5880|    #[test]\n*5881|    fn wake_gate_decision_truth_table() {\n 5882|        use WakeGate::*;\n 5883|        // Already live → dedup, whatever the claim / deadline state.\n 5884|        assert_eq!(wake_gate_decision(true, false, false), AlreadyLive);"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs","searchPath":"C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/broker.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":35,"fileCount":1,"files":["crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":35}],"truncated":false,"displayContent":" 4713│    #[test]\n*4714│    fn viewer_writer_emits_evicted_marker_only_on_an_evicted_close() {\n 4715│        // ── EVICTED close: flag true before tx drops → marker emitted. ──\n 4716│        {\n 4717│            let (send, client, _recv) = controller_socket_pair();\n     │...\n 4780│    #[test]\n*4781│    fn append_overflow_flags_the_evicted_viewer_for_skip_to_live() {\n 4782│        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 4783│        // A viewer whose live queue is depth-1 and which we NEVER drain (rx held,\n 4784│        // unread) — it cannot keep up with the drain. We do NOT use `add_viewer`\n     │...\n 4831│    #[test]\n*4832│    fn sessions_reply_resume_seq_defaults_when_absent() {\n 4833│        // Old-broker shape: no `resume_seq` key on the session object.\n 4834│        let old: SessionsReply =\n 4835│            serde_json::from_str(r#\"{\"sessions\":[{\"session_id\":7,\"endpoint\":\"e\"}]}\"#)\n     │...\n 4862│    #[test]\n*4863│    fn a_full_controller_does_not_starve_a_viewer_or_block_the_drain() {\n 4864│        let mut log = OutputLog::new(1, 1024, String::new(), (24, 80));\n 4865│        log.controller_epoch.store(9, Ordering::Release);\n 4866│        // A CONTROLLER whose 2-slot channel we NEVER drain (rx held, unread) — a\n     │...\n 4932│    #[test]\n*4933│    fn contiguous_advance_freezes_on_a_gap() {\n 4934│        let dt = AtomicU64::new(0);\n 4935│        contiguous_advance(&dt, 0);\n 4936│        assert_eq!(dt.load(Ordering::Acquire), 1, \"seq 0 (== cursor) advances to 1\");\n     │...\n 4959│    #[test]\n*4960│    fn advance_delivered_is_monotonic() {\n 4961│        let dt = AtomicU64::new(0);\n 4962│        advance_delivered(&dt, 0);\n 4963│        assert_eq!(dt.load(Ordering::Acquire), 1, \"seq 0 -> cursor 1\");\n     │...\n 4983│    /// the ORDER test; here the TEST is the consumer, draining `rx` deterministically.\n*4984│    fn test_input_writer(depth: usize) -> (InputWriter, Receiver<InputRecord>) {\n 4985│        let (tx, rx) = sync_channel::<InputRecord>(depth);\n 4986│        let w = InputWriter {\n 4987│            tx,\n     │...\n 5003│    #[test]\n*5004│    fn input_writer_preserves_strict_fifo_order_through_the_sole_writer() {\n 5005│        use spt_term::surface::{SessionSurface, SurfaceError, SurfaceSize};\n 5006│\n 5007│        /// A stub PTY write seam: appends every `write_input` to a shared Vec, in\n     │...\n 5010│        impl SessionSurface for RecordingSurface {\n*5011│            fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError> {\n 5012│                self.0.lock().unwrap().push(bytes.to_vec());\n 5013│                Ok(())\n 5014│            }\n*5015│            fn resize(&self, _: SurfaceSize) -> Result<(), SurfaceError> {\n 5016│                Ok(())\n 5017│            }\n 5018│        }\n     │...\n 5067│    #[test]\n*5068│    fn input_enqueue_drops_and_stamps_on_full_then_heals_on_drain() {\n 5069│        const DEPTH: usize = 3;\n 5070│        // Consumer is the test; it does NOT drain until we say so → the queue fills.\n 5071│        let (w, rx) = test_input_writer(DEPTH);\n     │...\n 5123│    #[test]\n*5124│    fn lock_floor_recovers_a_poisoned_inject_floor() {\n 5125│        let floor = Arc::new(Mutex::new(InjectFloor::default()));\n 5126│        // Poison it exactly as a panic-while-holding-the-floor-lock does.\n 5127│        let f = Arc::clone(&floor);\n     │...\n 5150│    #[test]\n*5151│    fn recover_reopens_a_poisoned_sessions_map() {\n 5152│        let sessions: Arc<Mutex<HashMap<u64, u64>>> = Arc::new(Mutex::new(HashMap::new()));\n 5153│        recover(&sessions).insert(7, 70); // a pre-poison \"session\" row\n 5154│        let s = Arc::clone(&sessions);\n     │...\n 5174│    #[test]\n*5175│    fn clamp_or_reset_resets_a_torn_ring_only() {\n 5176│        // Coherent ring → no-op.\n 5177│        let mut log = OutputLog::new(1, 4, String::new(), (24, 80));\n 5178│        log.append(b\"a\");\n     │...\n 5206│    #[test]\n*5207│    fn recover_log_clamps_a_poisoned_torn_ring() {\n 5208│        let mut seed = OutputLog::new(5, 4, String::new(), (24, 80));\n 5209│        seed.append(b\"live\");\n 5210│        seed.ring.push_back((999, b\"torn\".to_vec())); // corrupt: back >= next_seq\n     │...\n 5231│    #[test]\n*5232│    fn miss_respool_is_once_then_dead_letters() {\n 5233│        let mut set: HashSet<String> = HashSet::new();\n 5234│        // First miss → respool + remembered.\n 5235│        assert_eq!(note_miss_respool(&mut set, \"env-A\"), MissRespool::Respool);\n     │...\n 5251│    #[test]\n*5252│    fn miss_strikes_exhausted_at_budget() {\n 5253│        assert!(!miss_strikes_exhausted(0, 3));\n 5254│        assert!(!miss_strikes_exhausted(1, 3));\n 5255│        assert!(!miss_strikes_exhausted(2, 3));\n     │...\n 5267│    #[test]\n*5268│    fn echo_verified_catches_a_swallowed_head() {\n 5269│        let payload = b\"XLATE_OK line one\\nline two\\nline three\";\n 5270│        // Full echo present → verified.\n 5271│        let full_echo = b\"prompt> XLATE_OK line one\\nline two\\nline three\\n\";\n     │...\n 5296│    #[test]\n*5297│    fn should_settle_rearms_on_observable_pty() {\n 5298│        // Echoing/interactive PTY (probe observed → not unobservable): re-settle EVERY\n 5299│        // delivery's first byte — this is the class the `/clear` head-swallow bites.\n 5300│        assert!(should_settle(1, false), \"observable PTY re-settles before each delivery\");\n     │...\n 5313│    #[test]\n*5314│    fn contains_subslice_finds_contiguous_only() {\n 5315│        assert!(contains_subslice(b\"abcdef\", b\"cde\"));\n 5316│        assert!(contains_subslice(b\"abcdef\", b\"abc\"));\n 5317│        assert!(contains_subslice(b\"abcdef\", b\"def\"));\n     │...\n 5329│    #[test]\n*5330│    fn output_log_tap_reads_bytes_since_snapshot() {\n 5331│        let mut log = OutputLog::new(1, 64, String::new(), (24, 80));\n 5332│        assert_eq!(log.high_water(), 0, \"fresh log high-water is 0\");\n 5333│        log.append(b\"probe-echo\"); // pre-snapshot noise (a settle probe's echo)\n     │...\n 5353│    #[test]\n*5354│    fn chunk_text_splits_and_reassembles_exactly() {\n 5355│        // Small: whole, single emit.\n 5356│        let mut parts: Vec<Vec<u8>> = Vec::new();\n 5357│        chunk_text(b\"small\", 256, |p| parts.push(p.to_vec()));\n     │...\n 5378│    #[test]\n*5379│    fn supervised_worker_panic_faults_and_reaps_child() {\n 5380│        // A real, alive stand-in translation child (reads stdin, stays up).\n 5381│        #[cfg(windows)]\n 5382│        let prog = \"cmd\";\n     │...\n 5441│    #[test]\n*5442│    fn apply_once_enqueues_ptywrite_exactly_once_and_dedups_replay() {\n 5443│        use crate::effect::{EffectJournal, EffectKey, EffectKind, Minter, Outcome};\n 5444│\n 5445│        let dir = tempfile::tempdir().unwrap();\n     │...\n 5510│    /// `conn.split()`.\n*5511│    fn controller_socket_pair() -> (SharedSend, Stream, interprocess::local_socket::RecvHalf) {\n 5512│        use std::sync::atomic::AtomicU32;\n 5513│        // A process-unique pipe/UDS name so parallel test threads never collide.\n 5514│        static SEQ: AtomicU32 = AtomicU32::new(0);\n     │...\n 5534│    /// (no per-read deadline: Windows named pipes reject I/O timeouts).\n*5535│    fn drain_output_seqs(client: &mut Stream, n: usize) -> Vec<u64> {\n 5536│        let mut seqs = Vec::with_capacity(n);\n 5537│        while seqs.len() < n {\n 5538│            let env = read_frame(client).expect(\"output frame on the wire\");\n     │...\n 5581│    #[test]\n*5582│    fn superseded_controller_writer_is_epoch_gated_silent_one_live_writer_per_conn() {\n 5583│        let (send, mut client, _recv) = controller_socket_pair();\n 5584│        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 5585│        // Two chunks in the ring (seqs 0,1); the grid now holds their screen state.\n     │...\n 5651│    #[test]\n*5652│    fn attach_initial_batch_is_a_clean_alt_screen_repaint_not_the_raw_ring() {\n 5653│        let (send, mut client, _recv) = controller_socket_pair();\n 5654│        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 5655│        // The raw ring holds ALL of these chunks; a raw replay would dump the main\n     │...\n 5712│    #[test]\n*5713│    fn reap_dead_controller_clears_a_finished_writer() {\n 5714│        let (send, _client, _recv) = controller_socket_pair();\n 5715│        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 5716│        // A controller whose writer thread has already EXITED (models a writer that\n     │...\n 5755│    #[test]\n*5756│    fn reap_dead_controller_leaves_a_live_writer_untouched() {\n 5757│        let (send, _client, _recv) = controller_socket_pair();\n 5758│        let mut log = OutputLog::new(2, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 5759│        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n     │...\n 5784│    #[cfg(test)]\n*5785│    fn seed_controller_with_block(\n 5786│        log: &mut OutputLog,\n 5787│        by: Option<&str>,\n 5788│        blocked: Option<Instant>,\n     │...\n 5812│    #[cfg(test)]\n*5813│    fn wedged_since() -> Instant {\n 5814│        Instant::now()\n 5815│            .checked_sub(BRAIN_WRITE_DEADLINE + Duration::from_secs(1))\n 5816│            .expect(\"process has been up longer than the deadline\")\n     │...\n 5823│    #[test]\n*5824│    fn controller_write_stall_predicate_distinguishes_wedged_from_idle() {\n 5825│        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n 5826│        let blocked = seed_controller_with_block(&mut log, Some(\"n\"), None);\n 5827│        let now = Instant::now();\n     │...\n 5843│    #[test]\n*5844│    fn local_controller_wedged_scopes_to_the_brains_own_conn() {\n 5845│        let now = Instant::now();\n 5846│        // A LOCAL (by:None) controller wedged past the deadline → wedged.\n 5847│        let mut local = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n     │...\n 5880│    #[test]\n*5881│    fn wake_gate_decision_truth_table() {\n 5882│        use WakeGate::*;\n 5883│        // Already live → dedup, whatever the claim / deadline state.\n 5884│        assert_eq!(wake_gate_decision(true, false, false), AlreadyLive);"},"isError":false,"timestamp":1784247725432}}
{"type":"message","id":"70440680","parentId":"32de930b","timestamp":"2026-07-17T00:22:20.937Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Formulating hypotheses for test discrepancies**\n\n**Designing controlled broker reconnect tests**\n\n**Mapping exact resource ownership**"},{"type":"toolCall","id":"call_JSfhtWA03KLwwfmagBAQIEU3|fc_0996e78cf50e3160016a5975baca848199bedf4696022321cb","name":"read","arguments":{"i":"Reading attach two-connection ownership","path":"crates/spt-daemon/src/attach.rs","selector":"285-385"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":8942,"output":536,"cacheRead":162304,"cacheWrite":0,"totalTokens":171782,"reasoningTokens":493,"cost":{"input":0.04471000000000001,"output":0.01608,"cacheRead":0.081152,"cacheWrite":0,"total":0.141942}},"stopReason":"toolUse","timestamp":1784247725452,"responseId":"resp_0996e78cf50e3160016a5975add24c8199886e0fe00818e514","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a5975af62948199ac61dfee868034ec","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXW6-FQqx5lxfsve4W0x8uIEizvB1lDbopKa1mPqMcs5TezT4ybH4pgA128nNDeLci57JVsrkSwpof3dhRTQRYIu1BJqnUdVO2FgcHgNf5XDoVo8aLs-QuMUUPlhX4mHOPdENth-ikqUlDOBH62-rfBX3_AnnicMg1suk01H3Eoq1q8gwTqJPB-f18NNebnvF6jIZT598vnULtnVKCc58UH5LMP7jVY3rGSmN5ky-Wy_m3BcPER9cY_GCYTzZSiPSQ4ZilVMFU_yikphJpertPr_Nxkynyjpb1CgqzL6exOADPVsJCFz63pVJBH8E2JzO8DbqJtDXWoNnMmY-jkGHoFq842zAPqRjsHmgrdFuQbubmRFWfLTsvyEq4iG30bRGrGcqj6ZgdGSVM-5OadaAd0-GOCbKVhpENJR3sQXjNbfiY4S8EjP2VTLOyoAqU6pDDlkZCeog67xPx8oASzwuxHJQmnFoLqNVtmLXKAy_Wqirn6wydrSXb_0BUPZ4LxvyR_eEVBIlq7-n-ReU4vt-WSxtjrqKKdTqsOf0VfAHLhhfLRUIgHcThvellDQ17KwEQhvLZHmada1JhTbeUxS9Mgpu8qm208AlXxP2_QXX5EG-Bs_92_eUh6GG0GrtvgZ7H0nH3ylJsV9g08q-kpssrUfH-AAT3sEy6M79M5YDKRJyqpYNxwrPKKNJajgRDt_vEgMI2v2rAo_EYkvQCh3T2Uj439rmkDU2nXoIrim8qkRrprWPRLFmYoSxVCkcfJ1kh4jd2SMJLifqESMPDxxctut8MpeccAXJpTjo0J6ngjtYeG511i8mo8MDgJPF_A1Tu5K_-0JPj8ichuvh-FfmrxMA_H1vzch5LGRK18ECzG6NI-GG6Y5tAZMWxZe-951IqsAjZMr8JB8WcNMQPWg9qgPr9PPT0RtZWHflSEk3L5PE-1_nX-TFlE2YgETijJjJDH7Yk3Jxe-awU-oVB5mJjx-ucxbT8q01Z3VFosBkAKw-9NKEAFMah2aw4Z8zC1_JD-Bgnu9PHqsyocUYcI78t3i7BSkBVUNWsw_Evvs33khhZJGnoeU1ND0K6dcKccRChCqZ7OYxyppl0_yHKzjxRPKI3xyQFXyMky6ocv0aYl85iHwt9-mXm4HfMusrcJTzJyXS8G1jAyX7wryrNdyRP_LfXWWx8osLPrOZWRm7uiP9CmVoUqVNpfsRcSl-gORhT1eM-RdqD1ioikrZPg7p_de00hn60Mhwes8h9wPDq0MjYFyaeYH4dFDDFfvoEK4RRzXmVk8LlgnzQoQzCaHPYE26aEAprd7lWVXkwcGSabwxrGRxMNJJhQYRQLCIEwQHt2qTRiBZPwmJHaoxOmHEn4D73MTjW43HoH2dZOMHeWokOctFNwvLhNM2vojlMkqh-X_Cuq82-x8g5g5taWcYS2AVl1ICZznnjVaFK8dyqmcBvBRy6uEjvWhnNGGb96Jtj-fVyyhCEIzAneKrqgLh_z8ztGk6wPSnSCUFmHj6EtreVRAdTZtQDXa0cemsKDmPFsmoj7QGTzWhCZmH_uIVyMbBYw89htnKURst_5ztb65PAtk1-XdKnW96oGH-FDh5kCVDqenvG8mQamIqKR9h-S-3leXvVqGM1gJCp3CEKPnwNz-oBs9IesHBPV6AQpRkZytB3cJOlI1eeH9PQik6vhOOctdCgAXJYVV0LGG_q9HGNyTY63f-nfOneLROzP09n7GlC9cQYxinhwE3r8Clrb507wof6C3SRCAcrUJIqwRoFbUqZW2d1g6WvRbPVPO8t9zckacLQMMK4S2IK9dj7TQ_5kd0svPq5DStAZoutmKzfHv0L9Rurh07BziVKyYtTeHTDLJDIxSyfY6CFubGuu_oKFIYpLE5bZW5Z9nSGi2k8sZaPUJ7zfb8OHy3rT8Huqb7T_frHgQHyjSSyv-1uAEG5-da3iu59UMbv7WREUDDD6G-elV_fcI17G7MN3LTtjE_1SovvOm-SdgjnGdk_h-zGFZCWA55nGsUnZEFHexNwnqUtgZdc4cR4H9SIXsK6DBCKeyWU9cB2N0WoEhVyZte9GjuhIakDA_zFHvQpGKpGIJ85FkGjtoddgGJhofVXr45ow2FtL5URL63A3MN8ofdyCfNcSCooRgMCq9qUqAHMzJRxih26CxFgMKYPaDfS05SuaWefVPJC6IqyFOGkJlGfYJoK1_DfjW_kNxW8vRVS5w1u8T77aPMfuQ5RcqKz97MpFr-tKolyzuQVk7igH2pnYe_9H9WYoKx5b9eeuimwl9PWWJjIUCM0tzqTTw1Y-qrRUZI6NzZIP3CeKNQgCec_sErl1JYErTqn26jTVuVQxW7rKBg22MWCsX5iOzyQXz5a2pZopvF3_zsPmrPXFeNa-ytJveNE39_KXE34zWn7dIbqxcWDVQIxn6qNZtUDUaJBGay3A-b8zO4NaguwurCX8zsxBqD_IJez6dAAVUfVYq5GMNiq1n0D-7fpldra9lZfX0gtuAoWesoOG9rL-0Z4_qtwfTaiOcLZqm6iuLwXiXWtlkXd3uXtSiRxhdObBlOJSPZgOP4mzYrWaNwPu1l3b5DXQMnGlxeobAR56ZVFO1olpmiXk7SNG0LeHCclblwFUw1iv33Xrrbum7vYm2NgWmDjBqTfmJ0clr0b4WuY5CmuGT6wFJHdWcTuIBzyUpzoxvuK-NZS79HJpQtSTYCKfMncgbtQRqZDQiATkWlK-3Mumua6oAN-typtLzqER0PmtfRUt3Zwk-jaakaowjG_ZwsTBUYriRrpM3ZmBUqdQqQWDDgUC7AA0mYSZF6zSB9hodY3jkblq9WW4uTm824tCmb2LvPXc4EhmTPN_mLKgY19u1J1IoCmjQ7d955G9R6g0cdQElqqgkF4aBX3sNnE7myyUqSEFKwveNv5SDz9OULCMpTMLYQy7kDZpbgFhVQfKSpfJ3ic86PQsi_zusbRkt4FeOfrDX0XW5jbzJMwH_X-fcjXyMRk3UywGhUtxfBC2ekX7CMZSgYyd52J25jPzmOl5CF8bAFC1RjbU0bkWdLJK_oNsKjk21Wa5NNYtdUlSnGSBXLZNsO313pjoEOFQnswGA4GdFPNOS8LVkVWEMaIF3-iikIQ-2i4vdYGneriqlTc1KHGs_lQVCjeasI5FL7KXgL2cTsyZHSI6hADAvSn1g1odel3YK9Q4-jeOWF5ll3dJGqMKhl5JOHqvkWNXsZ8-3rfILHJAUhPK8VCxFLsb11_4Mm0RRKamqgIU83jA-XBXKQ09rN4niylPMKDG_i4VrDmFyQC5lOsEIZ3a_9C_gIHUFR14kZgt9IbrUB63rlB3w5YyvN44Vbd9_Ni2pxBuxPlNIZD86GnRUu9izng3kGh3AL0wSkTXX-ov7XNMZXLKq3JcBa9hfWCnjtEgeUSXpOeYft7aUqq5DhLKBwcwkgxqmyuYyAQMkojSO4ExxB8w8ZJpMs1LX4LZi80d3AUkvSOlahuyS3-Z1Gv70p9JF5TDAw4Wi5LXpcA7qhvlN7qiL1X8t3v5qQhatESifSbt9do6MsXaabSDwNM9jddd__Ns8zUbuFDhx8z5ByH3OVSdihTkcfPER8B9kacaZCAaM5LXJuT3trFhXKPTt49G0NMMGeprRLx5dks4bHQFNAMdhMh1Z_wloBq0RmiTu5OTf-1MgkGh4Xjo0fXY0bx09dedhFh1GMFpI5LwSVIywcSbJ2hi19tU1-lqC1lQHX-gNg-SGeU5vMP9klnqUJ-o7uHPKzxxt-JQ8tjldRXiqK4C0lXGyLnE4xPFaqrOtv8tKbTgHnUEkCAOq1w6x3QYY2TVwvUK34A6GfKSf9_52eR-_3nkvXrxqO6t8gcAHWErEvSoGtaUNkwMkoRDqz6lvEsRFx0ISujCSjsO65mgYq_9fX76FgxEMiZmUUg_OAQtYOPL4xnZINgoQ6T6FT00CoUa6abK234nbwN5CyXVBFOe_JNAKA8oKrCfbuPRF0LaRqRzj-6K9xg4DaSg=","summary":[{"type":"summary_text","text":"**Formulating hypotheses for test discrepancies**"},{"type":"summary_text","text":"**Designing controlled broker reconnect tests**"},{"type":"summary_text","text":"**Mapping exact resource ownership**"}]},{"id":"fc_0996e78cf50e3160016a5975baca848199bedf4696022321cb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach two-connection ownership\",\"path\":\"crates/spt-daemon/src/attach.rs\",\"selector\":\"285-385\"}","call_id":"call_JSfhtWA03KLwwfmagBAQIEU3","name":"read"}]},"duration":15478.245600000024,"ttft":1923.8792999982834,"contextSnapshot":{"promptTokens":171246,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_JSfhtWA03KLwwfmagBAQIEU3|fc_0996e78cf50e3160016a5975baca848199bedf4696022321cb","toolName":"read","startedAt":"2026-07-17T00:22:20.937Z","args":{"path":"crates/spt-daemon/src/attach.rs"},"intent":"Reading attach two-connection ownership"},"id":"44142b39","parentId":"70440680","timestamp":"2026-07-17T00:22:20.937Z"}
{"type":"message","id":"a9bc919b","parentId":"44142b39","timestamp":"2026-07-17T00:22:20.944Z","message":{"role":"toolResult","toolCallId":"call_JSfhtWA03KLwwfmagBAQIEU3|fc_0996e78cf50e3160016a5975baca848199bedf4696022321cb","toolName":"read","content":[{"type":"text","text":"/// wire-arriving Request (same gate, same subscribe); a Request that then\n/// replays from the ring anyway re-runs the same-identity re-take, which is\n/// silent and idempotent (the successor-re-take parity the fix must keep).\n// [impl->REQ-INST-8]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\n// [impl->REQ-STREAM-OPENER-DURABLE]\n#[allow(clippy::too_many_arguments)]\npub fn serve_attach(\n    brain: &mut Brain,\n    broker_name: &str,\n    stream_id: u64,\n    endpoint: &str,\n    origin_node: &str,\n    net_from_seq: u64,\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    // 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    // 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    }) = seed_request\n    {\n        if let Some(out) = 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            &mut intent,\n            &mut attached_session,\n            &mut attached,\n        )? {\n            return Ok(out);\n        }\n    }\n\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 = match (role, attached_session) {\n            (Some(ServeRole::Controller), Some(sid)) => {\n                match read_controller_event_resuming(brain, sid, 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…\n                    }\n…\n                }\n            }\n…\n        };\n…\n    }\n}\n\n[Showing lines 284-388 of 692. Use :389 to continue]"}],"details":{"truncation":{"content":"/// wire-arriving Request (same gate, same subscribe); a Request that then\r\n/// replays from the ring anyway re-runs the same-identity re-take, which is\r\n/// silent and idempotent (the successor-re-take parity the fix must keep).\r\n// [impl->REQ-INST-8]\r\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n// [impl->REQ-STREAM-OPENER-DURABLE]\r\n#[allow(clippy::too_many_arguments)]\r\npub fn serve_attach(\r\n    brain: &mut Brain,\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    endpoint: &str,\r\n    origin_node: &str,\r\n    net_from_seq: u64,\r\n    remote_session: Option<u64>,\r\n    seed_request: Option<AttachRecord>,\r\n) -> io::Result<AttachServeOutcome> {\r\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\r\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\r\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\r\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\r\n    // session output (broker→serve) — including the controller's synchronous,\r\n    // inline, multi-frame ring replay (`become_controller`), which holds the\r\n    // broker's single-threaded per-conn handler for the whole replay. If the\r\n    // forwards rode `brain` too, that stuck handler could not read them →\r\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\r\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\r\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\r\n    // two directions on independent single-threaded handlers cannot block each\r\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\r\n    // a second conn is correct. Order is preserved: this one serve thread reads\r\n    // every Output in seq order on `brain` and forwards it in that same order on\r\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\r\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\r\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\r\n    // the controller onto that async path would remove the inline-replay block at\r\n    // the source, but it touches the authoritative cursor-advancing path.)\r\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut attached = false;\r\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\r\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\r\n    // race on the input gate).\r\n    let mut intent = AttachIntent::Control;\r\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\r\n    // controller input that races ahead of the reply is BUFFERED, not dropped\r\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\r\n    // we know we control the session (the busy-refuse case).\r\n    let mut role: Option<ServeRole> = None;\r\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\r\n    // The attached session — captured from the Request so the terminal paths can\r\n    // explicitly release this worker's role (clearing driven_by / viewer_count\r\n    // promptly, not only on connection drop).\r\n    let mut attached_session: Option<u64> = None;\r\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\r\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\r\n    // intermittent live bursts, never a CPU spin.\r\n    let mut last_resubscribe: Option<Instant> = None;\r\n\r\n    // Durable-opener seed (ADR-0038 B): process the dispatcher-pinned Request\r\n    // BEFORE the event loop, through the exact wire-Request path — a rolled\r\n    // ring can no longer replay it. Non-Request seeds are ignored (the opener\r\n    // of an attach stream is always its Request).\r\n    if let Some(AttachRecord::Request {\r\n        session_id,\r\n        from_seq,\r\n        intent: req_intent,\r\n        endpoint_id: _,\r\n    }) = seed_request\r\n    {\r\n        if let Some(out) = handle_attach_request(\r\n            brain,\r\n            &mut wire,\r\n            stream_id,\r\n            endpoint,\r\n            origin_node,\r\n            remote_session,\r\n            session_id,\r\n            from_seq,\r\n            req_intent,\r\n            &mut intent,\r\n            &mut attached_session,\r\n            &mut attached,\r\n        )? {\r\n            return Ok(out);\r\n        }\r\n    }\r\n\r\n    loop {\r\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\r\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\r\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\r\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\r\n        // role resolves no Output flows, so a plain read is correct then.\r\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\n        let event = match (role, attached_session) {\r\n            (Some(ServeRole::Controller), Some(sid)) => {\r\n                match read_controller_event_resuming(brain, sid, Some(origin_node)) {\r\n                    Ok(ev) => ev,\r\n                    Err(e)\r\n                        if e.get_ref()\r\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\r\n                    {\r","truncated":true,"truncatedBy":"lines","totalLines":409,"totalBytes":5444,"outputLines":105,"outputBytes":5444,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// wire-arriving Request (same gate, same subscribe); a Request that then\r\n/// replays from the ring anyway re-runs the same-identity re-take, which is\r\n/// silent and idempotent (the successor-re-take parity the fix must keep).\r\n// [impl->REQ-INST-8]\r\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n// [impl->REQ-STREAM-OPENER-DURABLE]\r\n#[allow(clippy::too_many_arguments)]\r\npub fn serve_attach(\r\n    brain: &mut Brain,\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    endpoint: &str,\r\n    origin_node: &str,\r\n    net_from_seq: u64,\r\n    remote_session: Option<u64>,\r\n    seed_request: Option<AttachRecord>,\r\n) -> io::Result<AttachServeOutcome> {\r\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\r\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\r\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\r\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\r\n    // session output (broker→serve) — including the controller's synchronous,\r\n    // inline, multi-frame ring replay (`become_controller`), which holds the\r\n    // broker's single-threaded per-conn handler for the whole replay. If the\r\n    // forwards rode `brain` too, that stuck handler could not read them →\r\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\r\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\r\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\r\n    // two directions on independent single-threaded handlers cannot block each\r\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\r\n    // a second conn is correct. Order is preserved: this one serve thread reads\r\n    // every Output in seq order on `brain` and forwards it in that same order on\r\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\r\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\r\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\r\n    // the controller onto that async path would remove the inline-replay block at\r\n    // the source, but it touches the authoritative cursor-advancing path.)\r\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut attached = false;\r\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\r\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\r\n    // race on the input gate).\r\n    let mut intent = AttachIntent::Control;\r\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\r\n    // controller input that races ahead of the reply is BUFFERED, not dropped\r\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\r\n    // we know we control the session (the busy-refuse case).\r\n    let mut role: Option<ServeRole> = None;\r\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\r\n    // The attached session — captured from the Request so the terminal paths can\r\n    // explicitly release this worker's role (clearing driven_by / viewer_count\r\n    // promptly, not only on connection drop).\r\n    let mut attached_session: Option<u64> = None;\r\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\r\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\r\n    // intermittent live bursts, never a CPU spin.\r\n    let mut last_resubscribe: Option<Instant> = None;\r\n\r\n    // Durable-opener seed (ADR-0038 B): process the dispatcher-pinned Request\r\n    // BEFORE the event loop, through the exact wire-Request path — a rolled\r\n    // ring can no longer replay it. Non-Request seeds are ignored (the opener\r\n    // of an attach stream is always its Request).\r\n    if let Some(AttachRecord::Request {\r\n        session_id,\r\n        from_seq,\r\n        intent: req_intent,\r\n        endpoint_id: _,\r\n    }) = seed_request\r\n    {\r\n        if let Some(out) = handle_attach_request(\r\n            brain,\r\n            &mut wire,\r\n            stream_id,\r\n            endpoint,\r\n            origin_node,\r\n            remote_session,\r\n            session_id,\r\n            from_seq,\r\n            req_intent,\r\n            &mut intent,\r\n            &mut attached_session,\r\n            &mut attached,\r\n        )? {\r\n            return Ok(out);\r\n        }\r\n    }\r\n\r\n    loop {\r\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\r\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\r\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\r\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\r\n        // role resolves no Output flows, so a plain read is correct then.\r\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\n        let event = match (role, attached_session) {\r\n            (Some(ServeRole::Controller), Some(sid)) => {\r\n                match read_controller_event_resuming(brain, sid, Some(origin_node)) {\r\n                    Ok(ev) => ev,\r\n                    Err(e)\r\n                        if e.get_ref()\r\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\r\n                    {\r\n…\n                    }\n…\n                }\n            }\n…\n        };\n…\n    }\n}","startLine":284,"lineNumbers":[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,null,403,null,408,409,null,411,null,606,607]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\attach.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":692,"totalBytes":5444,"outputLines":105,"outputBytes":5444,"shownRange":{"start":284,"end":388},"nextOffset":389}}},"isError":false,"timestamp":1784247740943}}
{"type":"message","id":"4bf11477","parentId":"a9bc919b","timestamp":"2026-07-17T00:24:04.361Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Clarifying viewer and controller roles**\n\n**Detailing broker reconnection and recovery**"},{"type":"toolCall","id":"call_ZLZcHsrgGR3tiC6JuaTy9On4|fc_0996e78cf50e3160016a5975c67b748199883c9f390ec8cc84","name":"yield","arguments":{"type":null,"result":{"data":{"summary":"Confirmed a broker-stream redispatch ownership defect at the refresh/update interaction boundary. The broker correctly retains the PTY, child, output log, QUIC/loopback connections, and active net streams across a brain cycle; the brain-owned target-side `serve_attach` workers do not survive and must be reconstructed. In the affected pre-fix behavior, a fresh dispatcher enumerated every peer-initiated stream still in the broker table, including finished historical Attach streams. A historical worker could silently re-take the controller because the origin identity matched, then consume its replayed EOF and clear that controller. The legitimate active viewport stayed physically open but was no longer the selected controller sink, producing the exact “terminal frozen, no disconnect banner, detach plus fresh `spt rc` restores it” symptom. The working tree now contains ADR-0038, explicit stream retirement, pinned opener metadata, typed retryable claims, and production-path regression tests. No runtime mutation or test execution was performed, per the assignment.","files":[{"path":".claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt","description":"Incident-specific source-level RCA for the HFENDULEAM refresh/update event: 4/5 endpoints froze; records exact causal chain, field log tokens, why detach+new rc recovers, and the missing production-path tests."},{"path":"docs/adr/0038-broker-stream-lifecycle-truth.md","description":"Accepted lifecycle decision: finished Attach rows are terminal for redispatch, classification identity must be ring-independent, claims must be retryable and typed, and legitimate same-origin successor re-take remains silent."},{"path":"CONTEXT.md","description":"Domain authority for broker/brain split, broker-owned PTY continuity, remote-control semantics, and controller/viewer behavior."},{"path":"docs/KNOWN-HAZARDS.md","description":"Relevant invariants include viewer isolation, controller writer ordering, input-ack backpressure, broker/brain separation, brain-resume no-control-steal, physical connection poisoning, and attach-wedge recovery."},{"path":"docs/DEBUG-ROLLOUT.md","description":"Operational boundary: refresh and brain-compatible update preserve broker-held resources; broker-touching candidates require the documented rollout posture. Useful for a safe non-production reproduction plan."},{"path":"crates/spt/src/rc.rs","description":"Operator side. `run_attach_inner` → `establish_attach` → `request_attach_endpoint` → `net_stream_subscribe` → `attach_viewport`/`pump`. `pump::detach` FINs the stream; automatic reconnect happens only for `BrokerGone`/`Severed`, not for a silently deselected controller sink."},{"path":"crates/spt-daemon/src/attach.rs","description":"Target semantic relay. `serve_attach` owns a session-subscription Brain connection plus a separate wire-forwarding Brain connection. `handle_attach_request` calls `attach_as`/`attach_as_viewer_snap`; replayed stream EOF calls `detach_session`. `read_controller_event_resuming` handles output gaps but cannot restore a controller slot stolen and cleared by another worker."},{"path":"crates/spt-daemon/src/broker.rs","description":"PTY role truth. `OutputLog::resolve_subscribe`, `become_controller`, `add_viewer`, `clear_controller`, `detach_if`, `controller_writer`, and `controller_epoch` implement one controller plus isolated viewers. Same-`by` controller re-subscribe is a silent re-take. `handle_conn` removes only roles belonging to the disconnected broker client while preserving sessions."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Broker-owned data plane. `NetShared::{conns,streams}`, `StreamEntry`, `StreamLog`, `register_stream`, `stream_infos`, `retire_stream`, and `stream_opener` define stream persistence and redispatch eligibility. Current code pins the opener outside the bounded ring and hides retired rows without tearing down the transport."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"Brain-owned reconstruction plane. `run_dispatch_loop`, `worker`, `first_line`, `peek_first_line`, `ClaimState`, `should_claim`, `outcome_transition`, and `finished_row_is_terminal` are the exact rediscovery/classification/retry seams. Current code retires served rows and skips finished Attach rows unserved."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"Brain refresh boundary. `run_brain` uses cursor-only `resume_session_cursors`, then starts new net consumers via `spawn_net_consumers`; the fresh inbound dispatcher—not `BRAIN_RESUMED` itself—reconstructs active peer Attach streams."},{"path":"crates/spt-daemon/src/conn.rs","description":"Physical broker-client transport. `BrokerConn::write_gated`, watchdog, `poison_and_cancel`, `sever`, and `Drop` bound and retire a non-draining local IPC connection. This explains delayed `CONN_WRITE_POISONED` records but is secondary to silent controller selection loss."},{"path":"crates/spt-term/src/pty.rs","description":"Lowest PTY ownership layer. `PtySession` owns the master reader/writer and child; its drain and shared serialized writer remain broker-held across brain refresh."},{"path":"crates/spt-daemon/tests/redispatch.rs","description":"New production-path regressions using a killable real dispatcher generation: finished historical Attach cannot take/clear current control; finished-unretired Attach is skipped and drained; finished request/reply streams still serve; rolled-opener active Attach resumes; one transient worker failure retries."},{"path":"crates/spt-daemon/tests/fixtures/dispatch_fixture.rs","description":"Killable child running the real `dispatch::run_dispatch_loop`, allowing deterministic dispatcher-generation replacement without killing broker, PTY, or net streams."},{"path":"crates/spt-daemon/tests/attach.rs","description":"Existing attach/controller/viewer and target-brain restart coverage. Important gap: `attach_survives_target_brain_restart_exactly_once` manually re-served a known short stream, bypassing production rediscovery and opener classification."},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","description":"Covers the earlier local `run_brain::resume_sessions` control-steal bug and validates cursor-only supervised-brain resume. It does not cover peer-stream redispatch of remote Attach records."},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","description":"Guards that supervised brain startup seeds cursors without subscribing a non-draining heartbeat connection; distinguishes the retired local resume bug from the current remote-stream dispatcher seam."},{"path":"crates/spt-daemon/tests/brain_decouple.rs","description":"Deterministic non-draining controller connection and logical/physical stall-eviction coverage."},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","description":"Windows physical named-pipe black-hole harness: unrelated sessions remain live, bad connection is canceled/closed, and a fresh viewer can attach. Useful companion but not a redispatch ownership test."},{"path":"crates/spt/tests/attach_wedge_e2e.rs","description":"Real detached-daemon guard for abrupt rc loss plus PTY child death; proves one dropped viewport cannot wedge the broker globally, but does not recreate a dispatcher generation over retained historical streams."}],"architecture":"## Ownership and lifecycle map\n\n1. **PTY data plane — broker-owned and refresh-stable.** `spt-term::pty::PtySession` owns the child and PTY master reader/writer. `Broker::dispatch_spawn` stores it in `HostedSession`, starts its drain and exit-waiter, and appends child output into `OutputLog`. A brain cycle does not destroy these objects. This is why detach plus reattach can immediately recover without restarting the harness.\n2. **Operator/viewer connection — `spt rc` side.** `rc::establish_attach` opens one broker-owned loopback stream for a local target or a QUIC stream for a remote target, sends `AttachRecord::Request`, and subscribes the operator-side Brain to that stream. `rc::pump` forwards stdin as `Input`, renders `Output`, FINs on intentional detach, and reconnects only when it observes actual broker/stream severance. A live stream that simply stops receiving output is not a reconnect-class event; after the first event, the 30-second first-event guard also no longer helps.\n3. **Target semantic relay — brain-owned and refresh-ephemeral.** `dispatch::run_dispatch_loop` claims peer-initiated stream rows. Each Attach worker opens a fresh target-side Brain connection and enters `attach::serve_attach`. That loop subscribes to the retained net-stream ring, interprets Request/Input/Resize, subscribes to the broker PTY session as controller or viewer, and forwards session Output/Exit/Displaced over a second Brain connection. These worker threads die with the brain generation even though the stream and PTY survive.\n4. **Controller role — one selected sink per session.** `OutputLog::resolve_subscribe` owns the policy. Viewer intent calls `add_viewer` and never owns input. Control/Take may call `become_controller`; a matching `by` identity is intentionally a silent re-take so a legitimate successor brain can reconstruct the same operator without emitting `Displaced`. `controller_epoch` stops superseded writer threads from reordering initial replay, but it is not a dispatcher-generation authorization epoch: it does not prevent a historical same-origin worker from making a fresh, policy-valid `attach_as` call.\n5. **Viewer role — isolated, plural, non-owning.** Each viewer has a bounded SPSC queue and writer thread. Overflow evicts only that viewer and `serve_attach` re-subscribes it skip-to-live. Viewer behavior does not explain loss of input authority on four controllers.\n6. **Broker client cleanup.** `Broker::handle_conn` tracks `my_subs` and `my_stream_subs`. On actual EOF it detaches only that physical Brain connection from its PTY roles and net-stream subscriptions; sessions, stream logs, and PTYs persist. A half-open/non-draining old brain connection may remain until an output write actually blocks and `BrokerConn`'s watchdog poisons it.\n7. **Daemon brain refresh.** `brainproc::run_brain` now performs only `resume_session_cursors`; it does not subscribe the supervised heartbeat Brain as controller. Once net is enabled, `spawn_net_consumers` starts a brand-new dispatcher with new in-memory claim state. Thus `BRAIN_RESUMED: ... session cursors` reports PTY bookkeeping, not reconstruction of remote Attach workers.\n8. **Broker lifecycle.** Refresh/brain-compatible update preserves broker process, PTYs, QUIC connections, stream rows, and journals. A full broker restart instead loses connections/streams and should make rc observe `BrokerGone`/`Severed` and enter its bounded establish-again loop. The incident is specifically the former case.\n\n## Confirmed pre-fix failure chain\n\n- `NetShared.streams` retained finished stream rows; `StreamLog::finish` marked EOF but did not remove them from dispatcher enumeration.\n- Fresh `run_dispatch_loop` began with an empty one-shot claim set and enumerated all historical peer streams.\n- A historical Attach worker replayed its `Request`, called `attach_as(... Control, by=same_origin)`, and reached `resolve_subscribe`'s intentional same-identity silent re-take. `become_controller` replaced the current sink without notifying rc.\n- That same historical stream then replayed EOF. `serve_attach` called `detach_session`; broker `dispatch_unsubscribe`/`OutputLog::detach_if` cleared the controller that stale worker had just installed.\n- The legitimate active serve loop remained open and still held its local `role == Controller`, but the broker no longer selected its sink. The remote rc therefore got neither new output nor a severance event. Input records could remain on the open wire while no selected controller relay applied them.\n- Intentional detach closed the bad viewport. A new `spt rc` created a new stream and worker; it subscribed after the stale replay storm and became the selected controller, while reusing the same broker-held PTY.\n\n## Why most, not all\n\nThe stale replay effect is per-session and schedule/history dependent, not endpoint-id dependent. An endpoint is affected when it has a finished historical Attach row whose worker lands after the legitimate current worker, or when its active stream cannot be reconstructed. An endpoint with no relevant historical row, or whose legitimate active worker becomes controller last, remains live. [INFERENCE] This is consistent with 4/5 and a different endpoint later being affected, but the field logs do not map each stream id to each endpoint.\n\n## Automatic recovery versus indefinite freeze\n\nThe stale replay mechanism has no built-in 20- or 30-second recovery timer. If a stale worker is the final controller mutation, the viewport remains frozen until a fresh attach or another role-changing event. Automatic recovery can occur if the legitimate active worker finishes reconstruction after all finite historical workers and becomes controller last. [INFERENCE] Current W1 retry/fallback behavior can amplify or delay that last-worker ordering: `first_line` catches every `net_stream_opener` error and falls back to `peek_first_line`, while a `DispatchOutcome::Failed` retries up to three attempts with 500 ms then 1 s backoff. Therefore the redispatch defect is the ownership-corruption mechanism; broad `Err(_)` fallback and retry are a separate plausible cause of a deterministic 20–30-second delayed recovery and are not superseded by this RCA.\n\nThe delayed connection-poison evidence is also secondary. A non-draining old controller connection has `write_blocked_since == None` while the PTY is idle, so heartbeat `sessions()` polls cannot classify it as blocked. The first later PTY output enters `controller_writer` → `BrokerConn::write`, arms the 15-second watchdog, and then produces `CONN_WRITE_POISONED` approximately 15 seconds later. A minutes-long gap from refresh to `write-start` therefore means “no attempted output write yet,” not a minutes-long watchdog. Poisoning then forces broker EOF cleanup and may release a role, but it does not account for the immediate silent deselection caused by historical replay.\n\n## Invariants\n\n- Broker refresh must preserve PTY, child, output ring, effect journal, QUIC connection, and active stream.\n- Brain refresh must reconstruct only live peer streams; terminal Attach exchanges must never be served again.\n- Stream family identity must survive ring eviction; correctness metadata cannot live only in a bounded transport ring.\n- Same-origin re-take must remain silent for the legitimate successor, so origin identity cannot distinguish stale from live replay; lifecycle state must.\n- Exactly one controller sink is authoritative; viewer sinks never own PTY input and cannot block the drain/controller.\n- `controller_epoch` protects writer ordering, not stream freshness or dispatcher generation.\n- Intentional detach FINs the operator stream, target `serve_attach` releases its exact role, and the PTY session remains running.\n- A physical broker-client connection write that times out is poisoned and never reused because a length-prefixed frame may be partial.\n- Finished request/reply families cannot be blanket-filtered: sync/update requesters legitimately FIN their send side while awaiting a response. The terminal-on-finished rule is Attach-specific.\n- Retirement is redispatch visibility, not immediate physical teardown; removing the transport row can race a fire-and-forget reply and tear its EOF.\n\n## Current implementation response\n\n- `StreamEntry.retired` plus `NetHost::retire_stream` hide completed exchanges from `stream_infos` without tearing down their transport.\n- `finished_row_is_terminal(StreamFamily::Attach, finished)` skips and retires a finished Attach even if no earlier worker could report completion, covering pre-existing backlog and brain-down-detach orderings.\n- `OpenerCapture` pins the bounded first NDJSON record outside the ring; `stream_opener` exposes it; dispatcher `first_line` uses it before the N-1 fallback.\n- `ClaimState::{InFlight,Retry,Terminal}` and `outcome_transition` provide bounded retry for transient failure and no hot loop for served/unknown/exhausted outcomes.\n- Conn-close watcher sweeps dead connection stream rows; served workers retire their stream rows best-effort.\n\n## Existing coverage and the gap it closes\n\n- `redispatch.rs::finished_historical_attach_neither_takes_nor_clears_current_controller` is the direct field-bug regression through real rediscovery.\n- `finished_unretired_attach_is_skipped_and_drained_not_replayed` covers the brain-down-detach ordering where no old worker can report Served.\n- `finished_pending_sync_request_still_serves_across_generation_start` prevents an over-broad finished-row filter.\n- `opener_evicted_active_stream_resumes_after_dispatcher_restart` shrinks the ring, rolls out seq 0, and proves pinned classification.\n- `transient_worker_start_failure_recovers_via_claim_retry` proves retry through the real claim path.\n- The older `attach_survives_target_brain_restart_exactly_once` is insufficient because it manually picks and re-serves one known stream and never exercises production stream enumeration/classification.\n- `resume_no_control_steal_e2e` and `brain_resume_conn_deadlock` cover a different, already-fixed local supervised-brain subscription defect.\n- `brain_decouple` and `conn_blackhole_lifecycle` cover physical/non-draining IPC cleanup, not stale net-stream lifecycle.\n\n## Falsifiable hypotheses\n\n1. **Historical finished Attach replay is the primary ownership loss.** Prediction: after a dispatcher-only restart, broker lifecycle logs show a current controller attach followed by `controller-replaced` from a worker for an older stream, then detach/EOF cleanup; the rc stream remains open but post-restart PTY markers do not render. Falsifier: no finished historical Attach is enumerated/touched, and controller identity/connection never changes.\n2. **Active opener eviction causes permanent abandonment.** Prediction: affected active stream has `floor_seq > 0`; pre-fix classification from the ring begins at Input/Resize or errors, and its one-shot claim never starts `serve_attach`. Falsifier: opener remains retained/pinned, worker reaches `controller-attach`, and the same active stream forwards post-restart output.\n3. **Broad opener-error fallback plus retry amplifies recovery to 20–30 seconds.** Prediction: the same stream logs an opener-query error, fallback peek failure/block, and one or two `DispatchOutcome::Failed` retries before its eventual successful controller attach; recovery time follows those attempts rather than PTY activity. Falsifier: no opener/fallback/retry errors or recovery precedes any retry.\n4. **Half-open old Brain IPC connection is a secondary delayed stall.** Prediction: no poison occurs while the session is idle; first output causes `CONN_LIFECYCLE ... write-start`, and `CONN_WRITE_POISONED` follows one configured brain-write bound later, after which handler count/role cleanup advances. Falsifier: poison precedes any write, or the same physical conn id continues successful writes after poison.\n5. **Physical broker/QUIC loss is not the silent-freeze cause.** Prediction: rc sees neither `NetStreamEof` nor broker read error, so `attach_viewport` never enters reconnect; a true broker bounce produces a centered reconnect banner and a full re-establish. Falsifier: the frozen incident includes a severance event that rc misclassifies or ignores.\n\n## Safe deterministic feedback loop\n\nUse the existing hermetic two-broker shape; never invoke production `spt daemon refresh` or `spt update` and never touch real endpoints.\n\n1. Create isolated `SPT_HOME`; launch two in-process loopback-only `NetHost` brokers A and B; spawn a real echo PTY under A.\n2. Run the real dispatcher as `dispatch_fixture` child generation G1 against A. B acts as operator and opens one QUIC connection to A.\n3. On that same origin/connection, create historical Attach S1, drive a unique marker through the real PTY, then FIN S1. Create current Attach S2, drive a second marker, and assert it renders.\n4. Kill only G1 and start G2. Broker A, PTY child, QUIC connection, and S2 remain untouched. Send a third marker through S2. Pass condition: it renders within a bounded window, no EOF/Displaced reaches S2, S1 is absent from `stream_infos`, and S2 remains live. Capture dispatcher stderr and broker lifecycle records keyed by stream/session/physical conn.\n5. Brain-down-detach variant: kill G1 before FINning S1; verify S1 is `finished=true` but unretired; start G2 and prove it retires S1 without serving it, while S2 continues through several polls.\n6. Rolled-opener variant: set test-only ring cap to 8 before broker construction, drive more than 8 spaced records, prove `floor_seq > 0`, replace dispatcher, and assert the same stream renders another marker.\n7. Retry variant: arm `SPT_DISPATCH_INJECT_TRANSIENT_ONCE`; assert one Failed outcome, one bounded re-claim, eventual output, and exactly one controller.\n8. Timing discriminator for the newer 20–30-second observation: record monotonic timestamps for opener query result, fallback entry, each claim transition, `controller-attach`/`controller-replaced`, first post-cycle PTY output, `write-start`, poison, and successful rendered marker. This separates delayed reconstruction from output-triggered 15-second physical poison without mutating production.\n\nThis harness is deterministic because the generation boundary is a killable dispatcher child, not an in-process thread; it exercises the exact production enumeration path while preserving the broker-owned resources that define the bug."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":7769,"output":5042,"cacheRead":165376,"cacheWrite":0,"totalTokens":178187,"reasoningTokens":345,"cost":{"input":0.038845000000000005,"output":0.15126,"cacheRead":0.082688,"cacheWrite":0,"total":0.272793}},"stopReason":"toolUse","timestamp":1784247740965,"responseId":"resp_0996e78cf50e3160016a5975bd4e608199bfb8ac020c843f9f","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0996e78cf50e3160016a5975bf08b881999e47bf41be05f3e2","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWXXGkYOQD8KVPY8HIopI68LL6P6BTFC2Eg4RbWDmAJNI-N6vtP5peO0yDSsna3DjxYMvY_0yM-SRP96wE2neBDTvIpSffU7I5Qkp44wki1pHqFZtBLz2jNQRPvq-hh3OL_me6tvmO9njgOGtrBR3WOdukGw94B9nqGfO6R_DFtDydfh7cq212mkD32TPkogK6V3uHb7pf8t5YaEsonnVU9Yliqopm62d9tsmvlSZq_ov_KCVFvQM_ADu7vtPq5eZm0zoACTu8clzrrfT6CI9wdB3mkD3k5QqIU_MJYOcv_f5D21pkD4UVVjwSgb5grqeuSKClv8jFaOgdzqOOVvi_hhlXAeftXNwry_DtT6lbXZ19zMufxDlAAzVMgsmA3Cv2xbMZ1uvcS8vCw9rybJgLO0596txuZH1fnJhSO3qjbi3W5rzUEYdRe-TaUZBDamnz6Rv_wlFhvLad3ovvH79jClI-K4ybt-07damSPiRzLKPHt5LTTiNCzZs32CWmGZJbQDV9XflT9-PY8k7pkyAgKNxBVMG3S7Hai6RBQ8hQQuysoTZ0PAkZhJ-hTSfOvbURToh9cxcVTlCQ0wc-GEFP3Qi9oQ1SXq2tNoiE1p9hFXn2xaGYOmMjMySgNIDjox9kkNAi42oge0tYgDVUC1Evx2Y_V6hN78tDSH6qRln51wcaLb6oRbJn0UnTqpbW1NpiJZRvfeFxedDVy1oMm4Fj5h0MihOTC_XcsliMHpvpxy466XewaRRYvkVrO5IDR1OmBbHQniOO9y2AuNcVGngAs0Bs96DcldBWhyTsHAnr-69xe5WPMW_eOPmBcH_czjb_HCJBYXnaiIzsnbIuNdphCtSiv80m4CIsgXpRc9H09smoBlznhE2HEk2hk-3BtTIufqRyE_Z4wfeVRBmmQPvZLSCe1IqsGoip927bAAzOH9euQWWtrVhabbsPFWEy99dsaiNmvMiTcMw08qO1N7KCYPgK12o2upoH7_B271M4aM2Buo-CuSHpJlWunDsSSsMhw8lmXhRvGY_Hob_cGJ9E62U8UxtpXhLNaJBTIPZ6ak8k_I-BK8J4h5XYQVbuizWoGXzvMFCl-fMvdE10rxvRatdzOsRFM14ClG38Fi3w1MoC4bFsVq04iYvj_Y4wnIO6GGpait84kRqdYSLwtwYy4o5dmEXhAe5aYSSBpPcOnigl2_YEhhPSstkG_pfC-wwws7XcN6Kn8mlkzwb9b7gaw0uJ0w86AYjj3hbXH45jtwBvAL2qbvp0yFtBLkvplrmf6p_ncRa6r1beWpWLsUB3TdXUrqGWYYME8RtPDSuTyM-rbcWP9QrA-69srivfIcs8qItm_MfjQu5MotvsMflsM-B-ZGLM_IbCB3LQUw3oTcRZNzRRg-r8eeW3CWgf9nBXWoi6uymVC44IltvYPnZnGi8pLgq5-x-pZgj7Uvnyn_kVIvldK4i04edD5baw2F6_fkkawFLT7K_k9oEb6UHIR9BQJ7UK1ZhYw_8WSm0g3or3_Fn2E5DRGZb5nh2-tAceFODUs_bBjwU3aARzhlU-xcWFuIKoqRVTuMcfnKu2GSeq-QoPlmzlKO0eB4O0aI9k7l_rQ0qF3S8XlX0HqVmwrmV6N6KYf719OHqFDnldo4y33Ur_Bt-r5hPI9LMw7BaNznDomjqBn4vxCm3ak78y3MqLyPgZ3c0IYB_jJtvwvooAnBSLIMNSIzfoEx25F62IhVZFLGL-vzJWV5VxDwLLQ0wMy8hSDalrzK_U2MDqKq32o0xV0B26aK2MWnLNfUA_JEAOTuGOrSqPBZ1-n4kFZ9utoyv1uNHU5ibvZAGQOz9t9njiojJ9xDp_l0cAtziHO47kBcOPY45TCG-DJBKiBFCWKPUmkILGuFdsQ6Nd26YW9LLn35SoBzXCyqwba2B3CLYkfHayu_Ybv4ZKAzAt9qy4A06_qB6jB6_8C-vouz-3Z4eG994B71kWyPN5Rq-AEkOAfjVaTUi4yhkp7qUtyLjPqQqKyt9OEIAPvHmNStgyHDwR0K9ZOVYTievE74V092pg5IJLhqHs-vJL7ijKiursaj7MIPtQGoVW8yoGDhO8KWlaRZLryrylYv3qoMwUeM6wWXl-Fo3g39BswkNICrDOX4BOh--J1jvPSFrUNuoUU_59qjn9aB49vfUAjOXDE4uBqqK8NyJ6eaC6K37mfoj0TVP5ygRyq3xCmIx524PUdgRWth-ARaGm4UTR3PD677Cxu14vrYRDwXjX1VUbt-mvcBFUPZrMJdULwjdG7d4zjyn-IfEBKTK-wBkfuWTKam5vOkZBjEUjvb44Nuu_FVnlchACNBcXBA7bbbnrOb3PltsryOlqwFHAHxkd8DrZJDCRl4m2hyR6tg_IgaaxuTPej94kVyYpSSsIMZ6ITCgMu7Zyl9vsDegAKlr-qwaTj2eSffC-Olbzn0RO4lIPStGNNWWAM54emlZ77gKrFeGjqk8yJN1dPogQYRvXbqTeB7fQKIULGXdtxyY-8I56S1OrxRZ_PedKON1rgc3h4dzKax_CKdEYd_NmIqpt8DbuBS2R6J9XKx8yROSCD14dmGh-Lty--eQ_TaJkSHfS3CVJiqVLm0b8-3tK1WnNK8H2qQbbtpYkLVdNMMI366M8QklyfEt4O-_iCrtBV58SOJC7mQRMvtQz4fMMuSME8H_IREDXMW32K6y5KeLlvrPUZKx1ggt9XSx1fM2j4AIjFnMfteX4arsipVEMwiX8LkIVblGa8GRB1m_nOWwhCJk3cIr98sOG6SYpKkH8JyRomz3qE_WHWWn-YPQo4U1wpOLJxBaeG1O5P_qPj-IQR0ZTaaSKRMUTMowVrdsvjaokbQT6M5eU3zCphInyBM2VtO-XJbiSP6h3MN8alEbEruT-_cPK4XbC8PrF7uHuC2cP8qep39QUxcNlDMggHJ6WC4bXq7iG-h7efpjL6cnCYOMrqlSSwfJa_mJLVtMJBsxq3OPrqaHsyFu9FAXdIQoCXIteR4dYtPVa9hW1KOHvR5QyNJGa2uRrHjBOVdo4aImXf2ubXZI8GSI6aANiYaH3mo4s_onUaajcmu8FgWWhn-Ot6IIMT71S_uGdF1_BL6y0KmcEU4OolYNJyWR8rUeEHr-A3hJc2qCDPPhkAUy4n5x9CUqwUoUYjLLj6Digx4xuivYK0IhEcxupCOLYo7oGsrBEERdA_7-OFSFKuQCCkwkPpjypY5lTpwNayZjhMrpLh3XMu99lzdRgK92rhX_ABh4wYC2bfPHQf-9n6PKKEBtPQVxOQ==","summary":[{"type":"summary_text","text":"**Clarifying viewer and controller roles**"},{"type":"summary_text","text":"**Detailing broker reconnection and recovery**"}]},{"id":"fc_0996e78cf50e3160016a5975c67b748199883c9f390ec8cc84","type":"function_call","status":"completed","arguments":"{\"type\":null,\"result\":{\"data\":{\"summary\":\"Confirmed a broker-stream redispatch ownership defect at the refresh/update interaction boundary. The broker correctly retains the PTY, child, output log, QUIC/loopback connections, and active net streams across a brain cycle; the brain-owned target-side `serve_attach` workers do not survive and must be reconstructed. In the affected pre-fix behavior, a fresh dispatcher enumerated every peer-initiated stream still in the broker table, including finished historical Attach streams. A historical worker could silently re-take the controller because the origin identity matched, then consume its replayed EOF and clear that controller. The legitimate active viewport stayed physically open but was no longer the selected controller sink, producing the exact “terminal frozen, no disconnect banner, detach plus fresh `spt rc` restores it” symptom. The working tree now contains ADR-0038, explicit stream retirement, pinned opener metadata, typed retryable claims, and production-path regression tests. No runtime mutation or test execution was performed, per the assignment.\",\"files\":[{\"path\":\".claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt\",\"description\":\"Incident-specific source-level RCA for the HFENDULEAM refresh/update event: 4/5 endpoints froze; records exact causal chain, field log tokens, why detach+new rc recovers, and the missing production-path tests.\"},{\"path\":\"docs/adr/0038-broker-stream-lifecycle-truth.md\",\"description\":\"Accepted lifecycle decision: finished Attach rows are terminal for redispatch, classification identity must be ring-independent, claims must be retryable and typed, and legitimate same-origin successor re-take remains silent.\"},{\"path\":\"CONTEXT.md\",\"description\":\"Domain authority for broker/brain split, broker-owned PTY continuity, remote-control semantics, and controller/viewer behavior.\"},{\"path\":\"docs/KNOWN-HAZARDS.md\",\"description\":\"Relevant invariants include viewer isolation, controller writer ordering, input-ack backpressure, broker/brain separation, brain-resume no-control-steal, physical connection poisoning, and attach-wedge recovery.\"},{\"path\":\"docs/DEBUG-ROLLOUT.md\",\"description\":\"Operational boundary: refresh and brain-compatible update preserve broker-held resources; broker-touching candidates require the documented rollout posture. Useful for a safe non-production reproduction plan.\"},{\"path\":\"crates/spt/src/rc.rs\",\"description\":\"Operator side. `run_attach_inner` → `establish_attach` → `request_attach_endpoint` → `net_stream_subscribe` → `attach_viewport`/`pump`. `pump::detach` FINs the stream; automatic reconnect happens only for `BrokerGone`/`Severed`, not for a silently deselected controller sink.\"},{\"path\":\"crates/spt-daemon/src/attach.rs\",\"description\":\"Target semantic relay. `serve_attach` owns a session-subscription Brain connection plus a separate wire-forwarding Brain connection. `handle_attach_request` calls `attach_as`/`attach_as_viewer_snap`; replayed stream EOF calls `detach_session`. `read_controller_event_resuming` handles output gaps but cannot restore a controller slot stolen and cleared by another worker.\"},{\"path\":\"crates/spt-daemon/src/broker.rs\",\"description\":\"PTY role truth. `OutputLog::resolve_subscribe`, `become_controller`, `add_viewer`, `clear_controller`, `detach_if`, `controller_writer`, and `controller_epoch` implement one controller plus isolated viewers. Same-`by` controller re-subscribe is a silent re-take. `handle_conn` removes only roles belonging to the disconnected broker client while preserving sessions.\"},{\"path\":\"crates/spt-daemon/src/nethost.rs\",\"description\":\"Broker-owned data plane. `NetShared::{conns,streams}`, `StreamEntry`, `StreamLog`, `register_stream`, `stream_infos`, `retire_stream`, and `stream_opener` define stream persistence and redispatch eligibility. Current code pins the opener outside the bounded ring and hides retired rows without tearing down the transport.\"},{\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"description\":\"Brain-owned reconstruction plane. `run_dispatch_loop`, `worker`, `first_line`, `peek_first_line`, `ClaimState`, `should_claim`, `outcome_transition`, and `finished_row_is_terminal` are the exact rediscovery/classification/retry seams. Current code retires served rows and skips finished Attach rows unserved.\"},{\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"description\":\"Brain refresh boundary. `run_brain` uses cursor-only `resume_session_cursors`, then starts new net consumers via `spawn_net_consumers`; the fresh inbound dispatcher—not `BRAIN_RESUMED` itself—reconstructs active peer Attach streams.\"},{\"path\":\"crates/spt-daemon/src/conn.rs\",\"description\":\"Physical broker-client transport. `BrokerConn::write_gated`, watchdog, `poison_and_cancel`, `sever`, and `Drop` bound and retire a non-draining local IPC connection. This explains delayed `CONN_WRITE_POISONED` records but is secondary to silent controller selection loss.\"},{\"path\":\"crates/spt-term/src/pty.rs\",\"description\":\"Lowest PTY ownership layer. `PtySession` owns the master reader/writer and child; its drain and shared serialized writer remain broker-held across brain refresh.\"},{\"path\":\"crates/spt-daemon/tests/redispatch.rs\",\"description\":\"New production-path regressions using a killable real dispatcher generation: finished historical Attach cannot take/clear current control; finished-unretired Attach is skipped and drained; finished request/reply streams still serve; rolled-opener active Attach resumes; one transient worker failure retries.\"},{\"path\":\"crates/spt-daemon/tests/fixtures/dispatch_fixture.rs\",\"description\":\"Killable child running the real `dispatch::run_dispatch_loop`, allowing deterministic dispatcher-generation replacement without killing broker, PTY, or net streams.\"},{\"path\":\"crates/spt-daemon/tests/attach.rs\",\"description\":\"Existing attach/controller/viewer and target-brain restart coverage. Important gap: `attach_survives_target_brain_restart_exactly_once` manually re-served a known short stream, bypassing production rediscovery and opener classification.\"},{\"path\":\"crates/spt/tests/resume_no_control_steal_e2e.rs\",\"description\":\"Covers the earlier local `run_brain::resume_sessions` control-steal bug and validates cursor-only supervised-brain resume. It does not cover peer-stream redispatch of remote Attach records.\"},{\"path\":\"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs\",\"description\":\"Guards that supervised brain startup seeds cursors without subscribing a non-draining heartbeat connection; distinguishes the retired local resume bug from the current remote-stream dispatcher seam.\"},{\"path\":\"crates/spt-daemon/tests/brain_decouple.rs\",\"description\":\"Deterministic non-draining controller connection and logical/physical stall-eviction coverage.\"},{\"path\":\"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs\",\"description\":\"Windows physical named-pipe black-hole harness: unrelated sessions remain live, bad connection is canceled/closed, and a fresh viewer can attach. Useful companion but not a redispatch ownership test.\"},{\"path\":\"crates/spt/tests/attach_wedge_e2e.rs\",\"description\":\"Real detached-daemon guard for abrupt rc loss plus PTY child death; proves one dropped viewport cannot wedge the broker globally, but does not recreate a dispatcher generation over retained historical streams.\"}],\"architecture\":\"## Ownership and lifecycle map\\n\\n1. **PTY data plane — broker-owned and refresh-stable.** `spt-term::pty::PtySession` owns the child and PTY master reader/writer. `Broker::dispatch_spawn` stores it in `HostedSession`, starts its drain and exit-waiter, and appends child output into `OutputLog`. A brain cycle does not destroy these objects. This is why detach plus reattach can immediately recover without restarting the harness.\\n2. **Operator/viewer connection — `spt rc` side.** `rc::establish_attach` opens one broker-owned loopback stream for a local target or a QUIC stream for a remote target, sends `AttachRecord::Request`, and subscribes the operator-side Brain to that stream. `rc::pump` forwards stdin as `Input`, renders `Output`, FINs on intentional detach, and reconnects only when it observes actual broker/stream severance. A live stream that simply stops receiving output is not a reconnect-class event; after the first event, the 30-second first-event guard also no longer helps.\\n3. **Target semantic relay — brain-owned and refresh-ephemeral.** `dispatch::run_dispatch_loop` claims peer-initiated stream rows. Each Attach worker opens a fresh target-side Brain connection and enters `attach::serve_attach`. That loop subscribes to the retained net-stream ring, interprets Request/Input/Resize, subscribes to the broker PTY session as controller or viewer, and forwards session Output/Exit/Displaced over a second Brain connection. These worker threads die with the brain generation even though the stream and PTY survive.\\n4. **Controller role — one selected sink per session.** `OutputLog::resolve_subscribe` owns the policy. Viewer intent calls `add_viewer` and never owns input. Control/Take may call `become_controller`; a matching `by` identity is intentionally a silent re-take so a legitimate successor brain can reconstruct the same operator without emitting `Displaced`. `controller_epoch` stops superseded writer threads from reordering initial replay, but it is not a dispatcher-generation authorization epoch: it does not prevent a historical same-origin worker from making a fresh, policy-valid `attach_as` call.\\n5. **Viewer role — isolated, plural, non-owning.** Each viewer has a bounded SPSC queue and writer thread. Overflow evicts only that viewer and `serve_attach` re-subscribes it skip-to-live. Viewer behavior does not explain loss of input authority on four controllers.\\n6. **Broker client cleanup.** `Broker::handle_conn` tracks `my_subs` and `my_stream_subs`. On actual EOF it detaches only that physical Brain connection from its PTY roles and net-stream subscriptions; sessions, stream logs, and PTYs persist. A half-open/non-draining old brain connection may remain until an output write actually blocks and `BrokerConn`'s watchdog poisons it.\\n7. **Daemon brain refresh.** `brainproc::run_brain` now performs only `resume_session_cursors`; it does not subscribe the supervised heartbeat Brain as controller. Once net is enabled, `spawn_net_consumers` starts a brand-new dispatcher with new in-memory claim state. Thus `BRAIN_RESUMED: ... session cursors` reports PTY bookkeeping, not reconstruction of remote Attach workers.\\n8. **Broker lifecycle.** Refresh/brain-compatible update preserves broker process, PTYs, QUIC connections, stream rows, and journals. A full broker restart instead loses connections/streams and should make rc observe `BrokerGone`/`Severed` and enter its bounded establish-again loop. The incident is specifically the former case.\\n\\n## Confirmed pre-fix failure chain\\n\\n- `NetShared.streams` retained finished stream rows; `StreamLog::finish` marked EOF but did not remove them from dispatcher enumeration.\\n- Fresh `run_dispatch_loop` began with an empty one-shot claim set and enumerated all historical peer streams.\\n- A historical Attach worker replayed its `Request`, called `attach_as(... Control, by=same_origin)`, and reached `resolve_subscribe`'s intentional same-identity silent re-take. `become_controller` replaced the current sink without notifying rc.\\n- That same historical stream then replayed EOF. `serve_attach` called `detach_session`; broker `dispatch_unsubscribe`/`OutputLog::detach_if` cleared the controller that stale worker had just installed.\\n- The legitimate active serve loop remained open and still held its local `role == Controller`, but the broker no longer selected its sink. The remote rc therefore got neither new output nor a severance event. Input records could remain on the open wire while no selected controller relay applied them.\\n- Intentional detach closed the bad viewport. A new `spt rc` created a new stream and worker; it subscribed after the stale replay storm and became the selected controller, while reusing the same broker-held PTY.\\n\\n## Why most, not all\\n\\nThe stale replay effect is per-session and schedule/history dependent, not endpoint-id dependent. An endpoint is affected when it has a finished historical Attach row whose worker lands after the legitimate current worker, or when its active stream cannot be reconstructed. An endpoint with no relevant historical row, or whose legitimate active worker becomes controller last, remains live. [INFERENCE] This is consistent with 4/5 and a different endpoint later being affected, but the field logs do not map each stream id to each endpoint.\\n\\n## Automatic recovery versus indefinite freeze\\n\\nThe stale replay mechanism has no built-in 20- or 30-second recovery timer. If a stale worker is the final controller mutation, the viewport remains frozen until a fresh attach or another role-changing event. Automatic recovery can occur if the legitimate active worker finishes reconstruction after all finite historical workers and becomes controller last. [INFERENCE] Current W1 retry/fallback behavior can amplify or delay that last-worker ordering: `first_line` catches every `net_stream_opener` error and falls back to `peek_first_line`, while a `DispatchOutcome::Failed` retries up to three attempts with 500 ms then 1 s backoff. Therefore the redispatch defect is the ownership-corruption mechanism; broad `Err(_)` fallback and retry are a separate plausible cause of a deterministic 20–30-second delayed recovery and are not superseded by this RCA.\\n\\nThe delayed connection-poison evidence is also secondary. A non-draining old controller connection has `write_blocked_since == None` while the PTY is idle, so heartbeat `sessions()` polls cannot classify it as blocked. The first later PTY output enters `controller_writer` → `BrokerConn::write`, arms the 15-second watchdog, and then produces `CONN_WRITE_POISONED` approximately 15 seconds later. A minutes-long gap from refresh to `write-start` therefore means “no attempted output write yet,” not a minutes-long watchdog. Poisoning then forces broker EOF cleanup and may release a role, but it does not account for the immediate silent deselection caused by historical replay.\\n\\n## Invariants\\n\\n- Broker refresh must preserve PTY, child, output ring, effect journal, QUIC connection, and active stream.\\n- Brain refresh must reconstruct only live peer streams; terminal Attach exchanges must never be served again.\\n- Stream family identity must survive ring eviction; correctness metadata cannot live only in a bounded transport ring.\\n- Same-origin re-take must remain silent for the legitimate successor, so origin identity cannot distinguish stale from live replay; lifecycle state must.\\n- Exactly one controller sink is authoritative; viewer sinks never own PTY input and cannot block the drain/controller.\\n- `controller_epoch` protects writer ordering, not stream freshness or dispatcher generation.\\n- Intentional detach FINs the operator stream, target `serve_attach` releases its exact role, and the PTY session remains running.\\n- A physical broker-client connection write that times out is poisoned and never reused because a length-prefixed frame may be partial.\\n- Finished request/reply families cannot be blanket-filtered: sync/update requesters legitimately FIN their send side while awaiting a response. The terminal-on-finished rule is Attach-specific.\\n- Retirement is redispatch visibility, not immediate physical teardown; removing the transport row can race a fire-and-forget reply and tear its EOF.\\n\\n## Current implementation response\\n\\n- `StreamEntry.retired` plus `NetHost::retire_stream` hide completed exchanges from `stream_infos` without tearing down their transport.\\n- `finished_row_is_terminal(StreamFamily::Attach, finished)` skips and retires a finished Attach even if no earlier worker could report completion, covering pre-existing backlog and brain-down-detach orderings.\\n- `OpenerCapture` pins the bounded first NDJSON record outside the ring; `stream_opener` exposes it; dispatcher `first_line` uses it before the N-1 fallback.\\n- `ClaimState::{InFlight,Retry,Terminal}` and `outcome_transition` provide bounded retry for transient failure and no hot loop for served/unknown/exhausted outcomes.\\n- Conn-close watcher sweeps dead connection stream rows; served workers retire their stream rows best-effort.\\n\\n## Existing coverage and the gap it closes\\n\\n- `redispatch.rs::finished_historical_attach_neither_takes_nor_clears_current_controller` is the direct field-bug regression through real rediscovery.\\n- `finished_unretired_attach_is_skipped_and_drained_not_replayed` covers the brain-down-detach ordering where no old worker can report Served.\\n- `finished_pending_sync_request_still_serves_across_generation_start` prevents an over-broad finished-row filter.\\n- `opener_evicted_active_stream_resumes_after_dispatcher_restart` shrinks the ring, rolls out seq 0, and proves pinned classification.\\n- `transient_worker_start_failure_recovers_via_claim_retry` proves retry through the real claim path.\\n- The older `attach_survives_target_brain_restart_exactly_once` is insufficient because it manually picks and re-serves one known stream and never exercises production stream enumeration/classification.\\n- `resume_no_control_steal_e2e` and `brain_resume_conn_deadlock` cover a different, already-fixed local supervised-brain subscription defect.\\n- `brain_decouple` and `conn_blackhole_lifecycle` cover physical/non-draining IPC cleanup, not stale net-stream lifecycle.\\n\\n## Falsifiable hypotheses\\n\\n1. **Historical finished Attach replay is the primary ownership loss.** Prediction: after a dispatcher-only restart, broker lifecycle logs show a current controller attach followed by `controller-replaced` from a worker for an older stream, then detach/EOF cleanup; the rc stream remains open but post-restart PTY markers do not render. Falsifier: no finished historical Attach is enumerated/touched, and controller identity/connection never changes.\\n2. **Active opener eviction causes permanent abandonment.** Prediction: affected active stream has `floor_seq > 0`; pre-fix classification from the ring begins at Input/Resize or errors, and its one-shot claim never starts `serve_attach`. Falsifier: opener remains retained/pinned, worker reaches `controller-attach`, and the same active stream forwards post-restart output.\\n3. **Broad opener-error fallback plus retry amplifies recovery to 20–30 seconds.** Prediction: the same stream logs an opener-query error, fallback peek failure/block, and one or two `DispatchOutcome::Failed` retries before its eventual successful controller attach; recovery time follows those attempts rather than PTY activity. Falsifier: no opener/fallback/retry errors or recovery precedes any retry.\\n4. **Half-open old Brain IPC connection is a secondary delayed stall.** Prediction: no poison occurs while the session is idle; first output causes `CONN_LIFECYCLE ... write-start`, and `CONN_WRITE_POISONED` follows one configured brain-write bound later, after which handler count/role cleanup advances. Falsifier: poison precedes any write, or the same physical conn id continues successful writes after poison.\\n5. **Physical broker/QUIC loss is not the silent-freeze cause.** Prediction: rc sees neither `NetStreamEof` nor broker read error, so `attach_viewport` never enters reconnect; a true broker bounce produces a centered reconnect banner and a full re-establish. Falsifier: the frozen incident includes a severance event that rc misclassifies or ignores.\\n\\n## Safe deterministic feedback loop\\n\\nUse the existing hermetic two-broker shape; never invoke production `spt daemon refresh` or `spt update` and never touch real endpoints.\\n\\n1. Create isolated `SPT_HOME`; launch two in-process loopback-only `NetHost` brokers A and B; spawn a real echo PTY under A.\\n2. Run the real dispatcher as `dispatch_fixture` child generation G1 against A. B acts as operator and opens one QUIC connection to A.\\n3. On that same origin/connection, create historical Attach S1, drive a unique marker through the real PTY, then FIN S1. Create current Attach S2, drive a second marker, and assert it renders.\\n4. Kill only G1 and start G2. Broker A, PTY child, QUIC connection, and S2 remain untouched. Send a third marker through S2. Pass condition: it renders within a bounded window, no EOF/Displaced reaches S2, S1 is absent from `stream_infos`, and S2 remains live. Capture dispatcher stderr and broker lifecycle records keyed by stream/session/physical conn.\\n5. Brain-down-detach variant: kill G1 before FINning S1; verify S1 is `finished=true` but unretired; start G2 and prove it retires S1 without serving it, while S2 continues through several polls.\\n6. Rolled-opener variant: set test-only ring cap to 8 before broker construction, drive more than 8 spaced records, prove `floor_seq > 0`, replace dispatcher, and assert the same stream renders another marker.\\n7. Retry variant: arm `SPT_DISPATCH_INJECT_TRANSIENT_ONCE`; assert one Failed outcome, one bounded re-claim, eventual output, and exactly one controller.\\n8. Timing discriminator for the newer 20–30-second observation: record monotonic timestamps for opener query result, fallback entry, each claim transition, `controller-attach`/`controller-replaced`, first post-cycle PTY output, `write-start`, poison, and successful rendered marker. This separates delayed reconstruction from output-triggered 15-second physical poison without mutating production.\\n\\nThis harness is deterministic because the generation boundary is a killable dispatcher child, not an in-process thread; it exercises the exact production enumeration path while preserving the broker-owned resources that define the bug.\"}}}","call_id":"call_ZLZcHsrgGR3tiC6JuaTy9On4","name":"yield"}]},"duration":103392.96220000088,"ttft":2058.940300002694,"contextSnapshot":{"promptTokens":173145,"nonMessageTokens":15431}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZLZcHsrgGR3tiC6JuaTy9On4|fc_0996e78cf50e3160016a5975c67b748199883c9f390ec8cc84","toolName":"yield","startedAt":"2026-07-17T00:24:04.361Z"},"id":"597c1617","parentId":"4bf11477","timestamp":"2026-07-17T00:24:04.361Z"}
{"type":"message","id":"2561e979","parentId":"597c1617","timestamp":"2026-07-17T00:24:04.362Z","message":{"role":"toolResult","toolCallId":"call_ZLZcHsrgGR3tiC6JuaTy9On4|fc_0996e78cf50e3160016a5975c67b748199883c9f390ec8cc84","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"Confirmed a broker-stream redispatch ownership defect at the refresh/update interaction boundary. The broker correctly retains the PTY, child, output log, QUIC/loopback connections, and active net streams across a brain cycle; the brain-owned target-side `serve_attach` workers do not survive and must be reconstructed. In the affected pre-fix behavior, a fresh dispatcher enumerated every peer-initiated stream still in the broker table, including finished historical Attach streams. A historical worker could silently re-take the controller because the origin identity matched, then consume its replayed EOF and clear that controller. The legitimate active viewport stayed physically open but was no longer the selected controller sink, producing the exact “terminal frozen, no disconnect banner, detach plus fresh `spt rc` restores it” symptom. The working tree now contains ADR-0038, explicit stream retirement, pinned opener metadata, typed retryable claims, and production-path regression tests. No runtime mutation or test execution was performed, per the assignment.","files":[{"path":".claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt","description":"Incident-specific source-level RCA for the HFENDULEAM refresh/update event: 4/5 endpoints froze; records exact causal chain, field log tokens, why detach+new rc recovers, and the missing production-path tests."},{"path":"docs/adr/0038-broker-stream-lifecycle-truth.md","description":"Accepted lifecycle decision: finished Attach rows are terminal for redispatch, classification identity must be ring-independent, claims must be retryable and typed, and legitimate same-origin successor re-take remains silent."},{"path":"CONTEXT.md","description":"Domain authority for broker/brain split, broker-owned PTY continuity, remote-control semantics, and controller/viewer behavior."},{"path":"docs/KNOWN-HAZARDS.md","description":"Relevant invariants include viewer isolation, controller writer ordering, input-ack backpressure, broker/brain separation, brain-resume no-control-steal, physical connection poisoning, and attach-wedge recovery."},{"path":"docs/DEBUG-ROLLOUT.md","description":"Operational boundary: refresh and brain-compatible update preserve broker-held resources; broker-touching candidates require the documented rollout posture. Useful for a safe non-production reproduction plan."},{"path":"crates/spt/src/rc.rs","description":"Operator side. `run_attach_inner` → `establish_attach` → `request_attach_endpoint` → `net_stream_subscribe` → `attach_viewport`/`pump`. `pump::detach` FINs the stream; automatic reconnect happens only for `BrokerGone`/`Severed`, not for a silently deselected controller sink."},{"path":"crates/spt-daemon/src/attach.rs","description":"Target semantic relay. `serve_attach` owns a session-subscription Brain connection plus a separate wire-forwarding Brain connection. `handle_attach_request` calls `attach_as`/`attach_as_viewer_snap`; replayed stream EOF calls `detach_session`. `read_controller_event_resuming` handles output gaps but cannot restore a controller slot stolen and cleared by another worker."},{"path":"crates/spt-daemon/src/broker.rs","description":"PTY role truth. `OutputLog::resolve_subscribe`, `become_controller`, `add_viewer`, `clear_controller`, `detach_if`, `controller_writer`, and `controller_epoch` implement one controller plus isolated viewers. Same-`by` controller re-subscribe is a silent re-take. `handle_conn` removes only roles belonging to the disconnected broker client while preserving sessions."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Broker-owned data plane. `NetShared::{conns,streams}`, `StreamEntry`, `StreamLog`, `register_stream`, `stream_infos`, `retire_stream`, and `stream_opener` define stream persistence and redispatch eligibility. Current code pins the opener outside the bounded ring and hides retired rows without tearing down the transport."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"Brain-owned reconstruction plane. `run_dispatch_loop`, `worker`, `first_line`, `peek_first_line`, `ClaimState`, `should_claim`, `outcome_transition`, and `finished_row_is_terminal` are the exact rediscovery/classification/retry seams. Current code retires served rows and skips finished Attach rows unserved."},{"path":"crates/spt-daemon/src/brainproc.rs","description":"Brain refresh boundary. `run_brain` uses cursor-only `resume_session_cursors`, then starts new net consumers via `spawn_net_consumers`; the fresh inbound dispatcher—not `BRAIN_RESUMED` itself—reconstructs active peer Attach streams."},{"path":"crates/spt-daemon/src/conn.rs","description":"Physical broker-client transport. `BrokerConn::write_gated`, watchdog, `poison_and_cancel`, `sever`, and `Drop` bound and retire a non-draining local IPC connection. This explains delayed `CONN_WRITE_POISONED` records but is secondary to silent controller selection loss."},{"path":"crates/spt-term/src/pty.rs","description":"Lowest PTY ownership layer. `PtySession` owns the master reader/writer and child; its drain and shared serialized writer remain broker-held across brain refresh."},{"path":"crates/spt-daemon/tests/redispatch.rs","description":"New production-path regressions using a killable real dispatcher generation: finished historical Attach cannot take/clear current control; finished-unretired Attach is skipped and drained; finished request/reply streams still serve; rolled-opener active Attach resumes; one transient worker failure retries."},{"path":"crates/spt-daemon/tests/fixtures/dispatch_fixture.rs","description":"Killable child running the real `dispatch::run_dispatch_loop`, allowing deterministic dispatcher-generation replacement without killing broker, PTY, or net streams."},{"path":"crates/spt-daemon/tests/attach.rs","description":"Existing attach/controller/viewer and target-brain restart coverage. Important gap: `attach_survives_target_brain_restart_exactly_once` manually re-served a known short stream, bypassing production rediscovery and opener classification."},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","description":"Covers the earlier local `run_brain::resume_sessions` control-steal bug and validates cursor-only supervised-brain resume. It does not cover peer-stream redispatch of remote Attach records."},{"path":"crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","description":"Guards that supervised brain startup seeds cursors without subscribing a non-draining heartbeat connection; distinguishes the retired local resume bug from the current remote-stream dispatcher seam."},{"path":"crates/spt-daemon/tests/brain_decouple.rs","description":"Deterministic non-draining controller connection and logical/physical stall-eviction coverage."},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","description":"Windows physical named-pipe black-hole harness: unrelated sessions remain live, bad connection is canceled/closed, and a fresh viewer can attach. Useful companion but not a redispatch ownership test."},{"path":"crates/spt/tests/attach_wedge_e2e.rs","description":"Real detached-daemon guard for abrupt rc loss plus PTY child death; proves one dropped viewport cannot wedge the broker globally, but does not recreate a dispatcher generation over retained historical streams."}],"architecture":"## Ownership and lifecycle map\n\n1. **PTY data plane — broker-owned and refresh-stable.** `spt-term::pty::PtySession` owns the child and PTY master reader/writer. `Broker::dispatch_spawn` stores it in `HostedSession`, starts its drain and exit-waiter, and appends child output into `OutputLog`. A brain cycle does not destroy these objects. This is why detach plus reattach can immediately recover without restarting the harness.\n2. **Operator/viewer connection — `spt rc` side.** `rc::establish_attach` opens one broker-owned loopback stream for a local target or a QUIC stream for a remote target, sends `AttachRecord::Request`, and subscribes the operator-side Brain to that stream. `rc::pump` forwards stdin as `Input`, renders `Output`, FINs on intentional detach, and reconnects only when it observes actual broker/stream severance. A live stream that simply stops receiving output is not a reconnect-class event; after the first event, the 30-second first-event guard also no longer helps.\n3. **Target semantic relay — brain-owned and refresh-ephemeral.** `dispatch::run_dispatch_loop` claims peer-initiated stream rows. Each Attach worker opens a fresh target-side Brain connection and enters `attach::serve_attach`. That loop subscribes to the retained net-stream ring, interprets Request/Input/Resize, subscribes to the broker PTY session as controller or viewer, and forwards session Output/Exit/Displaced over a second Brain connection. These worker threads die with the brain generation even though the stream and PTY survive.\n4. **Controller role — one selected sink per session.** `OutputLog::resolve_subscribe` owns the policy. Viewer intent calls `add_viewer` and never owns input. Control/Take may call `become_controller`; a matching `by` identity is intentionally a silent re-take so a legitimate successor brain can reconstruct the same operator without emitting `Displaced`. `controller_epoch` stops superseded writer threads from reordering initial replay, but it is not a dispatcher-generation authorization epoch: it does not prevent a historical same-origin worker from making a fresh, policy-valid `attach_as` call.\n5. **Viewer role — isolated, plural, non-owning.** Each viewer has a bounded SPSC queue and writer thread. Overflow evicts only that viewer and `serve_attach` re-subscribes it skip-to-live. Viewer behavior does not explain loss of input authority on four controllers.\n6. **Broker client cleanup.** `Broker::handle_conn` tracks `my_subs` and `my_stream_subs`. On actual EOF it detaches only that physical Brain connection from its PTY roles and net-stream subscriptions; sessions, stream logs, and PTYs persist. A half-open/non-draining old brain connection may remain until an output write actually blocks and `BrokerConn`'s watchdog poisons it.\n7. **Daemon brain refresh.** `brainproc::run_brain` now performs only `resume_session_cursors`; it does not subscribe the supervised heartbeat Brain as controller. Once net is enabled, `spawn_net_consumers` starts a brand-new dispatcher with new in-memory claim state. Thus `BRAIN_RESUMED: ... session cursors` reports PTY bookkeeping, not reconstruction of remote Attach workers.\n8. **Broker lifecycle.** Refresh/brain-compatible update preserves broker process, PTYs, QUIC connections, stream rows, and journals. A full broker restart instead loses connections/streams and should make rc observe `BrokerGone`/`Severed` and enter its bounded establish-again loop. The incident is specifically the former case.\n\n## Confirmed pre-fix failure chain\n\n- `NetShared.streams` retained finished stream rows; `StreamLog::finish` marked EOF but did not remove them from dispatcher enumeration.\n- Fresh `run_dispatch_loop` began with an empty one-shot claim set and enumerated all historical peer streams.\n- A historical Attach worker replayed its `Request`, called `attach_as(... Control, by=same_origin)`, and reached `resolve_subscribe`'s intentional same-identity silent re-take. `become_controller` replaced the current sink without notifying rc.\n- That same historical stream then replayed EOF. `serve_attach` called `detach_session`; broker `dispatch_unsubscribe`/`OutputLog::detach_if` cleared the controller that stale worker had just installed.\n- The legitimate active serve loop remained open and still held its local `role == Controller`, but the broker no longer selected its sink. The remote rc therefore got neither new output nor a severance event. Input records could remain on the open wire while no selected controller relay applied them.\n- Intentional detach closed the bad viewport. A new `spt rc` created a new stream and worker; it subscribed after the stale replay storm and became the selected controller, while reusing the same broker-held PTY.\n\n## Why most, not all\n\nThe stale replay effect is per-session and schedule/history dependent, not endpoint-id dependent. An endpoint is affected when it has a finished historical Attach row whose worker lands after the legitimate current worker, or when its active stream cannot be reconstructed. An endpoint with no relevant historical row, or whose legitimate active worker becomes controller last, remains live. [INFERENCE] This is consistent with 4/5 and a different endpoint later being affected, but the field logs do not map each stream id to each endpoint.\n\n## Automatic recovery versus indefinite freeze\n\nThe stale replay mechanism has no built-in 20- or 30-second recovery timer. If a stale worker is the final controller mutation, the viewport remains frozen until a fresh attach or another role-changing event. Automatic recovery can occur if the legitimate active worker finishes reconstruction after all finite historical workers and becomes controller last. [INFERENCE] Current W1 retry/fallback behavior can amplify or delay that last-worker ordering: `first_line` catches every `net_stream_opener` error and falls back to `peek_first_line`, while a `DispatchOutcome::Failed` retries up to three attempts with 500 ms then 1 s backoff. Therefore the redispatch defect is the ownership-corruption mechanism; broad `Err(_)` fallback and retry are a separate plausible cause of a deterministic 20–30-second delayed recovery and are not superseded by this RCA.\n\nThe delayed connection-poison evidence is also secondary. A non-draining old controller connection has `write_blocked_since == None` while the PTY is idle, so heartbeat `sessions()` polls cannot classify it as blocked. The first later PTY output enters `controller_writer` → `BrokerConn::write`, arms the 15-second watchdog, and then produces `CONN_WRITE_POISONED` approximately 15 seconds later. A minutes-long gap from refresh to `write-start` therefore means “no attempted output write yet,” not a minutes-long watchdog. Poisoning then forces broker EOF cleanup and may release a role, but it does not account for the immediate silent deselection caused by historical replay.\n\n## Invariants\n\n- Broker refresh must preserve PTY, child, output ring, effect journal, QUIC connection, and active stream.\n- Brain refresh must reconstruct only live peer streams; terminal Attach exchanges must never be served again.\n- Stream family identity must survive ring eviction; correctness metadata cannot live only in a bounded transport ring.\n- Same-origin re-take must remain silent for the legitimate successor, so origin identity cannot distinguish stale from live replay; lifecycle state must.\n- Exactly one controller sink is authoritative; viewer sinks never own PTY input and cannot block the drain/controller.\n- `controller_epoch` protects writer ordering, not stream freshness or dispatcher generation.\n- Intentional detach FINs the operator stream, target `serve_attach` releases its exact role, and the PTY session remains running.\n- A physical broker-client connection write that times out is poisoned and never reused because a length-prefixed frame may be partial.\n- Finished request/reply families cannot be blanket-filtered: sync/update requesters legitimately FIN their send side while awaiting a response. The terminal-on-finished rule is Attach-specific.\n- Retirement is redispatch visibility, not immediate physical teardown; removing the transport row can race a fire-and-forget reply and tear its EOF.\n\n## Current implementation response\n\n- `StreamEntry.retired` plus `NetHost::retire_stream` hide completed exchanges from `stream_infos` without tearing down their transport.\n- `finished_row_is_terminal(StreamFamily::Attach, finished)` skips and retires a finished Attach even if no earlier worker could report completion, covering pre-existing backlog and brain-down-detach orderings.\n- `OpenerCapture` pins the bounded first NDJSON record outside the ring; `stream_opener` exposes it; dispatcher `first_line` uses it before the N-1 fallback.\n- `ClaimState::{InFlight,Retry,Terminal}` and `outcome_transition` provide bounded retry for transient failure and no hot loop for served/unknown/exhausted outcomes.\n- Conn-close watcher sweeps dead connection stream rows; served workers retire their stream rows best-effort.\n\n## Existing coverage and the gap it closes\n\n- `redispatch.rs::finished_historical_attach_neither_takes_nor_clears_current_controller` is the direct field-bug regression through real rediscovery.\n- `finished_unretired_attach_is_skipped_and_drained_not_replayed` covers the brain-down-detach ordering where no old worker can report Served.\n- `finished_pending_sync_request_still_serves_across_generation_start` prevents an over-broad finished-row filter.\n- `opener_evicted_active_stream_resumes_after_dispatcher_restart` shrinks the ring, rolls out seq 0, and proves pinned classification.\n- `transient_worker_start_failure_recovers_via_claim_retry` proves retry through the real claim path.\n- The older `attach_survives_target_brain_restart_exactly_once` is insufficient because it manually picks and re-serves one known stream and never exercises production stream enumeration/classification.\n- `resume_no_control_steal_e2e` and `brain_resume_conn_deadlock` cover a different, already-fixed local supervised-brain subscription defect.\n- `brain_decouple` and `conn_blackhole_lifecycle` cover physical/non-draining IPC cleanup, not stale net-stream lifecycle.\n\n## Falsifiable hypotheses\n\n1. **Historical finished Attach replay is the primary ownership loss.** Prediction: after a dispatcher-only restart, broker lifecycle logs show a current controller attach followed by `controller-replaced` from a worker for an older stream, then detach/EOF cleanup; the rc stream remains open but post-restart PTY markers do not render. Falsifier: no finished historical Attach is enumerated/touched, and controller identity/connection never changes.\n2. **Active opener eviction causes permanent abandonment.** Prediction: affected active stream has `floor_seq > 0`; pre-fix classification from the ring begins at Input/Resize or errors, and its one-shot claim never starts `serve_attach`. Falsifier: opener remains retained/pinned, worker reaches `controller-attach`, and the same active stream forwards post-restart output.\n3. **Broad opener-error fallback plus retry amplifies recovery to 20–30 seconds.** Prediction: the same stream logs an opener-query error, fallback peek failure/block, and one or two `DispatchOutcome::Failed` retries before its eventual successful controller attach; recovery time follows those attempts rather than PTY activity. Falsifier: no opener/fallback/retry errors or recovery precedes any retry.\n4. **Half-open old Brain IPC connection is a secondary delayed stall.** Prediction: no poison occurs while the session is idle; first output causes `CONN_LIFECYCLE ... write-start`, and `CONN_WRITE_POISONED` follows one configured brain-write bound later, after which handler count/role cleanup advances. Falsifier: poison precedes any write, or the same physical conn id continues successful writes after poison.\n5. **Physical broker/QUIC loss is not the silent-freeze cause.** Prediction: rc sees neither `NetStreamEof` nor broker read error, so `attach_viewport` never enters reconnect; a true broker bounce produces a centered reconnect banner and a full re-establish. Falsifier: the frozen incident includes a severance event that rc misclassifies or ignores.\n\n## Safe deterministic feedback loop\n\nUse the existing hermetic two-broker shape; never invoke production `spt daemon refresh` or `spt update` and never touch real endpoints.\n\n1. Create isolated `SPT_HOME`; launch two in-process loopback-only `NetHost` brokers A and B; spawn a real echo PTY under A.\n2. Run the real dispatcher as `dispatch_fixture` child generation G1 against A. B acts as operator and opens one QUIC connection to A.\n3. On that same origin/connection, create historical Attach S1, drive a unique marker through the real PTY, then FIN S1. Create current Attach S2, drive a second marker, and assert it renders.\n4. Kill only G1 and start G2. Broker A, PTY child, QUIC connection, and S2 remain untouched. Send a third marker through S2. Pass condition: it renders within a bounded window, no EOF/Displaced reaches S2, S1 is absent from `stream_infos`, and S2 remains live. Capture dispatcher stderr and broker lifecycle records keyed by stream/session/physical conn.\n5. Brain-down-detach variant: kill G1 before FINning S1; verify S1 is `finished=true` but unretired; start G2 and prove it retires S1 without serving it, while S2 continues through several polls.\n6. Rolled-opener variant: set test-only ring cap to 8 before broker construction, drive more than 8 spaced records, prove `floor_seq > 0`, replace dispatcher, and assert the same stream renders another marker.\n7. Retry variant: arm `SPT_DISPATCH_INJECT_TRANSIENT_ONCE`; assert one Failed outcome, one bounded re-claim, eventual output, and exactly one controller.\n8. Timing discriminator for the newer 20–30-second observation: record monotonic timestamps for opener query result, fallback entry, each claim transition, `controller-attach`/`controller-replaced`, first post-cycle PTY output, `write-start`, poison, and successful rendered marker. This separates delayed reconstruction from output-triggered 15-second physical poison without mutating production.\n\nThis harness is deterministic because the generation boundary is a killable dispatcher child, not an in-process thread; it exercises the exact production enumeration path while preserving the broker-owned resources that define the bug."},"status":"success"},"isError":false,"timestamp":1784247844361}}
{"type":"message","id":"9cd1811d","parentId":"2561e979","timestamp":"2026-07-17T00:24:07.486Z","message":{"role":"assistant","content":[{"type":"text","text":"","textSignature":"{\"v\":1,\"id\":\"msg_0996e78cf50e3160016a59762649508199a57cbb87440cd897\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":10777,"output":4,"cacheRead":167424,"cacheWrite":0,"totalTokens":178205,"cost":{"input":0.053885,"output":0.00012,"cacheRead":0.083712,"cacheWrite":0,"total":0.137717}},"stopReason":"stop","timestamp":1784247844371,"responseId":"resp_0996e78cf50e3160016a597624c8d4819982b18301bb1faae2","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"msg_0996e78cf50e3160016a59762649508199a57cbb87440cd897","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"phase":"final_answer","role":"assistant"}]},"duration":3105.0416000038385,"ttft":1941.2082000002265,"contextSnapshot":{"promptTokens":178201,"nonMessageTokens":15431}}}
{"type":"custom","customType":"session_exit","data":{"reason":"dispose","kind":"normal","recordedAt":"2026-07-17T00:31:07.591Z"},"id":"3878a532","parentId":"9cd1811d","timestamp":"2026-07-17T00:31:07.591Z"}
